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

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        :return: True if the vault lock is not set, False otherwise.
378        """
379        return self._vault['lock'] is None

Check if the vault lock is currently not set.

Returns

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

def lock(self) -> int:
381    def lock(self) -> int:
382        """
383        Acquires a lock on the ZakatTracker instance.
384
385        Returns:
386        int: The lock ID. This ID can be used to release the lock later.
387        """
388        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:
390    def box(self) -> dict:
391        """
392        Returns a copy of the internal vault dictionary.
393
394        This method is used to retrieve the current state of the ZakatTracker object.
395        It provides a snapshot of the internal data structure, allowing for further
396        processing or analysis.
397
398        :return: A copy of the internal vault dictionary.
399        """
400        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

A copy of the internal vault dictionary.

def steps(self) -> dict:
402    def steps(self) -> dict:
403        """
404        Returns a copy of the history of steps taken in the ZakatTracker.
405
406        The history is a dictionary where each key is a unique identifier for a step,
407        and the corresponding value is a dictionary containing information about the step.
408
409        :return: A copy of the history of steps taken in the ZakatTracker.
410        """
411        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

A copy of the history of steps taken in the ZakatTracker.

def free(self, lock: int, auto_save: bool = True) -> bool:
413    def free(self, lock: int, auto_save: bool = True) -> bool:
414        """
415        Releases the lock on the database.
416
417        Parameters:
418        lock (int): The lock ID to be released.
419        auto_save (bool): Whether to automatically save the database after releasing the lock.
420
421        Returns:
422        bool: True if the lock is successfully released and (optionally) saved, False otherwise.
423        """
424        if lock == self._vault['lock']:
425            self._vault['lock'] = None
426            if auto_save:
427                return self.save(self.path())
428            return True
429        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:
431    def account_exists(self, account) -> bool:
432        """
433        Check if the given account exists in the vault.
434
435        Parameters:
436        account (str): The account number to check.
437
438        Returns:
439        bool: True if the account exists, False otherwise.
440        """
441        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:
443    def box_size(self, account) -> int:
444        """
445        Calculate the size of the box for a specific account.
446
447        Parameters:
448        account (str): The account number for which the box size needs to be calculated.
449
450        Returns:
451        int: The size of the box for the given account. If the account does not exist, -1 is returned.
452        """
453        if self.account_exists(account):
454            return len(self._vault['account'][account]['box'])
455        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:
457    def log_size(self, account) -> int:
458        """
459        Get the size of the log for a specific account.
460
461        Parameters:
462        account (str): The account number for which the log size needs to be calculated.
463
464        Returns:
465        int: The size of the log for the given account. If the account does not exist, -1 is returned.
466        """
467        if self.account_exists(account):
468            return len(self._vault['account'][account]['log'])
469        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:
471    def recall(self, dry=True, debug=False) -> bool:
472        """
473        Revert the last operation.
474
475        Parameters:
476        dry (bool): If True, the function will not modify the data, but will simulate the operation. Default is True.
477        debug (bool): If True, the function will print debug information. Default is False.
478
479        Returns:
480        bool: True if the operation was successful, False otherwise.
481        """
482        if not self.nolock() or len(self._vault['history']) == 0:
483            return False
484        if len(self._vault['history']) <= 0:
485            return False
486        ref = sorted(self._vault['history'].keys())[-1]
487        if debug:
488            print('recall', ref)
489        memory = self._vault['history'][ref]
490        if debug:
491            print(type(memory), 'memory', memory)
492
493        limit = len(memory) + 1
494        sub_positive_log_negative = 0
495        for i in range(-1, -limit, -1):
496            x = memory[i]
497            if debug:
498                print(type(x), x)
499            match x['action']:
500                case Action.CREATE:
501                    if x['account'] is not None:
502                        if self.account_exists(x['account']):
503                            if debug:
504                                print('account', self._vault['account'][x['account']])
505                            assert len(self._vault['account'][x['account']]['box']) == 0
506                            assert self._vault['account'][x['account']]['balance'] == 0
507                            assert self._vault['account'][x['account']]['count'] == 0
508                            if dry:
509                                continue
510                            del self._vault['account'][x['account']]
511
512                case Action.TRACK:
513                    if x['account'] is not None:
514                        if self.account_exists(x['account']):
515                            if dry:
516                                continue
517                            self._vault['account'][x['account']]['balance'] -= x['value']
518                            self._vault['account'][x['account']]['count'] -= 1
519                            del self._vault['account'][x['account']]['box'][x['ref']]
520
521                case Action.LOG:
522                    if x['account'] is not None:
523                        if self.account_exists(x['account']):
524                            if x['ref'] in self._vault['account'][x['account']]['log']:
525                                if dry:
526                                    continue
527                                if sub_positive_log_negative == -x['value']:
528                                    self._vault['account'][x['account']]['count'] -= 1
529                                    sub_positive_log_negative = 0
530                                del self._vault['account'][x['account']]['log'][x['ref']]
531
532                case Action.SUB:
533                    if x['account'] is not None:
534                        if self.account_exists(x['account']):
535                            if x['ref'] in self._vault['account'][x['account']]['box']:
536                                if dry:
537                                    continue
538                                self._vault['account'][x['account']]['box'][x['ref']]['rest'] += x['value']
539                                self._vault['account'][x['account']]['balance'] += x['value']
540                                sub_positive_log_negative = x['value']
541
542                case Action.ADD_FILE:
543                    if x['account'] is not None:
544                        if self.account_exists(x['account']):
545                            if x['ref'] in self._vault['account'][x['account']]['log']:
546                                if x['file'] in self._vault['account'][x['account']]['log'][x['ref']]['file']:
547                                    if dry:
548                                        continue
549                                    del self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']]
550
551                case Action.REMOVE_FILE:
552                    if x['account'] is not None:
553                        if self.account_exists(x['account']):
554                            if x['ref'] in self._vault['account'][x['account']]['log']:
555                                if dry:
556                                    continue
557                                self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']] = x['value']
558
559                case Action.BOX_TRANSFER:
560                    if x['account'] is not None:
561                        if self.account_exists(x['account']):
562                            if x['ref'] in self._vault['account'][x['account']]['box']:
563                                if dry:
564                                    continue
565                                self._vault['account'][x['account']]['box'][x['ref']]['rest'] -= x['value']
566
567                case Action.EXCHANGE:
568                    if x['account'] is not None:
569                        if x['account'] in self._vault['exchange']:
570                            if x['ref'] in self._vault['exchange'][x['account']]:
571                                if dry:
572                                    continue
573                                del self._vault['exchange'][x['account']][x['ref']]
574
575                case Action.REPORT:
576                    if x['ref'] in self._vault['report']:
577                        if dry:
578                            continue
579                        del self._vault['report'][x['ref']]
580
581                case Action.ZAKAT:
582                    if x['account'] is not None:
583                        if self.account_exists(x['account']):
584                            if x['ref'] in self._vault['account'][x['account']]['box']:
585                                if x['key'] in self._vault['account'][x['account']]['box'][x['ref']]:
586                                    if dry:
587                                        continue
588                                    match x['math']:
589                                        case MathOperation.ADDITION:
590                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] -= x[
591                                                'value']
592                                        case MathOperation.EQUAL:
593                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] = x['value']
594                                        case MathOperation.SUBTRACTION:
595                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] += x[
596                                                'value']
597
598        if not dry:
599            del self._vault['history'][ref]
600        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:
602    def ref_exists(self, account: str, ref_type: str, ref: int) -> bool:
603        """
604        Check if a specific reference (transaction) exists in the vault for a given account and reference type.
605
606        Parameters:
607        account (str): The account number for which to check the existence of the reference.
608        ref_type (str): The type of reference (e.g., 'box', 'log', etc.).
609        ref (int): The reference (transaction) number to check for existence.
610
611        Returns:
612        bool: True if the reference exists for the given account and reference type, False otherwise.
613        """
614        if account in self._vault['account']:
615            return ref in self._vault['account'][account][ref_type]
616        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:
618    def box_exists(self, account: str, ref: int) -> bool:
619        """
620        Check if a specific box (transaction) exists in the vault for a given account and reference.
621
622        Parameters:
623        - account (str): The account number for which to check the existence of the box.
624        - ref (int): The reference (transaction) number to check for existence.
625
626        Returns:
627        - bool: True if the box exists for the given account and reference, False otherwise.
628        """
629        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:
631    def track(self, value: float = 0, desc: str = '', account: str = 1, logging: bool = True, created: int = None,
632              debug: bool = False) -> int:
633        """
634        This function tracks a transaction for a specific account.
635
636        Parameters:
637        value (float): The value of the transaction. Default is 0.
638        desc (str): The description of the transaction. Default is an empty string.
639        account (str): The account for which the transaction is being tracked. Default is '1'.
640        logging (bool): Whether to log the transaction. Default is True.
641        created (int): The timestamp of the transaction. If not provided, it will be generated. Default is None.
642        debug (bool): Whether to print debug information. Default is False.
643
644        Returns:
645        int: The timestamp of the transaction.
646
647        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.
648
649        Raises:
650        ValueError: The log transaction happened again in the same nanosecond time.
651        ValueError: The box transaction happened again in the same nanosecond time.
652        """
653        if created is None:
654            created = self.time()
655        no_lock = self.nolock()
656        self.lock()
657        if not self.account_exists(account):
658            if debug:
659                print(f"account {account} created")
660            self._vault['account'][account] = {
661                'balance': 0,
662                'box': {},
663                'count': 0,
664                'log': {},
665                'hide': False,
666                'zakatable': True,
667            }
668            self._step(Action.CREATE, account)
669        if value == 0:
670            if no_lock:
671                self.free(self.lock())
672            return 0
673        if logging:
674            self._log(value, desc, account, created, debug)
675        if debug:
676            print('create-box', created)
677        if self.box_exists(account, created):
678            raise ValueError(f"The box transaction happened again in the same nanosecond time({created}).")
679        if debug:
680            print('created-box', created)
681        self._vault['account'][account]['box'][created] = {
682            'capital': value,
683            'count': 0,
684            'last': 0,
685            'rest': value,
686            'total': 0,
687        }
688        self._step(Action.TRACK, account, ref=created, value=value)
689        if no_lock:
690            self.free(self.lock())
691        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:
693    def log_exists(self, account: str, ref: int) -> bool:
694        """
695        Checks if a specific transaction log entry exists for a given account.
696
697        Parameters:
698        account (str): The account number associated with the transaction log.
699        ref (int): The reference to the transaction log entry.
700
701        Returns:
702        bool: True if the transaction log entry exists, False otherwise.
703        """
704        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:
743    def exchange(self, account, created: int = None, rate: float = None, description: str = None,
744                 debug: bool = False) -> dict:
745        """
746        This method is used to record or retrieve exchange rates for a specific account.
747
748        Parameters:
749        - account (str): The account number for which the exchange rate is being recorded or retrieved.
750        - created (int): The timestamp of the exchange rate. If not provided, the current timestamp will be used.
751        - rate (float): The exchange rate to be recorded. If not provided, the method will retrieve the latest exchange rate.
752        - description (str): A description of the exchange rate.
753
754        Returns:
755        - dict: A dictionary containing the latest exchange rate and its description. If no exchange rate is found,
756        it returns a dictionary with default values for the rate and description.
757        """
758        if created is None:
759            created = self.time()
760        no_lock = self.nolock()
761        self.lock()
762        if rate is not None:
763            if rate <= 0:
764                return dict()
765            if account not in self._vault['exchange']:
766                self._vault['exchange'][account] = {}
767            if len(self._vault['exchange'][account]) == 0 and rate <= 1:
768                return {"rate": 1, "description": None}
769            self._vault['exchange'][account][created] = {"rate": rate, "description": description}
770            self._step(Action.EXCHANGE, account, ref=created, value=rate)
771            if no_lock:
772                self.free(self.lock())
773            if debug:
774                print("exchange-created-1",
775                      f'account: {account}, created: {created}, rate:{rate}, description:{description}')
776
777        if account in self._vault['exchange']:
778            valid_rates = [(ts, r) for ts, r in self._vault['exchange'][account].items() if ts <= created]
779            if valid_rates:
780                latest_rate = max(valid_rates, key=lambda x: x[0])
781                if debug:
782                    print("exchange-read-1",
783                          f'account: {account}, created: {created}, rate:{rate}, description:{description}',
784                          'latest_rate', latest_rate)
785                return latest_rate[1]  # إرجاع قاموس يحتوي على المعدل والوصف
786        if debug:
787            print("exchange-read-0", f'account: {account}, created: {created}, rate:{rate}, description:{description}')
788        return {"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:
790    @staticmethod
791    def exchange_calc(x: float, x_rate: float, y_rate: float) -> float:
792        """
793        This function calculates the exchanged amount of a currency.
794
795        Args:
796            x (float): The original amount of the currency.
797            x_rate (float): The exchange rate of the original currency.
798            y_rate (float): The exchange rate of the target currency.
799
800        Returns:
801            float: The exchanged amount of the target currency.
802        """
803        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:
805    def exchanges(self) -> dict:
806        """
807        Retrieve the recorded exchange rates for all accounts.
808
809        Parameters:
810        None
811
812        Returns:
813        dict: A dictionary containing all recorded exchange rates.
814        The keys are account names or numbers, and the values are dictionaries containing the exchange rates.
815        Each exchange rate dictionary has timestamps as keys and exchange rate details as values.
816        """
817        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:
819    def accounts(self) -> dict:
820        """
821        Returns a dictionary containing account numbers as keys and their respective balances as values.
822
823        Parameters:
824        None
825
826        Returns:
827        dict: A dictionary where keys are account numbers and values are their respective balances.
828        """
829        result = {}
830        for i in self._vault['account']:
831            result[i] = self._vault['account'][i]['balance']
832        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:
834    def boxes(self, account) -> dict:
835        """
836        Retrieve the boxes (transactions) associated with a specific account.
837
838        Parameters:
839        account (str): The account number for which to retrieve the boxes.
840
841        Returns:
842        dict: A dictionary containing the boxes associated with the given account.
843        If the account does not exist, an empty dictionary is returned.
844        """
845        if self.account_exists(account):
846            return self._vault['account'][account]['box']
847        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:
849    def logs(self, account) -> dict:
850        """
851        Retrieve the logs (transactions) associated with a specific account.
852
853        Parameters:
854        account (str): The account number for which to retrieve the logs.
855
856        Returns:
857        dict: A dictionary containing the logs associated with the given account.
858        If the account does not exist, an empty dictionary is returned.
859        """
860        if self.account_exists(account):
861            return self._vault['account'][account]['log']
862        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:
864    def add_file(self, account: str, ref: int, path: str) -> int:
865        """
866        Adds a file reference to a specific transaction log entry in the vault.
867
868        Parameters:
869        account (str): The account number associated with the transaction log.
870        ref (int): The reference to the transaction log entry.
871        path (str): The path of the file to be added.
872
873        Returns:
874        int: The reference of the added file. If the account or transaction log entry does not exist, returns 0.
875        """
876        if self.account_exists(account):
877            if ref in self._vault['account'][account]['log']:
878                file_ref = self.time()
879                self._vault['account'][account]['log'][ref]['file'][file_ref] = path
880                no_lock = self.nolock()
881                self.lock()
882                self._step(Action.ADD_FILE, account, ref=ref, file=file_ref)
883                if no_lock:
884                    self.free(self.lock())
885                return file_ref
886        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:
888    def remove_file(self, account: str, ref: int, file_ref: int) -> bool:
889        """
890        Removes a file reference from a specific transaction log entry in the vault.
891
892        Parameters:
893        account (str): The account number associated with the transaction log.
894        ref (int): The reference to the transaction log entry.
895        file_ref (int): The reference of the file to be removed.
896
897        Returns:
898        bool: True if the file reference is successfully removed, False otherwise.
899        """
900        if self.account_exists(account):
901            if ref in self._vault['account'][account]['log']:
902                if file_ref in self._vault['account'][account]['log'][ref]['file']:
903                    x = self._vault['account'][account]['log'][ref]['file'][file_ref]
904                    del self._vault['account'][account]['log'][ref]['file'][file_ref]
905                    no_lock = self.nolock()
906                    self.lock()
907                    self._step(Action.REMOVE_FILE, account, ref=ref, file=file_ref, value=x)
908                    if no_lock:
909                        self.free(self.lock())
910                    return True
911        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:
913    def balance(self, account: str = 1, cached: bool = True) -> int:
914        """
915        Calculate and return the balance of a specific account.
916
917        Parameters:
918        account (str): The account number. Default is '1'.
919        cached (bool): If True, use the cached balance. If False, calculate the balance from the box. Default is True.
920
921        Returns:
922        int: The balance of the account.
923
924        Note:
925        If cached is True, the function returns the cached balance.
926        If cached is False, the function calculates the balance from the box by summing up the 'rest' values of all box items.
927        """
928        if cached:
929            return self._vault['account'][account]['balance']
930        x = 0
931        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:
933    def hide(self, account, status: bool = None) -> bool:
934        """
935        Check or set the hide status of a specific account.
936
937        Parameters:
938        account (str): The account number.
939        status (bool, optional): The new hide status. If not provided, the function will return the current status.
940
941        Returns:
942        bool: The current or updated hide status of the account.
943
944        Raises:
945        None
946
947        Example:
948        >>> tracker = ZakatTracker()
949        >>> ref = tracker.track(51, 'desc', 'account1')
950        >>> tracker.hide('account1')  # Set the hide status of 'account1' to True
951        False
952        >>> tracker.hide('account1', True)  # Set the hide status of 'account1' to True
953        True
954        >>> tracker.hide('account1')  # Get the hide status of 'account1' by default
955        True
956        >>> tracker.hide('account1', False)
957        False
958        """
959        if self.account_exists(account):
960            if status is None:
961                return self._vault['account'][account]['hide']
962            self._vault['account'][account]['hide'] = status
963            return status
964        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:
966    def zakatable(self, account, status: bool = None) -> bool:
967        """
968        Check or set the zakatable status of a specific account.
969
970        Parameters:
971        account (str): The account number.
972        status (bool, optional): The new zakatable status. If not provided, the function will return the current status.
973
974        Returns:
975        bool: The current or updated zakatable status of the account.
976
977        Raises:
978        None
979
980        Example:
981        >>> tracker = ZakatTracker()
982        >>> ref = tracker.track(51, 'desc', 'account1')
983        >>> tracker.zakatable('account1')  # Set the zakatable status of 'account1' to True
984        True
985        >>> tracker.zakatable('account1', True)  # Set the zakatable status of 'account1' to True
986        True
987        >>> tracker.zakatable('account1')  # Get the zakatable status of 'account1' by default
988        True
989        >>> tracker.zakatable('account1', False)
990        False
991        """
992        if self.account_exists(account):
993            if status is None:
994                return self._vault['account'][account]['zakatable']
995            self._vault['account'][account]['zakatable'] = status
996            return status
997        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:
 999    def sub(self, x: float, desc: str = '', account: str = 1, created: int = None, debug: bool = False) -> tuple:
1000        """
1001        Subtracts a specified value from an account's balance.
1002
1003        Parameters:
1004        x (float): The amount to be subtracted.
1005        desc (str): A description for the transaction. Defaults to an empty string.
1006        account (str): The account from which the value will be subtracted. Defaults to '1'.
1007        created (int): The timestamp of the transaction. If not provided, the current timestamp will be used.
1008        debug (bool): A flag indicating whether to print debug information. Defaults to False.
1009
1010        Returns:
1011        tuple: A tuple containing the timestamp of the transaction and a list of tuples representing the age of each transaction.
1012
1013        If the amount to subtract is greater than the account's balance,
1014        the remaining amount will be transferred to a new transaction with a negative value.
1015
1016        Raises:
1017        ValueError: The box transaction happened again in the same nanosecond time.
1018        ValueError: The log transaction happened again in the same nanosecond time.
1019        """
1020        if x < 0:
1021            return tuple()
1022        if x == 0:
1023            ref = self.track(x, '', account)
1024            return ref, ref
1025        if created is None:
1026            created = self.time()
1027        no_lock = self.nolock()
1028        self.lock()
1029        self.track(0, '', account)
1030        self._log(-x, desc, account, created)
1031        ids = sorted(self._vault['account'][account]['box'].keys())
1032        limit = len(ids) + 1
1033        target = x
1034        if debug:
1035            print('ids', ids)
1036        ages = []
1037        for i in range(-1, -limit, -1):
1038            if target == 0:
1039                break
1040            j = ids[i]
1041            if debug:
1042                print('i', i, 'j', j)
1043            rest = self._vault['account'][account]['box'][j]['rest']
1044            if rest >= target:
1045                self._vault['account'][account]['box'][j]['rest'] -= target
1046                self._step(Action.SUB, account, ref=j, value=target)
1047                ages.append((j, target))
1048                target = 0
1049                break
1050            elif target > rest > 0:
1051                chunk = rest
1052                target -= chunk
1053                self._step(Action.SUB, account, ref=j, value=chunk)
1054                ages.append((j, chunk))
1055                self._vault['account'][account]['box'][j]['rest'] = 0
1056        if target > 0:
1057            self.track(-target, desc, account, False, created)
1058            ages.append((created, target))
1059        if no_lock:
1060            self.free(self.lock())
1061        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]:
1063    def transfer(self, amount: int, from_account: str, to_account: str, desc: str = '', created: int = None,
1064                 debug: bool = False) -> list[int]:
1065        """
1066        Transfers a specified value from one account to another.
1067
1068        Parameters:
1069        amount (int): The amount to be transferred.
1070        from_account (str): The account from which the value will be transferred.
1071        to_account (str): The account to which the value will be transferred.
1072        desc (str, optional): A description for the transaction. Defaults to an empty string.
1073        created (int, optional): The timestamp of the transaction. If not provided, the current timestamp will be used.
1074        debug (bool): A flag indicating whether to print debug information. Defaults to False.
1075
1076        Returns:
1077        list[int]: A list of timestamps corresponding to the transactions made during the transfer.
1078
1079        Raises:
1080        ValueError: Transfer to the same account is forbidden.
1081        ValueError: The box transaction happened again in the same nanosecond time.
1082        ValueError: The log transaction happened again in the same nanosecond time.
1083        """
1084        if from_account == to_account:
1085            raise ValueError(f'Transfer to the same account is forbidden. {to_account}')
1086        if amount <= 0:
1087            return []
1088        if created is None:
1089            created = self.time()
1090        (_, ages) = self.sub(amount, desc, from_account, created, debug=debug)
1091        times = []
1092        source_exchange = self.exchange(from_account, created)
1093        target_exchange = self.exchange(to_account, created)
1094
1095        if debug:
1096            print('ages', ages)
1097
1098        for age, value in ages:
1099            target_amount = self.exchange_calc(value, source_exchange['rate'], target_exchange['rate'])
1100            # Perform the transfer
1101            if self.box_exists(to_account, age):
1102                if debug:
1103                    print('box_exists', age)
1104                capital = self._vault['account'][to_account]['box'][age]['capital']
1105                rest = self._vault['account'][to_account]['box'][age]['rest']
1106                if debug:
1107                    print(
1108                        f"Transfer {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).")
1109                selected_age = age
1110                if rest + target_amount > capital:
1111                    self._vault['account'][to_account]['box'][age]['capital'] += target_amount
1112                    selected_age = ZakatTracker.time()
1113                self._vault['account'][to_account]['box'][age]['rest'] += target_amount
1114                self._step(Action.BOX_TRANSFER, to_account, ref=selected_age, value=target_amount)
1115                y = self._log(target_amount, desc=f'TRANSFER {from_account} -> {to_account}', account=to_account,
1116                              debug=debug)
1117                times.append((age, y))
1118                continue
1119            y = self.track(target_amount, desc, to_account, logging=True, created=age, debug=debug)
1120            if debug:
1121                print(
1122                    f"Transferred {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).")
1123            times.append(y)
1124        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:
1126    def check(self, silver_gram_price: float, nisab: float = None, debug: bool = False, now: int = None,
1127              cycle: float = None) -> tuple:
1128        """
1129        Check the eligibility for Zakat based on the given parameters.
1130
1131        Parameters:
1132        silver_gram_price (float): The price of a gram of silver.
1133        nisab (float): The minimum amount of wealth required for Zakat. If not provided,
1134                        it will be calculated based on the silver_gram_price.
1135        debug (bool): Flag to enable debug mode.
1136        now (int): The current timestamp. If not provided, it will be calculated using ZakatTracker.time().
1137        cycle (float): The time cycle for Zakat. If not provided, it will be calculated using ZakatTracker.TimeCycle().
1138
1139        Returns:
1140        tuple: A tuple containing a boolean indicating the eligibility for Zakat, a list of brief statistics,
1141        and a dictionary containing the Zakat plan.
1142        """
1143        if now is None:
1144            now = self.time()
1145        if cycle is None:
1146            cycle = ZakatTracker.TimeCycle()
1147        if nisab is None:
1148            nisab = ZakatTracker.Nisab(silver_gram_price)
1149        plan = {}
1150        below_nisab = 0
1151        brief = [0, 0, 0]
1152        valid = False
1153        for x in self._vault['account']:
1154            if not self.zakatable(x):
1155                continue
1156            _box = self._vault['account'][x]['box']
1157            _log = self._vault['account'][x]['log']
1158            limit = len(_box) + 1
1159            ids = sorted(self._vault['account'][x]['box'].keys())
1160            for i in range(-1, -limit, -1):
1161                j = ids[i]
1162                rest = _box[j]['rest']
1163                if rest <= 0:
1164                    continue
1165                exchange = self.exchange(x, created=j)
1166                if debug:
1167                    print('exchanges', self.exchanges())
1168                rest = ZakatTracker.exchange_calc(rest, exchange['rate'], 1)
1169                brief[0] += rest
1170                index = limit + i - 1
1171                epoch = (now - j) / cycle
1172                if debug:
1173                    print(f"Epoch: {epoch}", _box[j])
1174                if _box[j]['last'] > 0:
1175                    epoch = (now - _box[j]['last']) / cycle
1176                if debug:
1177                    print(f"Epoch: {epoch}")
1178                epoch = floor(epoch)
1179                if debug:
1180                    print(f"Epoch: {epoch}", type(epoch), epoch == 0, 1 - epoch, epoch)
1181                if epoch == 0:
1182                    continue
1183                if debug:
1184                    print("Epoch - PASSED")
1185                brief[1] += rest
1186                if rest >= nisab:
1187                    total = 0
1188                    for _ in range(epoch):
1189                        total += ZakatTracker.ZakatCut(float(rest) - float(total))
1190                    if total > 0:
1191                        if x not in plan:
1192                            plan[x] = {}
1193                        valid = True
1194                        brief[2] += total
1195                        plan[x][index] = {
1196                            'total': total,
1197                            'count': epoch,
1198                            'box_time': j,
1199                            'box_capital': _box[j]['capital'],
1200                            'box_rest': _box[j]['rest'],
1201                            'box_last': _box[j]['last'],
1202                            'box_total': _box[j]['total'],
1203                            'box_count': _box[j]['count'],
1204                            'box_log': _log[j]['desc'],
1205                        }
1206                else:
1207                    chunk = ZakatTracker.ZakatCut(float(rest))
1208                    if chunk > 0:
1209                        if x not in plan:
1210                            plan[x] = {}
1211                        if j not in plan[x].keys():
1212                            plan[x][index] = {}
1213                        below_nisab += rest
1214                        brief[2] += chunk
1215                        plan[x][index]['below_nisab'] = chunk
1216                        plan[x][index]['box_time'] = j
1217                        plan[x][index]['box_capital'] = _box[j]['capital']
1218                        plan[x][index]['box_rest'] = _box[j]['rest']
1219                        plan[x][index]['box_last'] = _box[j]['last']
1220                        plan[x][index]['box_total'] = _box[j]['total']
1221                        plan[x][index]['box_count'] = _box[j]['count']
1222                        plan[x][index]['box_log'] = _log[j]['desc']
1223        valid = valid or below_nisab >= nisab
1224        if debug:
1225            print(f"below_nisab({below_nisab}) >= nisab({nisab})")
1226        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:
1228    def build_payment_parts(self, demand: float, positive_only: bool = True) -> dict:
1229        """
1230        Build payment parts for the Zakat distribution.
1231
1232        Parameters:
1233        demand (float): The total demand for payment in local currency.
1234        positive_only (bool): If True, only consider accounts with positive balance. Default is True.
1235
1236        Returns:
1237        dict: A dictionary containing the payment parts for each account. The dictionary has the following structure:
1238        {
1239            'account': {
1240                'account_id': {'balance': float, 'rate': float, 'part': float},
1241                ...
1242            },
1243            'exceed': bool,
1244            'demand': float,
1245            'total': float,
1246        }
1247        """
1248        total = 0
1249        parts = {
1250            'account': {},
1251            'exceed': False,
1252            'demand': demand,
1253        }
1254        for x, y in self.accounts().items():
1255            if positive_only and y <= 0:
1256                continue
1257            total += y
1258            exchange = self.exchange(x)
1259            parts['account'][x] = {'balance': y, 'rate': exchange['rate'], 'part': 0}
1260        parts['total'] = total
1261        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) -> int:
1263    @staticmethod
1264    def check_payment_parts(parts: dict) -> int:
1265        """
1266        Checks the validity of payment parts.
1267
1268        Parameters:
1269        parts (dict): A dictionary containing payment parts information.
1270
1271        Returns:
1272        int: Returns 0 if the payment parts are valid, otherwise returns the error code.
1273
1274        Error Codes:
1275        1: 'demand', 'account', 'total', or 'exceed' key is missing in parts.
1276        2: 'balance', 'rate' or 'part' key is missing in parts['account'][x].
1277        3: 'part' value in parts['account'][x] is less than or equal to 0.
1278        4: If 'exceed' is False, 'balance' value in parts['account'][x] is less than or equal to 0.
1279        5: 'part' value in parts['account'][x] is less than 0.
1280        6: If 'exceed' is False, 'part' value in parts['account'][x] is greater than 'balance' value.
1281        7: The sum of 'part' values in parts['account'] does not match with 'demand' value.
1282        """
1283        for i in ['demand', 'account', 'total', 'exceed']:
1284            if i not in parts:
1285                return 1
1286        exceed = parts['exceed']
1287        for x in parts['account']:
1288            for j in ['balance', 'rate', 'part']:
1289                if j not in parts['account'][x]:
1290                    return 2
1291                if parts['account'][x]['part'] <= 0:
1292                    return 3
1293                if not exceed and parts['account'][x]['balance'] <= 0:
1294                    return 4
1295        demand = parts['demand']
1296        z = 0
1297        for _, y in parts['account'].items():
1298            if y['part'] < 0:
1299                return 5
1300            if not exceed and y['part'] > y['balance']:
1301                return 6
1302            z += ZakatTracker.exchange_calc(y['part'], y['rate'], 1)
1303        if z != demand:
1304            return 7
1305        return 0

Checks the validity of payment parts.

Parameters: parts (dict): A dictionary containing payment parts information.

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 or equal to 0. 4: If 'exceed' is False, 'balance' value in parts['account'][x] is less than or equal to 0. 5: 'part' value in parts['account'][x] is less than 0. 6: If 'exceed' is False, 'part' value in parts['account'][x] is greater than 'balance' value. 7: 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:
1307    def zakat(self, report: tuple, parts: List[Dict[str, Dict | bool | Any]] = None, debug: bool = False) -> bool:
1308        """
1309        Perform Zakat calculation based on the given report and optional parts.
1310
1311        Parameters:
1312        report (tuple): A tuple containing the validity of the report, the report data, and the zakat plan.
1313        parts (dict): A dictionary containing the payment parts for the zakat.
1314        debug (bool): A flag indicating whether to print debug information.
1315
1316        Returns:
1317        bool: True if the zakat calculation is successful, False otherwise.
1318        """
1319        valid, _, plan = report
1320        if not valid:
1321            return valid
1322        parts_exist = parts is not None
1323        if parts_exist:
1324            for part in parts:
1325                if self.check_payment_parts(part) != 0:
1326                    return False
1327        if debug:
1328            print('######### zakat #######')
1329            print('parts_exist', parts_exist)
1330        no_lock = self.nolock()
1331        self.lock()
1332        report_time = self.time()
1333        self._vault['report'][report_time] = report
1334        self._step(Action.REPORT, ref=report_time)
1335        created = self.time()
1336        for x in plan:
1337            if debug:
1338                print(plan[x])
1339                print('-------------')
1340                print(self._vault['account'][x]['box'])
1341            ids = sorted(self._vault['account'][x]['box'].keys())
1342            if debug:
1343                print('plan[x]', plan[x])
1344            for i in plan[x].keys():
1345                j = ids[i]
1346                if debug:
1347                    print('i', i, 'j', j)
1348                self._step(Action.ZAKAT, account=x, ref=j, value=self._vault['account'][x]['box'][j]['last'],
1349                           key='last',
1350                           math_operation=MathOperation.EQUAL)
1351                self._vault['account'][x]['box'][j]['last'] = created
1352                self._vault['account'][x]['box'][j]['total'] += plan[x][i]['total']
1353                self._step(Action.ZAKAT, account=x, ref=j, value=plan[x][i]['total'], key='total',
1354                           math_operation=MathOperation.ADDITION)
1355                self._vault['account'][x]['box'][j]['count'] += plan[x][i]['count']
1356                self._step(Action.ZAKAT, account=x, ref=j, value=plan[x][i]['count'], key='count',
1357                           math_operation=MathOperation.ADDITION)
1358                if not parts_exist:
1359                    self._vault['account'][x]['box'][j]['rest'] -= plan[x][i]['total']
1360                    self._step(Action.ZAKAT, account=x, ref=j, value=plan[x][i]['total'], key='rest',
1361                               math_operation=MathOperation.SUBTRACTION)
1362        if parts_exist:
1363            for transaction in parts:
1364                for account, part in transaction['account'].items():
1365                    if debug:
1366                        print('zakat-part', account, part['part'])
1367                    target_exchange = self.exchange(account)
1368                    amount = ZakatTracker.exchange_calc(part['part'], part['rate'], target_exchange['rate'])
1369                    self.sub(amount, desc='zakat-part', account=account, debug=debug)
1370        if no_lock:
1371            self.free(self.lock())
1372        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:
1374    def export_json(self, path: str = "data.json") -> bool:
1375        """
1376        Exports the current state of the ZakatTracker object to a JSON file.
1377
1378        Parameters:
1379        path (str): The path where the JSON file will be saved. Default is "data.json".
1380
1381        Returns:
1382        bool: True if the export is successful, False otherwise.
1383
1384        Raises:
1385        No specific exceptions are raised by this method.
1386        """
1387        with open(path, "w") as file:
1388            json.dump(self._vault, file, indent=4, cls=JSONEncoder)
1389            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:
1391    def save(self, path: str = None) -> bool:
1392        """
1393        Saves the ZakatTracker's current state to a pickle file.
1394
1395        This method serializes the internal data (`_vault`) along with metadata
1396        (Python version, pickle protocol) for future compatibility.
1397
1398        Parameters:
1399            path (str, optional): File path for saving. Defaults to a predefined location.
1400
1401        Returns:
1402            bool: True if the save operation is successful, False otherwise.
1403        """
1404        if path is None:
1405            path = self.path()
1406        with open(path, "wb") as f:
1407            version = f'{version_info.major}.{version_info.minor}.{version_info.micro}'
1408            pickle_protocol = pickle.HIGHEST_PROTOCOL
1409            data = {
1410                'python_version': version,
1411                'pickle_protocol': pickle_protocol,
1412                'data': self._vault,
1413            }
1414            pickle.dump(data, f, protocol=pickle_protocol)
1415            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:
1417    def load(self, path: str = None) -> bool:
1418        """
1419        Load the current state of the ZakatTracker object from a pickle file.
1420
1421        Parameters:
1422        path (str): The path where the pickle file is located. If not provided, it will use the default path.
1423
1424        Returns:
1425        bool: True if the load operation is successful, False otherwise.
1426        """
1427        if path is None:
1428            path = self.path()
1429        if os.path.exists(path):
1430            with open(path, "rb") as f:
1431                data = pickle.load(f)
1432                self._vault = data['data']
1433                return True
1434        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):
1436    def import_csv_cache_path(self):
1437        """
1438        Generates the cache file path for imported CSV data.
1439
1440        This function constructs the file path where cached data from CSV imports
1441        will be stored. The cache file is a pickle file (.pickle extension) appended
1442        to the base path of the object.
1443
1444        Returns:
1445            str: The full path to the import CSV cache file.
1446
1447        Example:
1448            >>> obj = ZakatTracker('/data/reports')
1449            >>> obj.import_csv_cache_path()
1450            '/data/reports.import_csv.pickle'
1451        """
1452        path = self.path()
1453        if path.endswith(".pickle"):
1454            path = path[:-7]
1455        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:
1457    def import_csv(self, path: str = 'file.csv', debug: bool = False) -> tuple:
1458        """
1459        The function reads the CSV file, checks for duplicate transactions, and creates the transactions in the system.
1460
1461        Parameters:
1462        path (str): The path to the CSV file. Default is 'file.csv'.
1463        debug (bool): A flag indicating whether to print debug information.
1464
1465        Returns:
1466        tuple: A tuple containing the number of transactions created, the number of transactions found in the cache,
1467                and a dictionary of bad transactions.
1468
1469        Notes:
1470            * Currency Pair Assumption: This function assumes that the exchange rates stored for each account
1471                                        are appropriate for the currency pairs involved in the conversions.
1472            * The exchange rate for each account is based on the last encountered transaction rate that is not equal
1473                to 1.0 or the previous rate for that account.
1474            * Those rates will be merged into the exchange rates main data, and later it will be used for all subsequent
1475              transactions of the same account within the whole imported and existing dataset when doing `check` and
1476              `zakat` operations.
1477
1478        Example Usage:
1479            The CSV file should have the following format, rate is optional per transaction:
1480            account, desc, value, date, rate
1481            For example:
1482            safe-45, "Some text", 34872, 1988-06-30 00:00:00, 1
1483        """
1484        cache: list[int] = []
1485        try:
1486            with open(self.import_csv_cache_path(), "rb") as f:
1487                cache = pickle.load(f)
1488        except:
1489            pass
1490        date_formats = [
1491            "%Y-%m-%d %H:%M:%S",
1492            "%Y-%m-%dT%H:%M:%S",
1493            "%Y-%m-%dT%H%M%S",
1494            "%Y-%m-%d",
1495        ]
1496        created, found, bad = 0, 0, {}
1497        data: list[tuple] = []
1498        with open(path, newline='', encoding="utf-8") as f:
1499            i = 0
1500            for row in csv.reader(f, delimiter=','):
1501                i += 1
1502                hashed = hash(tuple(row))
1503                if hashed in cache:
1504                    found += 1
1505                    continue
1506                account = row[0]
1507                desc = row[1]
1508                value = float(row[2])
1509                rate = 1.0
1510                if row[4:5]:  # Empty list if index is out of range
1511                    rate = float(row[4])
1512                date: int = 0
1513                for time_format in date_formats:
1514                    try:
1515                        date = self.time(datetime.datetime.strptime(row[3], time_format))
1516                        break
1517                    except:
1518                        pass
1519                # TODO: not allowed for negative dates
1520                if date == 0 or value == 0:
1521                    bad[i] = row
1522                    continue
1523                if date in data:
1524                    print('import_csv-duplicated(time)', date)
1525                    continue
1526                data.append((date, value, desc, account, rate, hashed))
1527
1528        if debug:
1529            print('import_csv', len(data))
1530        for row in sorted(data, key=lambda x: x[0]):
1531            (date, value, desc, account, rate, hashed) = row
1532            if rate > 1:
1533                self.exchange(account, created=date, rate=rate)
1534            if value > 0:
1535                self.track(value, desc, account, True, date)
1536            elif value < 0:
1537                self.sub(-value, desc, account, date)
1538            created += 1
1539            cache.append(hashed)
1540        with open(self.import_csv_cache_path(), "wb") as f:
1541            pickle.dump(cache, f)
1542        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:
1548    @staticmethod
1549    def duration_from_nanoseconds(ns: int) -> tuple:
1550        """
1551        REF https://github.com/JayRizzo/Random_Scripts/blob/master/time_measure.py#L106
1552        Convert NanoSeconds to Human Readable Time Format.
1553        A NanoSeconds is a unit of time in the International System of Units (SI) equal
1554        to one millionth (0.000001 or 10−6 or 1⁄1,000,000) of a second.
1555        Its symbol is μs, sometimes simplified to us when Unicode is not available.
1556        A microsecond is equal to 1000 nanoseconds or 1⁄1,000 of a millisecond.
1557
1558        INPUT : ms (AKA: MilliSeconds)
1559        OUTPUT: tuple(string time_lapsed, string spoken_time) like format.
1560        OUTPUT Variables: time_lapsed, spoken_time
1561
1562        Example  Input: duration_from_nanoseconds(ns)
1563        **"Millennium:Century:Years:Days:Hours:Minutes:Seconds:MilliSeconds:MicroSeconds:NanoSeconds"**
1564        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')
1565        duration_from_nanoseconds(1234567890123456789012)
1566        """
1567        us, ns = divmod(ns, 1000)
1568        ms, us = divmod(us, 1000)
1569        s, ms = divmod(ms, 1000)
1570        m, s = divmod(s, 60)
1571        h, m = divmod(m, 60)
1572        d, h = divmod(h, 24)
1573        y, d = divmod(d, 365)
1574        c, y = divmod(y, 100)
1575        n, c = divmod(c, 10)
1576        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}"
1577        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"
1578        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:
1580    @staticmethod
1581    def day_to_time(day: int, month: int = 6, year: int = 2024) -> int:  # افتراض أن الشهر هو يونيو والسنة 2024
1582        """
1583        Convert a specific day, month, and year into a timestamp.
1584
1585        Parameters:
1586        day (int): The day of the month.
1587        month (int): The month of the year. Default is 6 (June).
1588        year (int): The year. Default is 2024.
1589
1590        Returns:
1591        int: The timestamp representing the given day, month, and year.
1592
1593        Note:
1594        This method assumes the default month and year if not provided.
1595        """
1596        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:
1598    @staticmethod
1599    def generate_random_date(start_date: datetime.datetime, end_date: datetime.datetime) -> datetime.datetime:
1600        """
1601        Generate a random date between two given dates.
1602
1603        Parameters:
1604        start_date (datetime.datetime): The start date from which to generate a random date.
1605        end_date (datetime.datetime): The end date until which to generate a random date.
1606
1607        Returns:
1608        datetime.datetime: A random date between the start_date and end_date.
1609        """
1610        time_between_dates = end_date - start_date
1611        days_between_dates = time_between_dates.days
1612        random_number_of_days = random.randrange(days_between_dates)
1613        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:
1615    @staticmethod
1616    def generate_random_csv_file(path: str = "data.csv", count: int = 1000, with_rate: bool = False,
1617                                 debug: bool = False) -> int:
1618        """
1619        Generate a random CSV file with specified parameters.
1620
1621        Parameters:
1622        path (str): The path where the CSV file will be saved. Default is "data.csv".
1623        count (int): The number of rows to generate in the CSV file. Default is 1000.
1624        with_rate (bool): If True, a random rate between 1.2% and 12% is added. Default is False.
1625        debug (bool): A flag indicating whether to print debug information.
1626
1627        Returns:
1628        None. The function generates a CSV file at the specified path with the given count of rows.
1629        Each row contains a randomly generated account, description, value, and date.
1630        The value is randomly generated between 1000 and 100000,
1631        and the date is randomly generated between 1950-01-01 and 2023-12-31.
1632        If the row number is not divisible by 13, the value is multiplied by -1.
1633        """
1634        i = 0
1635        with open(path, "w", newline="") as csvfile:
1636            writer = csv.writer(csvfile)
1637            for i in range(count):
1638                account = f"acc-{random.randint(1, 1000)}"
1639                desc = f"Some text {random.randint(1, 1000)}"
1640                value = random.randint(1000, 100000)
1641                date = ZakatTracker.generate_random_date(datetime.datetime(1950, 1, 1),
1642                                                         datetime.datetime(2023, 12, 31)).strftime("%Y-%m-%d %H:%M:%S")
1643                if not i % 13 == 0:
1644                    value *= -1
1645                row = [account, desc, value, date]
1646                if with_rate:
1647                    rate = random.randint(1, 100) * 0.12
1648                    if debug:
1649                        print('before-append', row)
1650                    row.append(rate)
1651                    if debug:
1652                        print('after-append', row)
1653                writer.writerow(row)
1654                i = i + 1
1655        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):
1657    @staticmethod
1658    def create_random_list(max_sum, min_value=0, max_value=10):
1659        """
1660        Creates a list of random integers whose sum does not exceed the specified maximum.
1661
1662        Args:
1663            max_sum: The maximum allowed sum of the list elements.
1664            min_value: The minimum possible value for an element (inclusive).
1665            max_value: The maximum possible value for an element (inclusive).
1666
1667        Returns:
1668            A list of random integers.
1669        """
1670        result = []
1671        current_sum = 0
1672
1673        while current_sum < max_sum:
1674            # Calculate the remaining space for the next element
1675            remaining_sum = max_sum - current_sum
1676            # Determine the maximum possible value for the next element
1677            next_max_value = min(remaining_sum, max_value)
1678            # Generate a random element within the allowed range
1679            next_element = random.randint(min_value, next_max_value)
1680            result.append(next_element)
1681            current_sum += next_element
1682
1683        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:
1828    def test(self, debug: bool = False) -> bool:
1829
1830        try:
1831
1832            assert self._history()
1833
1834            # Not allowed for duplicate transactions in the same account and time
1835
1836            created = ZakatTracker.time()
1837            self.track(100, 'test-1', 'same', True, created)
1838            failed = False
1839            try:
1840                self.track(50, 'test-1', 'same', True, created)
1841            except:
1842                failed = True
1843            assert failed is True
1844
1845            self.reset()
1846
1847            # Same account transfer
1848            for x in [1, 'a', True, 1.8, None]:
1849                failed = False
1850                try:
1851                    self.transfer(1, x, x, 'same-account', debug=debug)
1852                except:
1853                    failed = True
1854                assert failed is True
1855
1856            # Always preserve box age during transfer
1857
1858            series: list[tuple] = [
1859                (30, 4),
1860                (60, 3),
1861                (90, 2),
1862            ]
1863            case = {
1864                30: {
1865                    'series': series,
1866                    'rest': 150,
1867                },
1868                60: {
1869                    'series': series,
1870                    'rest': 120,
1871                },
1872                90: {
1873                    'series': series,
1874                    'rest': 90,
1875                },
1876                180: {
1877                    'series': series,
1878                    'rest': 0,
1879                },
1880                270: {
1881                    'series': series,
1882                    'rest': -90,
1883                },
1884                360: {
1885                    'series': series,
1886                    'rest': -180,
1887                },
1888            }
1889
1890            selected_time = ZakatTracker.time() - ZakatTracker.TimeCycle()
1891
1892            for total in case:
1893                for x in case[total]['series']:
1894                    self.track(x[0], f"test-{x} ages", 'ages', True, selected_time * x[1])
1895
1896                refs = self.transfer(total, 'ages', 'future', 'Zakat Movement', debug=debug)
1897
1898                if debug:
1899                    print('refs', refs)
1900
1901                ages_cache_balance = self.balance('ages')
1902                ages_fresh_balance = self.balance('ages', False)
1903                rest = case[total]['rest']
1904                if debug:
1905                    print('source', ages_cache_balance, ages_fresh_balance, rest)
1906                assert ages_cache_balance == rest
1907                assert ages_fresh_balance == rest
1908
1909                future_cache_balance = self.balance('future')
1910                future_fresh_balance = self.balance('future', False)
1911                if debug:
1912                    print('target', future_cache_balance, future_fresh_balance, total)
1913                    print('refs', refs)
1914                assert future_cache_balance == total
1915                assert future_fresh_balance == total
1916
1917                for ref in self._vault['account']['ages']['box']:
1918                    ages_capital = self._vault['account']['ages']['box'][ref]['capital']
1919                    ages_rest = self._vault['account']['ages']['box'][ref]['rest']
1920                    future_capital = 0
1921                    if ref in self._vault['account']['future']['box']:
1922                        future_capital = self._vault['account']['future']['box'][ref]['capital']
1923                    future_rest = 0
1924                    if ref in self._vault['account']['future']['box']:
1925                        future_rest = self._vault['account']['future']['box'][ref]['rest']
1926                    if ages_capital != 0 and future_capital != 0 and future_rest != 0:
1927                        if debug:
1928                            print('================================================================')
1929                            print('ages', ages_capital, ages_rest)
1930                            print('future', future_capital, future_rest)
1931                        if ages_rest == 0:
1932                            assert ages_capital == future_capital
1933                        elif ages_rest < 0:
1934                            assert -ages_capital == future_capital
1935                        elif ages_rest > 0:
1936                            assert ages_capital == ages_rest + future_capital
1937                self.reset()
1938                assert len(self._vault['history']) == 0
1939
1940            assert self._history()
1941            assert self._history(False) is False
1942            assert self._history() is False
1943            assert self._history(True)
1944            assert self._history()
1945
1946            self._test_core(True, debug)
1947            self._test_core(False, debug)
1948
1949            transaction = [
1950                (
1951                    20, 'wallet', 1, 800, 800, 800, 4, 5,
1952                    -85, -85, -85, 6, 7,
1953                ),
1954                (
1955                    750, 'wallet', 'safe', 50, 50, 50, 4, 6,
1956                    750, 750, 750, 1, 1,
1957                ),
1958                (
1959                    600, 'safe', 'bank', 150, 150, 150, 1, 2,
1960                    600, 600, 600, 1, 1,
1961                ),
1962            ]
1963            for z in transaction:
1964                self.lock()
1965                x = z[1]
1966                y = z[2]
1967                self.transfer(z[0], x, y, 'test-transfer', debug=debug)
1968                assert self.balance(x) == z[3]
1969                xx = self.accounts()[x]
1970                assert xx == z[3]
1971                assert self.balance(x, False) == z[4]
1972                assert xx == z[4]
1973
1974                s = 0
1975                log = self._vault['account'][x]['log']
1976                for i in log:
1977                    s += log[i]['value']
1978                if debug:
1979                    print('s', s, 'z[5]', z[5])
1980                assert s == z[5]
1981
1982                assert self.box_size(x) == z[6]
1983                assert self.log_size(x) == z[7]
1984
1985                yy = self.accounts()[y]
1986                assert self.balance(y) == z[8]
1987                assert yy == z[8]
1988                assert self.balance(y, False) == z[9]
1989                assert yy == z[9]
1990
1991                s = 0
1992                log = self._vault['account'][y]['log']
1993                for i in log:
1994                    s += log[i]['value']
1995                assert s == z[10]
1996
1997                assert self.box_size(y) == z[11]
1998                assert self.log_size(y) == z[12]
1999
2000            if debug:
2001                pp().pprint(self.check(2.17))
2002
2003            assert not self.nolock()
2004            history_count = len(self._vault['history'])
2005            if debug:
2006                print('history-count', history_count)
2007            assert history_count == 11
2008            assert not self.free(ZakatTracker.time())
2009            assert self.free(self.lock())
2010            assert self.nolock()
2011            assert len(self._vault['history']) == 11
2012
2013            # storage
2014
2015            _path = self.path('test.pickle')
2016            if os.path.exists(_path):
2017                os.remove(_path)
2018            self.save()
2019            assert os.path.getsize(_path) > 0
2020            self.reset()
2021            assert self.recall(False, debug) is False
2022            self.load()
2023            assert self._vault['account'] is not None
2024
2025            # recall
2026
2027            assert self.nolock()
2028            assert len(self._vault['history']) == 11
2029            assert self.recall(False, debug) is True
2030            assert len(self._vault['history']) == 10
2031            assert self.recall(False, debug) is True
2032            assert len(self._vault['history']) == 9
2033
2034            csv_count = 1000
2035
2036            for with_rate, path in {
2037                False: 'test-import_csv-no-exchange',
2038                True: 'test-import_csv-with-exchange',
2039            }.items():
2040
2041                if debug:
2042                    print('test_import_csv', with_rate, path)
2043
2044                # csv
2045
2046                csv_path = path + '.csv'
2047                if os.path.exists(csv_path):
2048                    os.remove(csv_path)
2049                c = self.generate_random_csv_file(csv_path, csv_count, with_rate, debug)
2050                if debug:
2051                    print('generate_random_csv_file', c)
2052                assert c == csv_count
2053                assert os.path.getsize(csv_path) > 0
2054                cache_path = self.import_csv_cache_path()
2055                if os.path.exists(cache_path):
2056                    os.remove(cache_path)
2057                self.reset()
2058                (created, found, bad) = self.import_csv(csv_path, debug)
2059                bad_count = len(bad)
2060                if debug:
2061                    print(f"csv-imported: ({created}, {found}, {bad_count}) = count({csv_count})")
2062                tmp_size = os.path.getsize(cache_path)
2063                assert tmp_size > 0
2064                assert created + found + bad_count == csv_count
2065                assert created == csv_count
2066                assert bad_count == 0
2067                (created_2, found_2, bad_2) = self.import_csv(csv_path)
2068                bad_2_count = len(bad_2)
2069                if debug:
2070                    print(f"csv-imported: ({created_2}, {found_2}, {bad_2_count})")
2071                    print(bad)
2072                assert tmp_size == os.path.getsize(cache_path)
2073                assert created_2 + found_2 + bad_2_count == csv_count
2074                assert created == found_2
2075                assert bad_count == bad_2_count
2076                assert found_2 == csv_count
2077                assert bad_2_count == 0
2078                assert created_2 == 0
2079
2080                # payment parts
2081
2082                positive_parts = self.build_payment_parts(100, positive_only=True)
2083                assert self.check_payment_parts(positive_parts) != 0
2084                assert self.check_payment_parts(positive_parts) != 0
2085                all_parts = self.build_payment_parts(300, positive_only=False)
2086                assert self.check_payment_parts(all_parts) != 0
2087                assert self.check_payment_parts(all_parts) != 0
2088                if debug:
2089                    pp().pprint(positive_parts)
2090                    pp().pprint(all_parts)
2091                # dynamic discount
2092                suite = []
2093                count = 3
2094                for exceed in [False, True]:
2095                    case = []
2096                    for parts in [positive_parts, all_parts]:
2097                        part = parts.copy()
2098                        demand = part['demand']
2099                        if debug:
2100                            print(demand, part['total'])
2101                        i = 0
2102                        z = demand / count
2103                        cp = {
2104                            'account': {},
2105                            'demand': demand,
2106                            'exceed': exceed,
2107                            'total': part['total'],
2108                        }
2109                        j = ''
2110                        for x, y in part['account'].items():
2111                            x_exchange = self.exchange(x)
2112                            zz = self.exchange_calc(z, 1, x_exchange['rate'])
2113                            if exceed and zz <= demand:
2114                                i += 1
2115                                y['part'] = zz
2116                                if debug:
2117                                    print(exceed, y)
2118                                cp['account'][x] = y
2119                                case.append(y)
2120                            elif not exceed and y['balance'] >= zz:
2121                                i += 1
2122                                y['part'] = zz
2123                                if debug:
2124                                    print(exceed, y)
2125                                cp['account'][x] = y
2126                                case.append(y)
2127                            j = x
2128                            if i >= count:
2129                                break
2130                        if len(cp['account'][j]) > 0:
2131                            suite.append(cp)
2132                if debug:
2133                    print('suite', len(suite))
2134                for case in suite:
2135                    if debug:
2136                        print(case)
2137                    result = self.check_payment_parts(case)
2138                    if debug:
2139                        print('check_payment_parts', result, f'exceed: {exceed}')
2140                    assert result == 0
2141
2142                report = self.check(2.17, None, debug)
2143                (valid, brief, plan) = report
2144                if debug:
2145                    print('valid', valid)
2146                assert self.zakat(report, parts=suite, debug=debug)
2147                assert self.save(path + '.pickle')
2148                assert self.export_json(path + '.json')
2149
2150            # exchange
2151
2152            self.exchange("cash", 25, 3.75, "2024-06-25")
2153            self.exchange("cash", 22, 3.73, "2024-06-22")
2154            self.exchange("cash", 15, 3.69, "2024-06-15")
2155            self.exchange("cash", 10, 3.66)
2156
2157            for i in range(1, 30):
2158                rate, description = self.exchange("cash", i).values()
2159                if debug:
2160                    print(i, rate, description)
2161                if i < 10:
2162                    assert rate == 1
2163                    assert description is None
2164                elif i == 10:
2165                    assert rate == 3.66
2166                    assert description is None
2167                elif i < 15:
2168                    assert rate == 3.66
2169                    assert description is None
2170                elif i == 15:
2171                    assert rate == 3.69
2172                    assert description is not None
2173                elif i < 22:
2174                    assert rate == 3.69
2175                    assert description is not None
2176                elif i == 22:
2177                    assert rate == 3.73
2178                    assert description is not None
2179                elif i >= 25:
2180                    assert rate == 3.75
2181                    assert description is not None
2182                rate, description = self.exchange("bank", i).values()
2183                if debug:
2184                    print(i, rate, description)
2185                assert rate == 1
2186                assert description is None
2187
2188            assert len(self._vault['exchange']) > 0
2189            assert len(self.exchanges()) > 0
2190            self._vault['exchange'].clear()
2191            assert len(self._vault['exchange']) == 0
2192            assert len(self.exchanges()) == 0
2193
2194            # حفظ أسعار الصرف باستخدام التواريخ بالنانو ثانية
2195            self.exchange("cash", ZakatTracker.day_to_time(25), 3.75, "2024-06-25")
2196            self.exchange("cash", ZakatTracker.day_to_time(22), 3.73, "2024-06-22")
2197            self.exchange("cash", ZakatTracker.day_to_time(15), 3.69, "2024-06-15")
2198            self.exchange("cash", ZakatTracker.day_to_time(10), 3.66)
2199
2200            for i in [x * 0.12 for x in range(-15, 21)]:
2201                if i <= 0:
2202                    assert len(self.exchange("test", ZakatTracker.time(), i, f"range({i})")) == 0
2203                else:
2204                    assert len(self.exchange("test", ZakatTracker.time(), i, f"range({i})")) > 0
2205
2206            # اختبار النتائج باستخدام التواريخ بالنانو ثانية
2207            for i in range(1, 31):
2208                timestamp_ns = ZakatTracker.day_to_time(i)
2209                rate, description = self.exchange("cash", timestamp_ns).values()
2210                if debug:
2211                    print(i, rate, description)
2212                if i < 10:
2213                    assert rate == 1
2214                    assert description is None
2215                elif i == 10:
2216                    assert rate == 3.66
2217                    assert description is None
2218                elif i < 15:
2219                    assert rate == 3.66
2220                    assert description is None
2221                elif i == 15:
2222                    assert rate == 3.69
2223                    assert description is not None
2224                elif i < 22:
2225                    assert rate == 3.69
2226                    assert description is not None
2227                elif i == 22:
2228                    assert rate == 3.73
2229                    assert description is not None
2230                elif i >= 25:
2231                    assert rate == 3.75
2232                    assert description is not None
2233                rate, description = self.exchange("bank", i).values()
2234                if debug:
2235                    print(i, rate, description)
2236                assert rate == 1
2237                assert description is None
2238
2239            assert self.export_json("1000-transactions-test.json")
2240            assert self.save("1000-transactions-test.pickle")
2241
2242            self.reset()
2243
2244            # test transfer between accounts with different exchange rate
2245
2246            a_SAR = "Bank (SAR)"
2247            b_USD = "Bank (USD)"
2248            c_SAR = "Safe (SAR)"
2249            # 0: track, 1: check-exchange, 2: do-exchange, 3: transfer
2250            for case in [
2251                (0, a_SAR, "SAR Gift", 1000, 1000),
2252                (1, a_SAR, 1),
2253                (0, b_USD, "USD Gift", 500, 500),
2254                (1, b_USD, 1),
2255                (2, b_USD, 3.75),
2256                (1, b_USD, 3.75),
2257                (3, 100, b_USD, a_SAR, "100 USD -> SAR", 400, 1375),
2258                (0, c_SAR, "Salary", 750, 750),
2259                (3, 375, c_SAR, b_USD, "375 SAR -> USD", 375, 500),
2260                (3, 3.75, a_SAR, b_USD, "3.75 SAR -> USD", 1371.25, 501),
2261            ]:
2262                match (case[0]):
2263                    case 0:  # track
2264                        _, account, desc, x, balance = case
2265                        self.track(value=x, desc=desc, account=account, debug=debug)
2266
2267                        cached_value = self.balance(account, cached=True)
2268                        fresh_value = self.balance(account, cached=False)
2269                        if debug:
2270                            print('account', account, 'cached_value', cached_value, 'fresh_value', fresh_value)
2271                        assert cached_value == balance
2272                        assert fresh_value == balance
2273                    case 1:  # check-exchange
2274                        _, account, expected_rate = case
2275                        t_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug)
2276                        if debug:
2277                            print('t-exchange', t_exchange)
2278                        assert t_exchange['rate'] == expected_rate
2279                    case 2:  # do-exchange
2280                        _, account, rate = case
2281                        self.exchange(account, rate=rate, debug=debug)
2282                        b_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug)
2283                        if debug:
2284                            print('b-exchange', b_exchange)
2285                        assert b_exchange['rate'] == rate
2286                    case 3:  # transfer
2287                        _, x, a, b, desc, a_balance, b_balance = case
2288                        self.transfer(x, a, b, desc, debug=debug)
2289
2290                        cached_value = self.balance(a, cached=True)
2291                        fresh_value = self.balance(a, cached=False)
2292                        if debug:
2293                            print('account', a, 'cached_value', cached_value, 'fresh_value', fresh_value)
2294                        assert cached_value == a_balance
2295                        assert fresh_value == a_balance
2296
2297                        cached_value = self.balance(b, cached=True)
2298                        fresh_value = self.balance(b, cached=False)
2299                        if debug:
2300                            print('account', b, 'cached_value', cached_value, 'fresh_value', fresh_value)
2301                        assert cached_value == b_balance
2302                        assert fresh_value == b_balance
2303
2304            # Transfer all in many chunks randomly from B to A
2305            a_SAR_balance = 1371.25
2306            b_USD_balance = 501
2307            b_USD_exchange = self.exchange(b_USD)
2308            amounts = ZakatTracker.create_random_list(b_USD_balance)
2309            if debug:
2310                print('amounts', amounts)
2311            i = 0
2312            for x in amounts:
2313                if debug:
2314                    print(f'{i} - transfer-with-exchange({x})')
2315                self.transfer(x, b_USD, a_SAR, f"{x} USD -> SAR", debug=debug)
2316
2317                b_USD_balance -= x
2318                cached_value = self.balance(b_USD, cached=True)
2319                fresh_value = self.balance(b_USD, cached=False)
2320                if debug:
2321                    print('account', b_USD, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted',
2322                          b_USD_balance)
2323                assert cached_value == b_USD_balance
2324                assert fresh_value == b_USD_balance
2325
2326                a_SAR_balance += x * b_USD_exchange['rate']
2327                cached_value = self.balance(a_SAR, cached=True)
2328                fresh_value = self.balance(a_SAR, cached=False)
2329                if debug:
2330                    print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected',
2331                          a_SAR_balance, 'rate', b_USD_exchange['rate'])
2332                assert cached_value == a_SAR_balance
2333                assert fresh_value == a_SAR_balance
2334                i += 1
2335
2336            # Transfer all in many chunks randomly from C to A
2337            c_SAR_balance = 375
2338            amounts = ZakatTracker.create_random_list(c_SAR_balance)
2339            if debug:
2340                print('amounts', amounts)
2341            i = 0
2342            for x in amounts:
2343                if debug:
2344                    print(f'{i} - transfer-with-exchange({x})')
2345                self.transfer(x, c_SAR, a_SAR, f"{x} SAR -> a_SAR", debug=debug)
2346
2347                c_SAR_balance -= x
2348                cached_value = self.balance(c_SAR, cached=True)
2349                fresh_value = self.balance(c_SAR, cached=False)
2350                if debug:
2351                    print('account', c_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted',
2352                          c_SAR_balance)
2353                assert cached_value == c_SAR_balance
2354                assert fresh_value == c_SAR_balance
2355
2356                a_SAR_balance += x
2357                cached_value = self.balance(a_SAR, cached=True)
2358                fresh_value = self.balance(a_SAR, cached=False)
2359                if debug:
2360                    print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected',
2361                          a_SAR_balance)
2362                assert cached_value == a_SAR_balance
2363                assert fresh_value == a_SAR_balance
2364                i += 1
2365
2366            assert self.export_json("accounts-transfer-with-exchange-rates.json")
2367            assert self.save("accounts-transfer-with-exchange-rates.pickle")
2368
2369            # check & zakat with exchange rates for many cycles
2370
2371            for rate, values in {
2372                1: {
2373                    'in': [1000, 2000, 10000],
2374                    'exchanged': [1000, 2000, 10000],
2375                    'out': [25, 50, 731.40625],
2376                },
2377                3.75: {
2378                    'in': [200, 1000, 5000],
2379                    'exchanged': [750, 3750, 18750],
2380                    'out': [18.75, 93.75, 1371.38671875],
2381                },
2382            }.items():
2383                a, b, c = values['in']
2384                m, n, o = values['exchanged']
2385                x, y, z = values['out']
2386                if debug:
2387                    print('rate', rate, 'values', values)
2388                for case in [
2389                    (a, 'safe', ZakatTracker.time() - ZakatTracker.TimeCycle(), [
2390                        {'safe': {0: {'below_nisab': x}}},
2391                    ], False, m),
2392                    (b, 'safe', ZakatTracker.time() - ZakatTracker.TimeCycle(), [
2393                        {'safe': {0: {'count': 1, 'total': y}}},
2394                    ], True, n),
2395                    (c, 'cave', ZakatTracker.time() - (ZakatTracker.TimeCycle() * 3), [
2396                        {'cave': {0: {'count': 3, 'total': z}}},
2397                    ], True, o),
2398                ]:
2399                    if debug:
2400                        print(f"############# check(rate: {rate}) #############")
2401                    self.reset()
2402                    self.exchange(account=case[1], created=case[2], rate=rate)
2403                    self.track(value=case[0], desc='test-check', account=case[1], logging=True, created=case[2])
2404
2405                    # assert self.nolock()
2406                    # history_size = len(self._vault['history'])
2407                    # print('history_size', history_size)
2408                    # assert history_size == 2
2409                    assert self.lock()
2410                    assert not self.nolock()
2411                    report = self.check(2.17, None, debug)
2412                    (valid, brief, plan) = report
2413                    assert valid == case[4]
2414                    if debug:
2415                        print('brief', brief)
2416                    assert case[5] == brief[0]
2417                    assert case[5] == brief[1]
2418
2419                    if debug:
2420                        pp().pprint(plan)
2421
2422                    for x in plan:
2423                        assert case[1] == x
2424                        if 'total' in case[3][0][x][0].keys():
2425                            assert case[3][0][x][0]['total'] == brief[2]
2426                            assert plan[x][0]['total'] == case[3][0][x][0]['total']
2427                            assert plan[x][0]['count'] == case[3][0][x][0]['count']
2428                        else:
2429                            assert plan[x][0]['below_nisab'] == case[3][0][x][0]['below_nisab']
2430                    if debug:
2431                        pp().pprint(report)
2432                    result = self.zakat(report, debug=debug)
2433                    if debug:
2434                        print('zakat-result', result, case[4])
2435                    assert result == case[4]
2436                    report = self.check(2.17, None, debug)
2437                    (valid, brief, plan) = report
2438                    assert valid is False
2439
2440            history_size = len(self._vault['history'])
2441            if debug:
2442                print('history_size', history_size)
2443            assert history_size == 3
2444            assert not self.nolock()
2445            assert self.recall(False, debug) is False
2446            self.free(self.lock())
2447            assert self.nolock()
2448            for i in range(3, 0, -1):
2449                history_size = len(self._vault['history'])
2450                if debug:
2451                    print('history_size', history_size)
2452                assert history_size == i
2453                assert self.recall(False, debug) is True
2454
2455            assert self.nolock()
2456
2457            assert self.recall(False, debug) is False
2458            history_size = len(self._vault['history'])
2459            if debug:
2460                print('history_size', history_size)
2461            assert history_size == 0
2462
2463            assert len(self._vault['account']) == 0
2464            assert len(self._vault['history']) == 0
2465            assert len(self._vault['report']) == 0
2466            assert self.nolock()
2467            return True
2468        except:
2469            # pp().pprint(self._vault)
2470            assert self.export_json("test-snapshot.json")
2471            assert self.save("test-snapshot.pickle")
2472            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'>