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

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 {"time": created, "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                result = latest_rate[1]
786                result['time'] = latest_rate[0]
787                return result  # إرجاع قاموس يحتوي على المعدل والوصف
788        if debug:
789            print("exchange-read-0", f'account: {account}, created: {created}, rate:{rate}, description:{description}')
790        return {"time": created, "rate": 1, "description": None}  # إرجاع القيمة الافتراضية مع وصف فارغ

This method is used to record or retrieve exchange rates for a specific account.

Parameters:

  • account (str): The account number for which the exchange rate is being recorded or retrieved.
  • created (int): The timestamp of the exchange rate. If not provided, the current timestamp will be used.
  • rate (float): The exchange rate to be recorded. If not provided, the method will retrieve the latest exchange rate.
  • description (str): A description of the exchange rate.

Returns:

  • dict: A dictionary containing the latest exchange rate and its description. If no exchange rate is found, it returns a dictionary with default values for the rate and description.
@staticmethod
def exchange_calc(x: float, x_rate: float, y_rate: float) -> float:
792    @staticmethod
793    def exchange_calc(x: float, x_rate: float, y_rate: float) -> float:
794        """
795        This function calculates the exchanged amount of a currency.
796
797        Args:
798            x (float): The original amount of the currency.
799            x_rate (float): The exchange rate of the original currency.
800            y_rate (float): The exchange rate of the target currency.
801
802        Returns:
803            float: The exchanged amount of the target currency.
804        """
805        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:
807    def exchanges(self) -> dict:
808        """
809        Retrieve the recorded exchange rates for all accounts.
810
811        Parameters:
812        None
813
814        Returns:
815        dict: A dictionary containing all recorded exchange rates.
816        The keys are account names or numbers, and the values are dictionaries containing the exchange rates.
817        Each exchange rate dictionary has timestamps as keys and exchange rate details as values.
818        """
819        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:
821    def accounts(self) -> dict:
822        """
823        Returns a dictionary containing account numbers as keys and their respective balances as values.
824
825        Parameters:
826        None
827
828        Returns:
829        dict: A dictionary where keys are account numbers and values are their respective balances.
830        """
831        result = {}
832        for i in self._vault['account']:
833            result[i] = self._vault['account'][i]['balance']
834        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:
836    def boxes(self, account) -> dict:
837        """
838        Retrieve the boxes (transactions) associated with a specific account.
839
840        Parameters:
841        account (str): The account number for which to retrieve the boxes.
842
843        Returns:
844        dict: A dictionary containing the boxes associated with the given account.
845        If the account does not exist, an empty dictionary is returned.
846        """
847        if self.account_exists(account):
848            return self._vault['account'][account]['box']
849        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:
851    def logs(self, account) -> dict:
852        """
853        Retrieve the logs (transactions) associated with a specific account.
854
855        Parameters:
856        account (str): The account number for which to retrieve the logs.
857
858        Returns:
859        dict: A dictionary containing the logs associated with the given account.
860        If the account does not exist, an empty dictionary is returned.
861        """
862        if self.account_exists(account):
863            return self._vault['account'][account]['log']
864        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:
866    def add_file(self, account: str, ref: int, path: str) -> int:
867        """
868        Adds a file reference to a specific transaction log entry in the vault.
869
870        Parameters:
871        account (str): The account number associated with the transaction log.
872        ref (int): The reference to the transaction log entry.
873        path (str): The path of the file to be added.
874
875        Returns:
876        int: The reference of the added file. If the account or transaction log entry does not exist, returns 0.
877        """
878        if self.account_exists(account):
879            if ref in self._vault['account'][account]['log']:
880                file_ref = self.time()
881                self._vault['account'][account]['log'][ref]['file'][file_ref] = path
882                no_lock = self.nolock()
883                self.lock()
884                self._step(Action.ADD_FILE, account, ref=ref, file=file_ref)
885                if no_lock:
886                    self.free(self.lock())
887                return file_ref
888        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:
890    def remove_file(self, account: str, ref: int, file_ref: int) -> bool:
891        """
892        Removes a file reference from a specific transaction log entry in the vault.
893
894        Parameters:
895        account (str): The account number associated with the transaction log.
896        ref (int): The reference to the transaction log entry.
897        file_ref (int): The reference of the file to be removed.
898
899        Returns:
900        bool: True if the file reference is successfully removed, False otherwise.
901        """
902        if self.account_exists(account):
903            if ref in self._vault['account'][account]['log']:
904                if file_ref in self._vault['account'][account]['log'][ref]['file']:
905                    x = self._vault['account'][account]['log'][ref]['file'][file_ref]
906                    del self._vault['account'][account]['log'][ref]['file'][file_ref]
907                    no_lock = self.nolock()
908                    self.lock()
909                    self._step(Action.REMOVE_FILE, account, ref=ref, file=file_ref, value=x)
910                    if no_lock:
911                        self.free(self.lock())
912                    return True
913        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:
915    def balance(self, account: str = 1, cached: bool = True) -> int:
916        """
917        Calculate and return the balance of a specific account.
918
919        Parameters:
920        account (str): The account number. Default is '1'.
921        cached (bool): If True, use the cached balance. If False, calculate the balance from the box. Default is True.
922
923        Returns:
924        int: The balance of the account.
925
926        Note:
927        If cached is True, the function returns the cached balance.
928        If cached is False, the function calculates the balance from the box by summing up the 'rest' values of all box items.
929        """
930        if cached:
931            return self._vault['account'][account]['balance']
932        x = 0
933        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:
935    def hide(self, account, status: bool = None) -> bool:
936        """
937        Check or set the hide status of a specific account.
938
939        Parameters:
940        account (str): The account number.
941        status (bool, optional): The new hide status. If not provided, the function will return the current status.
942
943        Returns:
944        bool: The current or updated hide status of the account.
945
946        Raises:
947        None
948
949        Example:
950        >>> tracker = ZakatTracker()
951        >>> ref = tracker.track(51, 'desc', 'account1')
952        >>> tracker.hide('account1')  # Set the hide status of 'account1' to True
953        False
954        >>> tracker.hide('account1', True)  # Set the hide status of 'account1' to True
955        True
956        >>> tracker.hide('account1')  # Get the hide status of 'account1' by default
957        True
958        >>> tracker.hide('account1', False)
959        False
960        """
961        if self.account_exists(account):
962            if status is None:
963                return self._vault['account'][account]['hide']
964            self._vault['account'][account]['hide'] = status
965            return status
966        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:
968    def zakatable(self, account, status: bool = None) -> bool:
969        """
970        Check or set the zakatable status of a specific account.
971
972        Parameters:
973        account (str): The account number.
974        status (bool, optional): The new zakatable status. If not provided, the function will return the current status.
975
976        Returns:
977        bool: The current or updated zakatable status of the account.
978
979        Raises:
980        None
981
982        Example:
983        >>> tracker = ZakatTracker()
984        >>> ref = tracker.track(51, 'desc', 'account1')
985        >>> tracker.zakatable('account1')  # Set the zakatable status of 'account1' to True
986        True
987        >>> tracker.zakatable('account1', True)  # Set the zakatable status of 'account1' to True
988        True
989        >>> tracker.zakatable('account1')  # Get the zakatable status of 'account1' by default
990        True
991        >>> tracker.zakatable('account1', False)
992        False
993        """
994        if self.account_exists(account):
995            if status is None:
996                return self._vault['account'][account]['zakatable']
997            self._vault['account'][account]['zakatable'] = status
998            return status
999        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:
1001    def sub(self, x: float, desc: str = '', account: str = 1, created: int = None, debug: bool = False) -> tuple:
1002        """
1003        Subtracts a specified value from an account's balance.
1004
1005        Parameters:
1006        x (float): The amount to be subtracted.
1007        desc (str): A description for the transaction. Defaults to an empty string.
1008        account (str): The account from which the value will be subtracted. Defaults to '1'.
1009        created (int): The timestamp of the transaction. If not provided, the current timestamp will be used.
1010        debug (bool): A flag indicating whether to print debug information. Defaults to False.
1011
1012        Returns:
1013        tuple: A tuple containing the timestamp of the transaction and a list of tuples representing the age of each transaction.
1014
1015        If the amount to subtract is greater than the account's balance,
1016        the remaining amount will be transferred to a new transaction with a negative value.
1017
1018        Raises:
1019        ValueError: The box transaction happened again in the same nanosecond time.
1020        ValueError: The log transaction happened again in the same nanosecond time.
1021        """
1022        if x < 0:
1023            return tuple()
1024        if x == 0:
1025            ref = self.track(x, '', account)
1026            return ref, ref
1027        if created is None:
1028            created = self.time()
1029        no_lock = self.nolock()
1030        self.lock()
1031        self.track(0, '', account)
1032        self._log(-x, desc, account, created)
1033        ids = sorted(self._vault['account'][account]['box'].keys())
1034        limit = len(ids) + 1
1035        target = x
1036        if debug:
1037            print('ids', ids)
1038        ages = []
1039        for i in range(-1, -limit, -1):
1040            if target == 0:
1041                break
1042            j = ids[i]
1043            if debug:
1044                print('i', i, 'j', j)
1045            rest = self._vault['account'][account]['box'][j]['rest']
1046            if rest >= target:
1047                self._vault['account'][account]['box'][j]['rest'] -= target
1048                self._step(Action.SUB, account, ref=j, value=target)
1049                ages.append((j, target))
1050                target = 0
1051                break
1052            elif target > rest > 0:
1053                chunk = rest
1054                target -= chunk
1055                self._step(Action.SUB, account, ref=j, value=chunk)
1056                ages.append((j, chunk))
1057                self._vault['account'][account]['box'][j]['rest'] = 0
1058        if target > 0:
1059            self.track(-target, desc, account, False, created)
1060            ages.append((created, target))
1061        if no_lock:
1062            self.free(self.lock())
1063        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]:
1065    def transfer(self, amount: int, from_account: str, to_account: str, desc: str = '', created: int = None,
1066                 debug: bool = False) -> list[int]:
1067        """
1068        Transfers a specified value from one account to another.
1069
1070        Parameters:
1071        amount (int): The amount to be transferred.
1072        from_account (str): The account from which the value will be transferred.
1073        to_account (str): The account to which the value will be transferred.
1074        desc (str, optional): A description for the transaction. Defaults to an empty string.
1075        created (int, optional): The timestamp of the transaction. If not provided, the current timestamp will be used.
1076        debug (bool): A flag indicating whether to print debug information. Defaults to False.
1077
1078        Returns:
1079        list[int]: A list of timestamps corresponding to the transactions made during the transfer.
1080
1081        Raises:
1082        ValueError: Transfer to the same account is forbidden.
1083        ValueError: The box transaction happened again in the same nanosecond time.
1084        ValueError: The log transaction happened again in the same nanosecond time.
1085        """
1086        if from_account == to_account:
1087            raise ValueError(f'Transfer to the same account is forbidden. {to_account}')
1088        if amount <= 0:
1089            return []
1090        if created is None:
1091            created = self.time()
1092        (_, ages) = self.sub(amount, desc, from_account, created, debug=debug)
1093        times = []
1094        source_exchange = self.exchange(from_account, created)
1095        target_exchange = self.exchange(to_account, created)
1096
1097        if debug:
1098            print('ages', ages)
1099
1100        for age, value in ages:
1101            target_amount = self.exchange_calc(value, source_exchange['rate'], target_exchange['rate'])
1102            # Perform the transfer
1103            if self.box_exists(to_account, age):
1104                if debug:
1105                    print('box_exists', age)
1106                capital = self._vault['account'][to_account]['box'][age]['capital']
1107                rest = self._vault['account'][to_account]['box'][age]['rest']
1108                if debug:
1109                    print(
1110                        f"Transfer {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).")
1111                selected_age = age
1112                if rest + target_amount > capital:
1113                    self._vault['account'][to_account]['box'][age]['capital'] += target_amount
1114                    selected_age = ZakatTracker.time()
1115                self._vault['account'][to_account]['box'][age]['rest'] += target_amount
1116                self._step(Action.BOX_TRANSFER, to_account, ref=selected_age, value=target_amount)
1117                y = self._log(target_amount, desc=f'TRANSFER {from_account} -> {to_account}', account=to_account,
1118                              debug=debug)
1119                times.append((age, y))
1120                continue
1121            y = self.track(target_amount, desc, to_account, logging=True, created=age, debug=debug)
1122            if debug:
1123                print(
1124                    f"Transferred {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).")
1125            times.append(y)
1126        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:
1128    def check(self, silver_gram_price: float, nisab: float = None, debug: bool = False, now: int = None,
1129              cycle: float = None) -> tuple:
1130        """
1131        Check the eligibility for Zakat based on the given parameters.
1132
1133        Parameters:
1134        silver_gram_price (float): The price of a gram of silver.
1135        nisab (float): The minimum amount of wealth required for Zakat. If not provided,
1136                        it will be calculated based on the silver_gram_price.
1137        debug (bool): Flag to enable debug mode.
1138        now (int): The current timestamp. If not provided, it will be calculated using ZakatTracker.time().
1139        cycle (float): The time cycle for Zakat. If not provided, it will be calculated using ZakatTracker.TimeCycle().
1140
1141        Returns:
1142        tuple: A tuple containing a boolean indicating the eligibility for Zakat, a list of brief statistics,
1143        and a dictionary containing the Zakat plan.
1144        """
1145        if now is None:
1146            now = self.time()
1147        if cycle is None:
1148            cycle = ZakatTracker.TimeCycle()
1149        if nisab is None:
1150            nisab = ZakatTracker.Nisab(silver_gram_price)
1151        plan = {}
1152        below_nisab = 0
1153        brief = [0, 0, 0]
1154        valid = False
1155        for x in self._vault['account']:
1156            if not self.zakatable(x):
1157                continue
1158            _box = self._vault['account'][x]['box']
1159            _log = self._vault['account'][x]['log']
1160            limit = len(_box) + 1
1161            ids = sorted(self._vault['account'][x]['box'].keys())
1162            for i in range(-1, -limit, -1):
1163                j = ids[i]
1164                rest = _box[j]['rest']
1165                if rest <= 0:
1166                    continue
1167                exchange = self.exchange(x, created=self.time())
1168                if debug:
1169                    print('exchanges', self.exchanges())
1170                rest = ZakatTracker.exchange_calc(rest, exchange['rate'], 1)
1171                brief[0] += rest
1172                index = limit + i - 1
1173                epoch = (now - j) / cycle
1174                if debug:
1175                    print(f"Epoch: {epoch}", _box[j])
1176                if _box[j]['last'] > 0:
1177                    epoch = (now - _box[j]['last']) / cycle
1178                if debug:
1179                    print(f"Epoch: {epoch}")
1180                epoch = floor(epoch)
1181                if debug:
1182                    print(f"Epoch: {epoch}", type(epoch), epoch == 0, 1 - epoch, epoch)
1183                if epoch == 0:
1184                    continue
1185                if debug:
1186                    print("Epoch - PASSED")
1187                brief[1] += rest
1188                if rest >= nisab:
1189                    total = 0
1190                    for _ in range(epoch):
1191                        total += ZakatTracker.ZakatCut(float(rest) - float(total))
1192                    if total > 0:
1193                        if x not in plan:
1194                            plan[x] = {}
1195                        valid = True
1196                        brief[2] += total
1197                        plan[x][index] = {
1198                            'total': total,
1199                            'count': epoch,
1200                            'box_time': j,
1201                            'box_capital': _box[j]['capital'],
1202                            'box_rest': _box[j]['rest'],
1203                            'box_last': _box[j]['last'],
1204                            'box_total': _box[j]['total'],
1205                            'box_count': _box[j]['count'],
1206                            'box_log': _log[j]['desc'],
1207                            'exchange_rate': exchange['rate'],
1208                            'exchange_time': exchange['time'],
1209                            'exchange_desc': exchange['description'],
1210                        }
1211                else:
1212                    chunk = ZakatTracker.ZakatCut(float(rest))
1213                    if chunk > 0:
1214                        if x not in plan:
1215                            plan[x] = {}
1216                        if j not in plan[x].keys():
1217                            plan[x][index] = {}
1218                        below_nisab += rest
1219                        brief[2] += chunk
1220                        plan[x][index]['below_nisab'] = chunk
1221                        plan[x][index]['box_time'] = j
1222                        plan[x][index]['box_capital'] = _box[j]['capital']
1223                        plan[x][index]['box_rest'] = _box[j]['rest']
1224                        plan[x][index]['box_last'] = _box[j]['last']
1225                        plan[x][index]['box_total'] = _box[j]['total']
1226                        plan[x][index]['box_count'] = _box[j]['count']
1227                        plan[x][index]['box_log'] = _log[j]['desc']
1228                        plan[x][index]['exchange_rate'] = exchange['rate']
1229                        plan[x][index]['exchange_time'] = exchange['time']
1230                        plan[x][index]['exchange_desc'] = exchange['description']
1231        valid = valid or below_nisab >= nisab
1232        if debug:
1233            print(f"below_nisab({below_nisab}) >= nisab({nisab})")
1234        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:
1236    def build_payment_parts(self, demand: float, positive_only: bool = True) -> dict:
1237        """
1238        Build payment parts for the Zakat distribution.
1239
1240        Parameters:
1241        demand (float): The total demand for payment in local currency.
1242        positive_only (bool): If True, only consider accounts with positive balance. Default is True.
1243
1244        Returns:
1245        dict: A dictionary containing the payment parts for each account. The dictionary has the following structure:
1246        {
1247            'account': {
1248                'account_id': {'balance': float, 'rate': float, 'part': float},
1249                ...
1250            },
1251            'exceed': bool,
1252            'demand': float,
1253            'total': float,
1254        }
1255        """
1256        total = 0
1257        parts = {
1258            'account': {},
1259            'exceed': False,
1260            'demand': demand,
1261        }
1262        for x, y in self.accounts().items():
1263            if positive_only and y <= 0:
1264                continue
1265            total += y
1266            exchange = self.exchange(x)
1267            parts['account'][x] = {'balance': y, 'rate': exchange['rate'], 'part': 0}
1268        parts['total'] = total
1269        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:
1271    @staticmethod
1272    def check_payment_parts(parts: dict) -> int:
1273        """
1274        Checks the validity of payment parts.
1275
1276        Parameters:
1277        parts (dict): A dictionary containing payment parts information.
1278
1279        Returns:
1280        int: Returns 0 if the payment parts are valid, otherwise returns the error code.
1281
1282        Error Codes:
1283        1: 'demand', 'account', 'total', or 'exceed' key is missing in parts.
1284        2: 'balance', 'rate' or 'part' key is missing in parts['account'][x].
1285        3: 'part' value in parts['account'][x] is less than or equal to 0.
1286        4: If 'exceed' is False, 'balance' value in parts['account'][x] is less than or equal to 0.
1287        5: 'part' value in parts['account'][x] is less than 0.
1288        6: If 'exceed' is False, 'part' value in parts['account'][x] is greater than 'balance' value.
1289        7: The sum of 'part' values in parts['account'] does not match with 'demand' value.
1290        """
1291        for i in ['demand', 'account', 'total', 'exceed']:
1292            if i not in parts:
1293                return 1
1294        exceed = parts['exceed']
1295        for x in parts['account']:
1296            for j in ['balance', 'rate', 'part']:
1297                if j not in parts['account'][x]:
1298                    return 2
1299                if parts['account'][x]['part'] <= 0:
1300                    return 3
1301                if not exceed and parts['account'][x]['balance'] <= 0:
1302                    return 4
1303        demand = parts['demand']
1304        z = 0
1305        for _, y in parts['account'].items():
1306            if y['part'] < 0:
1307                return 5
1308            if not exceed and y['part'] > y['balance']:
1309                return 6
1310            z += ZakatTracker.exchange_calc(y['part'], y['rate'], 1)
1311        if z != demand:
1312            return 7
1313        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:
1315    def zakat(self, report: tuple, parts: List[Dict[str, Dict | bool | Any]] = None, debug: bool = False) -> bool:
1316        """
1317        Perform Zakat calculation based on the given report and optional parts.
1318
1319        Parameters:
1320        report (tuple): A tuple containing the validity of the report, the report data, and the zakat plan.
1321        parts (dict): A dictionary containing the payment parts for the zakat.
1322        debug (bool): A flag indicating whether to print debug information.
1323
1324        Returns:
1325        bool: True if the zakat calculation is successful, False otherwise.
1326        """
1327        valid, _, plan = report
1328        if not valid:
1329            return valid
1330        parts_exist = parts is not None
1331        if parts_exist:
1332            for part in parts:
1333                if self.check_payment_parts(part) != 0:
1334                    return False
1335        if debug:
1336            print('######### zakat #######')
1337            print('parts_exist', parts_exist)
1338        no_lock = self.nolock()
1339        self.lock()
1340        report_time = self.time()
1341        self._vault['report'][report_time] = report
1342        self._step(Action.REPORT, ref=report_time)
1343        created = self.time()
1344        for x in plan:
1345            if debug:
1346                print(plan[x])
1347                print('-------------')
1348                print(self._vault['account'][x]['box'])
1349            ids = sorted(self._vault['account'][x]['box'].keys())
1350            if debug:
1351                print('plan[x]', plan[x])
1352            for i in plan[x].keys():
1353                j = ids[i]
1354                if debug:
1355                    print('i', i, 'j', j)
1356                self._step(Action.ZAKAT, account=x, ref=j, value=self._vault['account'][x]['box'][j]['last'],
1357                           key='last',
1358                           math_operation=MathOperation.EQUAL)
1359                self._vault['account'][x]['box'][j]['last'] = created
1360                self._vault['account'][x]['box'][j]['total'] += plan[x][i]['total']
1361                self._step(Action.ZAKAT, account=x, ref=j, value=plan[x][i]['total'], key='total',
1362                           math_operation=MathOperation.ADDITION)
1363                self._vault['account'][x]['box'][j]['count'] += plan[x][i]['count']
1364                self._step(Action.ZAKAT, account=x, ref=j, value=plan[x][i]['count'], key='count',
1365                           math_operation=MathOperation.ADDITION)
1366                if not parts_exist:
1367                    self._vault['account'][x]['box'][j]['rest'] -= plan[x][i]['total']
1368                    self._step(Action.ZAKAT, account=x, ref=j, value=plan[x][i]['total'], key='rest',
1369                               math_operation=MathOperation.SUBTRACTION)
1370        if parts_exist:
1371            for transaction in parts:
1372                for account, part in transaction['account'].items():
1373                    if debug:
1374                        print('zakat-part', account, part['part'])
1375                    target_exchange = self.exchange(account)
1376                    amount = ZakatTracker.exchange_calc(part['part'], part['rate'], target_exchange['rate'])
1377                    self.sub(amount, desc='zakat-part', account=account, debug=debug)
1378        if no_lock:
1379            self.free(self.lock())
1380        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:
1382    def export_json(self, path: str = "data.json") -> bool:
1383        """
1384        Exports the current state of the ZakatTracker object to a JSON file.
1385
1386        Parameters:
1387        path (str): The path where the JSON file will be saved. Default is "data.json".
1388
1389        Returns:
1390        bool: True if the export is successful, False otherwise.
1391
1392        Raises:
1393        No specific exceptions are raised by this method.
1394        """
1395        with open(path, "w") as file:
1396            json.dump(self._vault, file, indent=4, cls=JSONEncoder)
1397            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:
1399    def save(self, path: str = None) -> bool:
1400        """
1401        Saves the ZakatTracker's current state to a pickle file.
1402
1403        This method serializes the internal data (`_vault`) along with metadata
1404        (Python version, pickle protocol) for future compatibility.
1405
1406        Parameters:
1407            path (str, optional): File path for saving. Defaults to a predefined location.
1408
1409        Returns:
1410            bool: True if the save operation is successful, False otherwise.
1411        """
1412        if path is None:
1413            path = self.path()
1414        with open(path, "wb") as f:
1415            version = f'{version_info.major}.{version_info.minor}.{version_info.micro}'
1416            pickle_protocol = pickle.HIGHEST_PROTOCOL
1417            data = {
1418                'python_version': version,
1419                'pickle_protocol': pickle_protocol,
1420                'data': self._vault,
1421            }
1422            pickle.dump(data, f, protocol=pickle_protocol)
1423            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:
1425    def load(self, path: str = None) -> bool:
1426        """
1427        Load the current state of the ZakatTracker object from a pickle file.
1428
1429        Parameters:
1430        path (str): The path where the pickle file is located. If not provided, it will use the default path.
1431
1432        Returns:
1433        bool: True if the load operation is successful, False otherwise.
1434        """
1435        if path is None:
1436            path = self.path()
1437        if os.path.exists(path):
1438            with open(path, "rb") as f:
1439                data = pickle.load(f)
1440                self._vault = data['data']
1441                return True
1442        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):
1444    def import_csv_cache_path(self):
1445        """
1446        Generates the cache file path for imported CSV data.
1447
1448        This function constructs the file path where cached data from CSV imports
1449        will be stored. The cache file is a pickle file (.pickle extension) appended
1450        to the base path of the object.
1451
1452        Returns:
1453            str: The full path to the import CSV cache file.
1454
1455        Example:
1456            >>> obj = ZakatTracker('/data/reports')
1457            >>> obj.import_csv_cache_path()
1458            '/data/reports.import_csv.pickle'
1459        """
1460        path = self.path()
1461        if path.endswith(".pickle"):
1462            path = path[:-7]
1463        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:
1465    def import_csv(self, path: str = 'file.csv', debug: bool = False) -> tuple:
1466        """
1467        The function reads the CSV file, checks for duplicate transactions, and creates the transactions in the system.
1468
1469        Parameters:
1470        path (str): The path to the CSV file. Default is 'file.csv'.
1471        debug (bool): A flag indicating whether to print debug information.
1472
1473        Returns:
1474        tuple: A tuple containing the number of transactions created, the number of transactions found in the cache,
1475                and a dictionary of bad transactions.
1476
1477        Notes:
1478            * Currency Pair Assumption: This function assumes that the exchange rates stored for each account
1479                                        are appropriate for the currency pairs involved in the conversions.
1480            * The exchange rate for each account is based on the last encountered transaction rate that is not equal
1481                to 1.0 or the previous rate for that account.
1482            * Those rates will be merged into the exchange rates main data, and later it will be used for all subsequent
1483              transactions of the same account within the whole imported and existing dataset when doing `check` and
1484              `zakat` operations.
1485
1486        Example Usage:
1487            The CSV file should have the following format, rate is optional per transaction:
1488            account, desc, value, date, rate
1489            For example:
1490            safe-45, "Some text", 34872, 1988-06-30 00:00:00, 1
1491        """
1492        cache: list[int] = []
1493        try:
1494            with open(self.import_csv_cache_path(), "rb") as f:
1495                cache = pickle.load(f)
1496        except:
1497            pass
1498        date_formats = [
1499            "%Y-%m-%d %H:%M:%S",
1500            "%Y-%m-%dT%H:%M:%S",
1501            "%Y-%m-%dT%H%M%S",
1502            "%Y-%m-%d",
1503        ]
1504        created, found, bad = 0, 0, {}
1505        data: list[tuple] = []
1506        with open(path, newline='', encoding="utf-8") as f:
1507            i = 0
1508            for row in csv.reader(f, delimiter=','):
1509                i += 1
1510                hashed = hash(tuple(row))
1511                if hashed in cache:
1512                    found += 1
1513                    continue
1514                account = row[0]
1515                desc = row[1]
1516                value = float(row[2])
1517                rate = 1.0
1518                if row[4:5]:  # Empty list if index is out of range
1519                    rate = float(row[4])
1520                date: int = 0
1521                for time_format in date_formats:
1522                    try:
1523                        date = self.time(datetime.datetime.strptime(row[3], time_format))
1524                        break
1525                    except:
1526                        pass
1527                # TODO: not allowed for negative dates
1528                if date == 0 or value == 0:
1529                    bad[i] = row
1530                    continue
1531                if date in data:
1532                    print('import_csv-duplicated(time)', date)
1533                    continue
1534                data.append((date, value, desc, account, rate, hashed))
1535
1536        if debug:
1537            print('import_csv', len(data))
1538        for row in sorted(data, key=lambda x: x[0]):
1539            (date, value, desc, account, rate, hashed) = row
1540            if rate > 1:
1541                self.exchange(account, created=date, rate=rate)
1542            if value > 0:
1543                self.track(value, desc, account, True, date)
1544            elif value < 0:
1545                self.sub(-value, desc, account, date)
1546            created += 1
1547            cache.append(hashed)
1548        with open(self.import_csv_cache_path(), "wb") as f:
1549            pickle.dump(cache, f)
1550        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:
1556    @staticmethod
1557    def duration_from_nanoseconds(ns: int) -> tuple:
1558        """
1559        REF https://github.com/JayRizzo/Random_Scripts/blob/master/time_measure.py#L106
1560        Convert NanoSeconds to Human Readable Time Format.
1561        A NanoSeconds is a unit of time in the International System of Units (SI) equal
1562        to one millionth (0.000001 or 10−6 or 1⁄1,000,000) of a second.
1563        Its symbol is μs, sometimes simplified to us when Unicode is not available.
1564        A microsecond is equal to 1000 nanoseconds or 1⁄1,000 of a millisecond.
1565
1566        INPUT : ms (AKA: MilliSeconds)
1567        OUTPUT: tuple(string time_lapsed, string spoken_time) like format.
1568        OUTPUT Variables: time_lapsed, spoken_time
1569
1570        Example  Input: duration_from_nanoseconds(ns)
1571        **"Millennium:Century:Years:Days:Hours:Minutes:Seconds:MilliSeconds:MicroSeconds:NanoSeconds"**
1572        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')
1573        duration_from_nanoseconds(1234567890123456789012)
1574        """
1575        us, ns = divmod(ns, 1000)
1576        ms, us = divmod(us, 1000)
1577        s, ms = divmod(ms, 1000)
1578        m, s = divmod(s, 60)
1579        h, m = divmod(m, 60)
1580        d, h = divmod(h, 24)
1581        y, d = divmod(d, 365)
1582        c, y = divmod(y, 100)
1583        n, c = divmod(c, 10)
1584        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}"
1585        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"
1586        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:
1588    @staticmethod
1589    def day_to_time(day: int, month: int = 6, year: int = 2024) -> int:  # افتراض أن الشهر هو يونيو والسنة 2024
1590        """
1591        Convert a specific day, month, and year into a timestamp.
1592
1593        Parameters:
1594        day (int): The day of the month.
1595        month (int): The month of the year. Default is 6 (June).
1596        year (int): The year. Default is 2024.
1597
1598        Returns:
1599        int: The timestamp representing the given day, month, and year.
1600
1601        Note:
1602        This method assumes the default month and year if not provided.
1603        """
1604        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:
1606    @staticmethod
1607    def generate_random_date(start_date: datetime.datetime, end_date: datetime.datetime) -> datetime.datetime:
1608        """
1609        Generate a random date between two given dates.
1610
1611        Parameters:
1612        start_date (datetime.datetime): The start date from which to generate a random date.
1613        end_date (datetime.datetime): The end date until which to generate a random date.
1614
1615        Returns:
1616        datetime.datetime: A random date between the start_date and end_date.
1617        """
1618        time_between_dates = end_date - start_date
1619        days_between_dates = time_between_dates.days
1620        random_number_of_days = random.randrange(days_between_dates)
1621        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:
1623    @staticmethod
1624    def generate_random_csv_file(path: str = "data.csv", count: int = 1000, with_rate: bool = False,
1625                                 debug: bool = False) -> int:
1626        """
1627        Generate a random CSV file with specified parameters.
1628
1629        Parameters:
1630        path (str): The path where the CSV file will be saved. Default is "data.csv".
1631        count (int): The number of rows to generate in the CSV file. Default is 1000.
1632        with_rate (bool): If True, a random rate between 1.2% and 12% is added. Default is False.
1633        debug (bool): A flag indicating whether to print debug information.
1634
1635        Returns:
1636        None. The function generates a CSV file at the specified path with the given count of rows.
1637        Each row contains a randomly generated account, description, value, and date.
1638        The value is randomly generated between 1000 and 100000,
1639        and the date is randomly generated between 1950-01-01 and 2023-12-31.
1640        If the row number is not divisible by 13, the value is multiplied by -1.
1641        """
1642        i = 0
1643        with open(path, "w", newline="") as csvfile:
1644            writer = csv.writer(csvfile)
1645            for i in range(count):
1646                account = f"acc-{random.randint(1, 1000)}"
1647                desc = f"Some text {random.randint(1, 1000)}"
1648                value = random.randint(1000, 100000)
1649                date = ZakatTracker.generate_random_date(datetime.datetime(1950, 1, 1),
1650                                                         datetime.datetime(2023, 12, 31)).strftime("%Y-%m-%d %H:%M:%S")
1651                if not i % 13 == 0:
1652                    value *= -1
1653                row = [account, desc, value, date]
1654                if with_rate:
1655                    rate = random.randint(1, 100) * 0.12
1656                    if debug:
1657                        print('before-append', row)
1658                    row.append(rate)
1659                    if debug:
1660                        print('after-append', row)
1661                writer.writerow(row)
1662                i = i + 1
1663        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):
1665    @staticmethod
1666    def create_random_list(max_sum, min_value=0, max_value=10):
1667        """
1668        Creates a list of random integers whose sum does not exceed the specified maximum.
1669
1670        Args:
1671            max_sum: The maximum allowed sum of the list elements.
1672            min_value: The minimum possible value for an element (inclusive).
1673            max_value: The maximum possible value for an element (inclusive).
1674
1675        Returns:
1676            A list of random integers.
1677        """
1678        result = []
1679        current_sum = 0
1680
1681        while current_sum < max_sum:
1682            # Calculate the remaining space for the next element
1683            remaining_sum = max_sum - current_sum
1684            # Determine the maximum possible value for the next element
1685            next_max_value = min(remaining_sum, max_value)
1686            # Generate a random element within the allowed range
1687            next_element = random.randint(min_value, next_max_value)
1688            result.append(next_element)
1689            current_sum += next_element
1690
1691        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:
1836    def test(self, debug: bool = False) -> bool:
1837
1838        try:
1839
1840            assert self._history()
1841
1842            # Not allowed for duplicate transactions in the same account and time
1843
1844            created = ZakatTracker.time()
1845            self.track(100, 'test-1', 'same', True, created)
1846            failed = False
1847            try:
1848                self.track(50, 'test-1', 'same', True, created)
1849            except:
1850                failed = True
1851            assert failed is True
1852
1853            self.reset()
1854
1855            # Same account transfer
1856            for x in [1, 'a', True, 1.8, None]:
1857                failed = False
1858                try:
1859                    self.transfer(1, x, x, 'same-account', debug=debug)
1860                except:
1861                    failed = True
1862                assert failed is True
1863
1864            # Always preserve box age during transfer
1865
1866            series: list[tuple] = [
1867                (30, 4),
1868                (60, 3),
1869                (90, 2),
1870            ]
1871            case = {
1872                30: {
1873                    'series': series,
1874                    'rest': 150,
1875                },
1876                60: {
1877                    'series': series,
1878                    'rest': 120,
1879                },
1880                90: {
1881                    'series': series,
1882                    'rest': 90,
1883                },
1884                180: {
1885                    'series': series,
1886                    'rest': 0,
1887                },
1888                270: {
1889                    'series': series,
1890                    'rest': -90,
1891                },
1892                360: {
1893                    'series': series,
1894                    'rest': -180,
1895                },
1896            }
1897
1898            selected_time = ZakatTracker.time() - ZakatTracker.TimeCycle()
1899
1900            for total in case:
1901                for x in case[total]['series']:
1902                    self.track(x[0], f"test-{x} ages", 'ages', True, selected_time * x[1])
1903
1904                refs = self.transfer(total, 'ages', 'future', 'Zakat Movement', debug=debug)
1905
1906                if debug:
1907                    print('refs', refs)
1908
1909                ages_cache_balance = self.balance('ages')
1910                ages_fresh_balance = self.balance('ages', False)
1911                rest = case[total]['rest']
1912                if debug:
1913                    print('source', ages_cache_balance, ages_fresh_balance, rest)
1914                assert ages_cache_balance == rest
1915                assert ages_fresh_balance == rest
1916
1917                future_cache_balance = self.balance('future')
1918                future_fresh_balance = self.balance('future', False)
1919                if debug:
1920                    print('target', future_cache_balance, future_fresh_balance, total)
1921                    print('refs', refs)
1922                assert future_cache_balance == total
1923                assert future_fresh_balance == total
1924
1925                for ref in self._vault['account']['ages']['box']:
1926                    ages_capital = self._vault['account']['ages']['box'][ref]['capital']
1927                    ages_rest = self._vault['account']['ages']['box'][ref]['rest']
1928                    future_capital = 0
1929                    if ref in self._vault['account']['future']['box']:
1930                        future_capital = self._vault['account']['future']['box'][ref]['capital']
1931                    future_rest = 0
1932                    if ref in self._vault['account']['future']['box']:
1933                        future_rest = self._vault['account']['future']['box'][ref]['rest']
1934                    if ages_capital != 0 and future_capital != 0 and future_rest != 0:
1935                        if debug:
1936                            print('================================================================')
1937                            print('ages', ages_capital, ages_rest)
1938                            print('future', future_capital, future_rest)
1939                        if ages_rest == 0:
1940                            assert ages_capital == future_capital
1941                        elif ages_rest < 0:
1942                            assert -ages_capital == future_capital
1943                        elif ages_rest > 0:
1944                            assert ages_capital == ages_rest + future_capital
1945                self.reset()
1946                assert len(self._vault['history']) == 0
1947
1948            assert self._history()
1949            assert self._history(False) is False
1950            assert self._history() is False
1951            assert self._history(True)
1952            assert self._history()
1953
1954            self._test_core(True, debug)
1955            self._test_core(False, debug)
1956
1957            transaction = [
1958                (
1959                    20, 'wallet', 1, 800, 800, 800, 4, 5,
1960                    -85, -85, -85, 6, 7,
1961                ),
1962                (
1963                    750, 'wallet', 'safe', 50, 50, 50, 4, 6,
1964                    750, 750, 750, 1, 1,
1965                ),
1966                (
1967                    600, 'safe', 'bank', 150, 150, 150, 1, 2,
1968                    600, 600, 600, 1, 1,
1969                ),
1970            ]
1971            for z in transaction:
1972                self.lock()
1973                x = z[1]
1974                y = z[2]
1975                self.transfer(z[0], x, y, 'test-transfer', debug=debug)
1976                assert self.balance(x) == z[3]
1977                xx = self.accounts()[x]
1978                assert xx == z[3]
1979                assert self.balance(x, False) == z[4]
1980                assert xx == z[4]
1981
1982                s = 0
1983                log = self._vault['account'][x]['log']
1984                for i in log:
1985                    s += log[i]['value']
1986                if debug:
1987                    print('s', s, 'z[5]', z[5])
1988                assert s == z[5]
1989
1990                assert self.box_size(x) == z[6]
1991                assert self.log_size(x) == z[7]
1992
1993                yy = self.accounts()[y]
1994                assert self.balance(y) == z[8]
1995                assert yy == z[8]
1996                assert self.balance(y, False) == z[9]
1997                assert yy == z[9]
1998
1999                s = 0
2000                log = self._vault['account'][y]['log']
2001                for i in log:
2002                    s += log[i]['value']
2003                assert s == z[10]
2004
2005                assert self.box_size(y) == z[11]
2006                assert self.log_size(y) == z[12]
2007
2008            if debug:
2009                pp().pprint(self.check(2.17))
2010
2011            assert not self.nolock()
2012            history_count = len(self._vault['history'])
2013            if debug:
2014                print('history-count', history_count)
2015            assert history_count == 11
2016            assert not self.free(ZakatTracker.time())
2017            assert self.free(self.lock())
2018            assert self.nolock()
2019            assert len(self._vault['history']) == 11
2020
2021            # storage
2022
2023            _path = self.path('test.pickle')
2024            if os.path.exists(_path):
2025                os.remove(_path)
2026            self.save()
2027            assert os.path.getsize(_path) > 0
2028            self.reset()
2029            assert self.recall(False, debug) is False
2030            self.load()
2031            assert self._vault['account'] is not None
2032
2033            # recall
2034
2035            assert self.nolock()
2036            assert len(self._vault['history']) == 11
2037            assert self.recall(False, debug) is True
2038            assert len(self._vault['history']) == 10
2039            assert self.recall(False, debug) is True
2040            assert len(self._vault['history']) == 9
2041
2042            csv_count = 1000
2043
2044            for with_rate, path in {
2045                False: 'test-import_csv-no-exchange',
2046                True: 'test-import_csv-with-exchange',
2047            }.items():
2048
2049                if debug:
2050                    print('test_import_csv', with_rate, path)
2051
2052                # csv
2053
2054                csv_path = path + '.csv'
2055                if os.path.exists(csv_path):
2056                    os.remove(csv_path)
2057                c = self.generate_random_csv_file(csv_path, csv_count, with_rate, debug)
2058                if debug:
2059                    print('generate_random_csv_file', c)
2060                assert c == csv_count
2061                assert os.path.getsize(csv_path) > 0
2062                cache_path = self.import_csv_cache_path()
2063                if os.path.exists(cache_path):
2064                    os.remove(cache_path)
2065                self.reset()
2066                (created, found, bad) = self.import_csv(csv_path, debug)
2067                bad_count = len(bad)
2068                if debug:
2069                    print(f"csv-imported: ({created}, {found}, {bad_count}) = count({csv_count})")
2070                tmp_size = os.path.getsize(cache_path)
2071                assert tmp_size > 0
2072                assert created + found + bad_count == csv_count
2073                assert created == csv_count
2074                assert bad_count == 0
2075                (created_2, found_2, bad_2) = self.import_csv(csv_path)
2076                bad_2_count = len(bad_2)
2077                if debug:
2078                    print(f"csv-imported: ({created_2}, {found_2}, {bad_2_count})")
2079                    print(bad)
2080                assert tmp_size == os.path.getsize(cache_path)
2081                assert created_2 + found_2 + bad_2_count == csv_count
2082                assert created == found_2
2083                assert bad_count == bad_2_count
2084                assert found_2 == csv_count
2085                assert bad_2_count == 0
2086                assert created_2 == 0
2087
2088                # payment parts
2089
2090                positive_parts = self.build_payment_parts(100, positive_only=True)
2091                assert self.check_payment_parts(positive_parts) != 0
2092                assert self.check_payment_parts(positive_parts) != 0
2093                all_parts = self.build_payment_parts(300, positive_only=False)
2094                assert self.check_payment_parts(all_parts) != 0
2095                assert self.check_payment_parts(all_parts) != 0
2096                if debug:
2097                    pp().pprint(positive_parts)
2098                    pp().pprint(all_parts)
2099                # dynamic discount
2100                suite = []
2101                count = 3
2102                for exceed in [False, True]:
2103                    case = []
2104                    for parts in [positive_parts, all_parts]:
2105                        part = parts.copy()
2106                        demand = part['demand']
2107                        if debug:
2108                            print(demand, part['total'])
2109                        i = 0
2110                        z = demand / count
2111                        cp = {
2112                            'account': {},
2113                            'demand': demand,
2114                            'exceed': exceed,
2115                            'total': part['total'],
2116                        }
2117                        j = ''
2118                        for x, y in part['account'].items():
2119                            x_exchange = self.exchange(x)
2120                            zz = self.exchange_calc(z, 1, x_exchange['rate'])
2121                            if exceed and zz <= demand:
2122                                i += 1
2123                                y['part'] = zz
2124                                if debug:
2125                                    print(exceed, y)
2126                                cp['account'][x] = y
2127                                case.append(y)
2128                            elif not exceed and y['balance'] >= zz:
2129                                i += 1
2130                                y['part'] = zz
2131                                if debug:
2132                                    print(exceed, y)
2133                                cp['account'][x] = y
2134                                case.append(y)
2135                            j = x
2136                            if i >= count:
2137                                break
2138                        if len(cp['account'][j]) > 0:
2139                            suite.append(cp)
2140                if debug:
2141                    print('suite', len(suite))
2142                for case in suite:
2143                    if debug:
2144                        print(case)
2145                    result = self.check_payment_parts(case)
2146                    if debug:
2147                        print('check_payment_parts', result, f'exceed: {exceed}')
2148                    assert result == 0
2149
2150                report = self.check(2.17, None, debug)
2151                (valid, brief, plan) = report
2152                if debug:
2153                    print('valid', valid)
2154                assert self.zakat(report, parts=suite, debug=debug)
2155                assert self.save(path + '.pickle')
2156                assert self.export_json(path + '.json')
2157
2158            # exchange
2159
2160            self.exchange("cash", 25, 3.75, "2024-06-25")
2161            self.exchange("cash", 22, 3.73, "2024-06-22")
2162            self.exchange("cash", 15, 3.69, "2024-06-15")
2163            self.exchange("cash", 10, 3.66)
2164
2165            for i in range(1, 30):
2166                exchange = self.exchange("cash", i)
2167                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2168                if debug:
2169                    print(i, rate, description, created)
2170                assert created
2171                if i < 10:
2172                    assert rate == 1
2173                    assert description is None
2174                elif i == 10:
2175                    assert rate == 3.66
2176                    assert description is None
2177                elif i < 15:
2178                    assert rate == 3.66
2179                    assert description is None
2180                elif i == 15:
2181                    assert rate == 3.69
2182                    assert description is not None
2183                elif i < 22:
2184                    assert rate == 3.69
2185                    assert description is not None
2186                elif i == 22:
2187                    assert rate == 3.73
2188                    assert description is not None
2189                elif i >= 25:
2190                    assert rate == 3.75
2191                    assert description is not None
2192                exchange = self.exchange("bank", i)
2193                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2194                if debug:
2195                    print(i, rate, description, created)
2196                assert created
2197                assert rate == 1
2198                assert description is None
2199
2200            assert len(self._vault['exchange']) > 0
2201            assert len(self.exchanges()) > 0
2202            self._vault['exchange'].clear()
2203            assert len(self._vault['exchange']) == 0
2204            assert len(self.exchanges()) == 0
2205
2206            # حفظ أسعار الصرف باستخدام التواريخ بالنانو ثانية
2207            self.exchange("cash", ZakatTracker.day_to_time(25), 3.75, "2024-06-25")
2208            self.exchange("cash", ZakatTracker.day_to_time(22), 3.73, "2024-06-22")
2209            self.exchange("cash", ZakatTracker.day_to_time(15), 3.69, "2024-06-15")
2210            self.exchange("cash", ZakatTracker.day_to_time(10), 3.66)
2211
2212            for i in [x * 0.12 for x in range(-15, 21)]:
2213                if i <= 0:
2214                    assert len(self.exchange("test", ZakatTracker.time(), i, f"range({i})")) == 0
2215                else:
2216                    assert len(self.exchange("test", ZakatTracker.time(), i, f"range({i})")) > 0
2217
2218            # اختبار النتائج باستخدام التواريخ بالنانو ثانية
2219            for i in range(1, 31):
2220                timestamp_ns = ZakatTracker.day_to_time(i)
2221                exchange = self.exchange("cash", timestamp_ns)
2222                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2223                if debug:
2224                    print(i, rate, description, created)
2225                assert created
2226                if i < 10:
2227                    assert rate == 1
2228                    assert description is None
2229                elif i == 10:
2230                    assert rate == 3.66
2231                    assert description is None
2232                elif i < 15:
2233                    assert rate == 3.66
2234                    assert description is None
2235                elif i == 15:
2236                    assert rate == 3.69
2237                    assert description is not None
2238                elif i < 22:
2239                    assert rate == 3.69
2240                    assert description is not None
2241                elif i == 22:
2242                    assert rate == 3.73
2243                    assert description is not None
2244                elif i >= 25:
2245                    assert rate == 3.75
2246                    assert description is not None
2247                exchange = self.exchange("bank", i)
2248                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2249                if debug:
2250                    print(i, rate, description, created)
2251                assert created
2252                assert rate == 1
2253                assert description is None
2254
2255            assert self.export_json("1000-transactions-test.json")
2256            assert self.save("1000-transactions-test.pickle")
2257
2258            self.reset()
2259
2260            # test transfer between accounts with different exchange rate
2261
2262            a_SAR = "Bank (SAR)"
2263            b_USD = "Bank (USD)"
2264            c_SAR = "Safe (SAR)"
2265            # 0: track, 1: check-exchange, 2: do-exchange, 3: transfer
2266            for case in [
2267                (0, a_SAR, "SAR Gift", 1000, 1000),
2268                (1, a_SAR, 1),
2269                (0, b_USD, "USD Gift", 500, 500),
2270                (1, b_USD, 1),
2271                (2, b_USD, 3.75),
2272                (1, b_USD, 3.75),
2273                (3, 100, b_USD, a_SAR, "100 USD -> SAR", 400, 1375),
2274                (0, c_SAR, "Salary", 750, 750),
2275                (3, 375, c_SAR, b_USD, "375 SAR -> USD", 375, 500),
2276                (3, 3.75, a_SAR, b_USD, "3.75 SAR -> USD", 1371.25, 501),
2277            ]:
2278                match (case[0]):
2279                    case 0:  # track
2280                        _, account, desc, x, balance = case
2281                        self.track(value=x, desc=desc, account=account, debug=debug)
2282
2283                        cached_value = self.balance(account, cached=True)
2284                        fresh_value = self.balance(account, cached=False)
2285                        if debug:
2286                            print('account', account, 'cached_value', cached_value, 'fresh_value', fresh_value)
2287                        assert cached_value == balance
2288                        assert fresh_value == balance
2289                    case 1:  # check-exchange
2290                        _, account, expected_rate = case
2291                        t_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug)
2292                        if debug:
2293                            print('t-exchange', t_exchange)
2294                        assert t_exchange['rate'] == expected_rate
2295                    case 2:  # do-exchange
2296                        _, account, rate = case
2297                        self.exchange(account, rate=rate, debug=debug)
2298                        b_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug)
2299                        if debug:
2300                            print('b-exchange', b_exchange)
2301                        assert b_exchange['rate'] == rate
2302                    case 3:  # transfer
2303                        _, x, a, b, desc, a_balance, b_balance = case
2304                        self.transfer(x, a, b, desc, debug=debug)
2305
2306                        cached_value = self.balance(a, cached=True)
2307                        fresh_value = self.balance(a, cached=False)
2308                        if debug:
2309                            print('account', a, 'cached_value', cached_value, 'fresh_value', fresh_value)
2310                        assert cached_value == a_balance
2311                        assert fresh_value == a_balance
2312
2313                        cached_value = self.balance(b, cached=True)
2314                        fresh_value = self.balance(b, cached=False)
2315                        if debug:
2316                            print('account', b, 'cached_value', cached_value, 'fresh_value', fresh_value)
2317                        assert cached_value == b_balance
2318                        assert fresh_value == b_balance
2319
2320            # Transfer all in many chunks randomly from B to A
2321            a_SAR_balance = 1371.25
2322            b_USD_balance = 501
2323            b_USD_exchange = self.exchange(b_USD)
2324            amounts = ZakatTracker.create_random_list(b_USD_balance)
2325            if debug:
2326                print('amounts', amounts)
2327            i = 0
2328            for x in amounts:
2329                if debug:
2330                    print(f'{i} - transfer-with-exchange({x})')
2331                self.transfer(x, b_USD, a_SAR, f"{x} USD -> SAR", debug=debug)
2332
2333                b_USD_balance -= x
2334                cached_value = self.balance(b_USD, cached=True)
2335                fresh_value = self.balance(b_USD, cached=False)
2336                if debug:
2337                    print('account', b_USD, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted',
2338                          b_USD_balance)
2339                assert cached_value == b_USD_balance
2340                assert fresh_value == b_USD_balance
2341
2342                a_SAR_balance += x * b_USD_exchange['rate']
2343                cached_value = self.balance(a_SAR, cached=True)
2344                fresh_value = self.balance(a_SAR, cached=False)
2345                if debug:
2346                    print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected',
2347                          a_SAR_balance, 'rate', b_USD_exchange['rate'])
2348                assert cached_value == a_SAR_balance
2349                assert fresh_value == a_SAR_balance
2350                i += 1
2351
2352            # Transfer all in many chunks randomly from C to A
2353            c_SAR_balance = 375
2354            amounts = ZakatTracker.create_random_list(c_SAR_balance)
2355            if debug:
2356                print('amounts', amounts)
2357            i = 0
2358            for x in amounts:
2359                if debug:
2360                    print(f'{i} - transfer-with-exchange({x})')
2361                self.transfer(x, c_SAR, a_SAR, f"{x} SAR -> a_SAR", debug=debug)
2362
2363                c_SAR_balance -= x
2364                cached_value = self.balance(c_SAR, cached=True)
2365                fresh_value = self.balance(c_SAR, cached=False)
2366                if debug:
2367                    print('account', c_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted',
2368                          c_SAR_balance)
2369                assert cached_value == c_SAR_balance
2370                assert fresh_value == c_SAR_balance
2371
2372                a_SAR_balance += x
2373                cached_value = self.balance(a_SAR, cached=True)
2374                fresh_value = self.balance(a_SAR, cached=False)
2375                if debug:
2376                    print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected',
2377                          a_SAR_balance)
2378                assert cached_value == a_SAR_balance
2379                assert fresh_value == a_SAR_balance
2380                i += 1
2381
2382            assert self.export_json("accounts-transfer-with-exchange-rates.json")
2383            assert self.save("accounts-transfer-with-exchange-rates.pickle")
2384
2385            # check & zakat with exchange rates for many cycles
2386
2387            for rate, values in {
2388                1: {
2389                    'in': [1000, 2000, 10000],
2390                    'exchanged': [1000, 2000, 10000],
2391                    'out': [25, 50, 731.40625],
2392                },
2393                3.75: {
2394                    'in': [200, 1000, 5000],
2395                    'exchanged': [750, 3750, 18750],
2396                    'out': [18.75, 93.75, 1371.38671875],
2397                },
2398            }.items():
2399                a, b, c = values['in']
2400                m, n, o = values['exchanged']
2401                x, y, z = values['out']
2402                if debug:
2403                    print('rate', rate, 'values', values)
2404                for case in [
2405                    (a, 'safe', ZakatTracker.time() - ZakatTracker.TimeCycle(), [
2406                        {'safe': {0: {'below_nisab': x}}},
2407                    ], False, m),
2408                    (b, 'safe', ZakatTracker.time() - ZakatTracker.TimeCycle(), [
2409                        {'safe': {0: {'count': 1, 'total': y}}},
2410                    ], True, n),
2411                    (c, 'cave', ZakatTracker.time() - (ZakatTracker.TimeCycle() * 3), [
2412                        {'cave': {0: {'count': 3, 'total': z}}},
2413                    ], True, o),
2414                ]:
2415                    if debug:
2416                        print(f"############# check(rate: {rate}) #############")
2417                    self.reset()
2418                    self.exchange(account=case[1], created=case[2], rate=rate)
2419                    self.track(value=case[0], desc='test-check', account=case[1], logging=True, created=case[2])
2420
2421                    # assert self.nolock()
2422                    # history_size = len(self._vault['history'])
2423                    # print('history_size', history_size)
2424                    # assert history_size == 2
2425                    assert self.lock()
2426                    assert not self.nolock()
2427                    report = self.check(2.17, None, debug)
2428                    (valid, brief, plan) = report
2429                    assert valid == case[4]
2430                    if debug:
2431                        print('brief', brief)
2432                    assert case[5] == brief[0]
2433                    assert case[5] == brief[1]
2434
2435                    if debug:
2436                        pp().pprint(plan)
2437
2438                    for x in plan:
2439                        assert case[1] == x
2440                        if 'total' in case[3][0][x][0].keys():
2441                            assert case[3][0][x][0]['total'] == brief[2]
2442                            assert plan[x][0]['total'] == case[3][0][x][0]['total']
2443                            assert plan[x][0]['count'] == case[3][0][x][0]['count']
2444                        else:
2445                            assert plan[x][0]['below_nisab'] == case[3][0][x][0]['below_nisab']
2446                    if debug:
2447                        pp().pprint(report)
2448                    result = self.zakat(report, debug=debug)
2449                    if debug:
2450                        print('zakat-result', result, case[4])
2451                    assert result == case[4]
2452                    report = self.check(2.17, None, debug)
2453                    (valid, brief, plan) = report
2454                    assert valid is False
2455
2456            history_size = len(self._vault['history'])
2457            if debug:
2458                print('history_size', history_size)
2459            assert history_size == 3
2460            assert not self.nolock()
2461            assert self.recall(False, debug) is False
2462            self.free(self.lock())
2463            assert self.nolock()
2464            for i in range(3, 0, -1):
2465                history_size = len(self._vault['history'])
2466                if debug:
2467                    print('history_size', history_size)
2468                assert history_size == i
2469                assert self.recall(False, debug) is True
2470
2471            assert self.nolock()
2472
2473            assert self.recall(False, debug) is False
2474            history_size = len(self._vault['history'])
2475            if debug:
2476                print('history_size', history_size)
2477            assert history_size == 0
2478
2479            assert len(self._vault['account']) == 0
2480            assert len(self._vault['history']) == 0
2481            assert len(self._vault['report']) == 0
2482            assert self.nolock()
2483            return True
2484        except:
2485            # pp().pprint(self._vault)
2486            assert self.export_json("test-snapshot.json")
2487            assert self.save("test-snapshot.pickle")
2488            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'>