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

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