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

Returns the current version of the software.

This function returns a string representing the current version of the software, including major, minor, and patch version numbers in the format "X.Y.Z".

Returns: str: The current version of the software.

@staticmethod
def ZakatCut(x: float) -> float:
184    @staticmethod
185    def ZakatCut(x: float) -> float:
186        """
187        Calculates the Zakat amount due on an asset.
188
189        This function calculates the zakat amount due on a given asset value over one lunar year.
190        Zakat is an Islamic obligatory alms-giving, calculated as a fixed percentage of an individual's wealth
191        that exceeds a certain threshold (Nisab).
192
193        Parameters:
194        x: The total value of the asset on which Zakat is to be calculated.
195
196        Returns:
197        The amount of Zakat due on the asset, calculated as 2.5% of the asset's value.
198        """
199        return 0.025 * x  # Zakat Cut in one Lunar Year

Calculates the Zakat amount due on an asset.

This function calculates the zakat amount due on a given asset value over one lunar year. Zakat is an Islamic obligatory alms-giving, calculated as a fixed percentage of an individual's wealth that exceeds a certain threshold (Nisab).

Parameters: x: The total value of the asset on which Zakat is to be calculated.

Returns: The amount of Zakat due on the asset, calculated as 2.5% of the asset's value.

@staticmethod
def TimeCycle(days: int = 355) -> int:
201    @staticmethod
202    def TimeCycle(days: int = 355) -> int:
203        """
204        Calculates the approximate duration of a lunar year in nanoseconds.
205
206        This function calculates the approximate duration of a lunar year based on the given number of days.
207        It converts the given number of days into nanoseconds for use in high-precision timing applications.
208
209        Parameters:
210        days: The number of days in a lunar year. Defaults to 355,
211              which is an approximation of the average length of a lunar year.
212
213        Returns:
214        The approximate duration of a lunar year in nanoseconds.
215        """
216        return int(60 * 60 * 24 * days * 1e9)  # Lunar Year in nanoseconds

Calculates the approximate duration of a lunar year in nanoseconds.

This function calculates the approximate duration of a lunar year based on the given number of days. It converts the given number of days into nanoseconds for use in high-precision timing applications.

Parameters: days: The number of days in a lunar year. Defaults to 355, which is an approximation of the average length of a lunar year.

Returns: The approximate duration of a lunar year in nanoseconds.

@staticmethod
def Nisab(gram_price: float, gram_quantity: float = 595) -> float:
218    @staticmethod
219    def Nisab(gram_price: float, gram_quantity: float = 595) -> float:
220        """
221        Calculate the total value of Nisab (a unit of weight in Islamic jurisprudence) based on the given price per gram.
222
223        This function calculates the Nisab value, which is the minimum threshold of wealth,
224        that makes an individual liable for paying Zakat.
225        The Nisab value is determined by the equivalent value of a specific amount
226        of gold or silver (currently 595 grams in silver) in the local currency.
227
228        Parameters:
229        - gram_price (float): The price per gram of Nisab.
230        - gram_quantity (float): The quantity of grams in a Nisab. Default is 595 grams of silver.
231
232        Returns:
233        - float: The total value of Nisab based on the given price per gram.
234        """
235        return gram_price * gram_quantity

Calculate the total value of Nisab (a unit of weight in Islamic jurisprudence) based on the given price per gram.

This function calculates the Nisab value, which is the minimum threshold of wealth, that makes an individual liable for paying Zakat. The Nisab value is determined by the equivalent value of a specific amount of gold or silver (currently 595 grams in silver) in the local currency.

Parameters:

  • gram_price (float): The price per gram of Nisab.
  • gram_quantity (float): The quantity of grams in a Nisab. Default is 595 grams of silver.

Returns:

  • float: The total value of Nisab based on the given price per gram.
def path(self, path: str = None) -> str:
255    def path(self, path: str = None) -> str:
256        """
257        Set or get the database path.
258
259        Parameters:
260        path (str): The path to the database file. If not provided, it returns the current path.
261
262        Returns:
263        str: The current database path.
264        """
265        if path is not None:
266            self._vault_path = path
267        return self._vault_path

Set or get the database path.

Parameters: path (str): The path to the database file. If not provided, it returns the current path.

Returns: str: The current database path.

def reset(self) -> None:
283    def reset(self) -> None:
284        """
285        Reset the internal data structure to its initial state.
286
287        Parameters:
288        None
289
290        Returns:
291        None
292        """
293        self._vault = {
294            'account': {},
295            'exchange': {},
296            'history': {},
297            'lock': None,
298            'report': {},
299        }

Reset the internal data structure to its initial state.

Parameters: None

Returns: None

@staticmethod
def time( now: <module 'datetime' from '/opt/hostedtoolcache/Python/3.12.10/x64/lib/python3.12/datetime.py'> = None) -> int:
301    @staticmethod
302    def time(now: datetime = None) -> int:
303        """
304        Generates a timestamp based on the provided datetime object or the current datetime.
305
306        Parameters:
307        now (datetime, optional): The datetime object to generate the timestamp from.
308        If not provided, the current datetime is used.
309
310        Returns:
311        int: The timestamp in positive nanoseconds since the Unix epoch (January 1, 1970),
312            before 1970 will return in negative until 1000AD.
313        """
314        if now is None:
315            now = datetime.datetime.now()
316        ordinal_day = now.toordinal()
317        ns_in_day = (now - now.replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() * 10 ** 9
318        return int((ordinal_day - 719_163) * 86_400_000_000_000 + ns_in_day)

Generates a timestamp based on the provided datetime object or the current datetime.

Parameters: now (datetime, optional): The datetime object to generate the timestamp from. If not provided, the current datetime is used.

Returns: int: The timestamp in positive nanoseconds since the Unix epoch (January 1, 1970), before 1970 will return in negative until 1000AD.

@staticmethod
def time_to_datetime( ordinal_ns: int) -> <module 'datetime' from '/opt/hostedtoolcache/Python/3.12.10/x64/lib/python3.12/datetime.py'>:
320    @staticmethod
321    def time_to_datetime(ordinal_ns: int) -> datetime:
322        """
323        Converts an ordinal number (number of days since 1000-01-01) to a datetime object.
324
325        Parameters:
326        ordinal_ns (int): The ordinal number of days since 1000-01-01.
327
328        Returns:
329        datetime: The corresponding datetime object.
330        """
331        ordinal_day = ordinal_ns // 86_400_000_000_000 + 719_163
332        ns_in_day = ordinal_ns % 86_400_000_000_000
333        d = datetime.datetime.fromordinal(ordinal_day)
334        t = datetime.timedelta(seconds=ns_in_day // 10 ** 9)
335        return datetime.datetime.combine(d, datetime.time()) + t

Converts an ordinal number (number of days since 1000-01-01) to a datetime object.

Parameters: ordinal_ns (int): The ordinal number of days since 1000-01-01.

Returns: datetime: The corresponding datetime object.

def nolock(self) -> bool:
373    def nolock(self) -> bool:
374        """
375        Check if the vault lock is currently not set.
376
377        :return: True if the vault lock is not set, False otherwise.
378        """
379        return self._vault['lock'] is None

Check if the vault lock is currently not set.

Returns

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

def lock(self) -> int:
381    def lock(self) -> int:
382        """
383        Acquires a lock on the ZakatTracker instance.
384
385        Returns:
386        int: The lock ID. This ID can be used to release the lock later.
387        """
388        return self._step()

Acquires a lock on the ZakatTracker instance.

Returns: int: The lock ID. This ID can be used to release the lock later.

def box(self) -> dict:
390    def box(self) -> dict:
391        """
392        Returns a copy of the internal vault dictionary.
393
394        This method is used to retrieve the current state of the ZakatTracker object.
395        It provides a snapshot of the internal data structure, allowing for further
396        processing or analysis.
397
398        :return: A copy of the internal vault dictionary.
399        """
400        return self._vault.copy()

Returns a copy of the internal vault dictionary.

This method is used to retrieve the current state of the ZakatTracker object. It provides a snapshot of the internal data structure, allowing for further processing or analysis.

Returns

A copy of the internal vault dictionary.

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

Returns a copy of the history of steps taken in the ZakatTracker.

The history is a dictionary where each key is a unique identifier for a step, and the corresponding value is a dictionary containing information about the step.

Returns

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

def free(self, lock: int, auto_save: bool = True) -> bool:
413    def free(self, lock: int, auto_save: bool = True) -> bool:
414        """
415        Releases the lock on the database.
416
417        Parameters:
418        lock (int): The lock ID to be released.
419        auto_save (bool): Whether to automatically save the database after releasing the lock.
420
421        Returns:
422        bool: True if the lock is successfully released and (optionally) saved, False otherwise.
423        """
424        if lock == self._vault['lock']:
425            self._vault['lock'] = None
426            if auto_save:
427                return self.save(self.path())
428            return True
429        return False

Releases the lock on the database.

Parameters: lock (int): The lock ID to be released. auto_save (bool): Whether to automatically save the database after releasing the lock.

Returns: bool: True if the lock is successfully released and (optionally) saved, False otherwise.

def account_exists(self, account) -> bool:
431    def account_exists(self, account) -> bool:
432        """
433        Check if the given account exists in the vault.
434
435        Parameters:
436        account (str): The account number to check.
437
438        Returns:
439        bool: True if the account exists, False otherwise.
440        """
441        return account in self._vault['account']

Check if the given account exists in the vault.

Parameters: account (str): The account number to check.

Returns: bool: True if the account exists, False otherwise.

def box_size(self, account) -> int:
443    def box_size(self, account) -> int:
444        """
445        Calculate the size of the box for a specific account.
446
447        Parameters:
448        account (str): The account number for which the box size needs to be calculated.
449
450        Returns:
451        int: The size of the box for the given account. If the account does not exist, -1 is returned.
452        """
453        if self.account_exists(account):
454            return len(self._vault['account'][account]['box'])
455        return -1

Calculate the size of the box for a specific account.

Parameters: account (str): The account number for which the box size needs to be calculated.

Returns: int: The size of the box for the given account. If the account does not exist, -1 is returned.

def log_size(self, account) -> int:
457    def log_size(self, account) -> int:
458        """
459        Get the size of the log for a specific account.
460
461        Parameters:
462        account (str): The account number for which the log size needs to be calculated.
463
464        Returns:
465        int: The size of the log for the given account. If the account does not exist, -1 is returned.
466        """
467        if self.account_exists(account):
468            return len(self._vault['account'][account]['log'])
469        return -1

Get the size of the log for a specific account.

Parameters: account (str): The account number for which the log size needs to be calculated.

Returns: int: The size of the log for the given account. If the account does not exist, -1 is returned.

def recall(self, dry=True, debug=False) -> bool:
471    def recall(self, dry=True, debug=False) -> bool:
472        """
473        Revert the last operation.
474
475        Parameters:
476        dry (bool): If True, the function will not modify the data, but will simulate the operation. Default is True.
477        debug (bool): If True, the function will print debug information. Default is False.
478
479        Returns:
480        bool: True if the operation was successful, False otherwise.
481        """
482        if not self.nolock() or len(self._vault['history']) == 0:
483            return False
484        if len(self._vault['history']) <= 0:
485            return False
486        ref = sorted(self._vault['history'].keys())[-1]
487        if debug:
488            print('recall', ref)
489        memory = self._vault['history'][ref]
490        if debug:
491            print(type(memory), 'memory', memory)
492
493        limit = len(memory) + 1
494        sub_positive_log_negative = 0
495        for i in range(-1, -limit, -1):
496            x = memory[i]
497            if debug:
498                print(type(x), x)
499            match x['action']:
500                case Action.CREATE:
501                    if x['account'] is not None:
502                        if self.account_exists(x['account']):
503                            if debug:
504                                print('account', self._vault['account'][x['account']])
505                            assert len(self._vault['account'][x['account']]['box']) == 0
506                            assert self._vault['account'][x['account']]['balance'] == 0
507                            assert self._vault['account'][x['account']]['count'] == 0
508                            if dry:
509                                continue
510                            del self._vault['account'][x['account']]
511
512                case Action.TRACK:
513                    if x['account'] is not None:
514                        if self.account_exists(x['account']):
515                            if dry:
516                                continue
517                            self._vault['account'][x['account']]['balance'] -= x['value']
518                            self._vault['account'][x['account']]['count'] -= 1
519                            del self._vault['account'][x['account']]['box'][x['ref']]
520
521                case Action.LOG:
522                    if x['account'] is not None:
523                        if self.account_exists(x['account']):
524                            if x['ref'] in self._vault['account'][x['account']]['log']:
525                                if dry:
526                                    continue
527                                if sub_positive_log_negative == -x['value']:
528                                    self._vault['account'][x['account']]['count'] -= 1
529                                    sub_positive_log_negative = 0
530                                del self._vault['account'][x['account']]['log'][x['ref']]
531
532                case Action.SUB:
533                    if x['account'] is not None:
534                        if self.account_exists(x['account']):
535                            if x['ref'] in self._vault['account'][x['account']]['box']:
536                                if dry:
537                                    continue
538                                self._vault['account'][x['account']]['box'][x['ref']]['rest'] += x['value']
539                                self._vault['account'][x['account']]['balance'] += x['value']
540                                sub_positive_log_negative = x['value']
541
542                case Action.ADD_FILE:
543                    if x['account'] is not None:
544                        if self.account_exists(x['account']):
545                            if x['ref'] in self._vault['account'][x['account']]['log']:
546                                if x['file'] in self._vault['account'][x['account']]['log'][x['ref']]['file']:
547                                    if dry:
548                                        continue
549                                    del self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']]
550
551                case Action.REMOVE_FILE:
552                    if x['account'] is not None:
553                        if self.account_exists(x['account']):
554                            if x['ref'] in self._vault['account'][x['account']]['log']:
555                                if dry:
556                                    continue
557                                self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']] = x['value']
558
559                case Action.BOX_TRANSFER:
560                    if x['account'] is not None:
561                        if self.account_exists(x['account']):
562                            if x['ref'] in self._vault['account'][x['account']]['box']:
563                                if dry:
564                                    continue
565                                self._vault['account'][x['account']]['box'][x['ref']]['rest'] -= x['value']
566
567                case Action.EXCHANGE:
568                    if x['account'] is not None:
569                        if x['account'] in self._vault['exchange']:
570                            if x['ref'] in self._vault['exchange'][x['account']]:
571                                if dry:
572                                    continue
573                                del self._vault['exchange'][x['account']][x['ref']]
574
575                case Action.REPORT:
576                    if x['ref'] in self._vault['report']:
577                        if dry:
578                            continue
579                        del self._vault['report'][x['ref']]
580
581                case Action.ZAKAT:
582                    if x['account'] is not None:
583                        if self.account_exists(x['account']):
584                            if x['ref'] in self._vault['account'][x['account']]['box']:
585                                if x['key'] in self._vault['account'][x['account']]['box'][x['ref']]:
586                                    if dry:
587                                        continue
588                                    match x['math']:
589                                        case MathOperation.ADDITION:
590                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] -= x[
591                                                'value']
592                                        case MathOperation.EQUAL:
593                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] = x['value']
594                                        case MathOperation.SUBTRACTION:
595                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] += x[
596                                                'value']
597
598        if not dry:
599            del self._vault['history'][ref]
600        return True

Revert the last operation.

Parameters: dry (bool): If True, the function will not modify the data, but will simulate the operation. Default is True. debug (bool): If True, the function will print debug information. Default is False.

Returns: bool: True if the operation was successful, False otherwise.

def ref_exists(self, account: str, ref_type: str, ref: int) -> bool:
602    def ref_exists(self, account: str, ref_type: str, ref: int) -> bool:
603        """
604        Check if a specific reference (transaction) exists in the vault for a given account and reference type.
605
606        Parameters:
607        account (str): The account number for which to check the existence of the reference.
608        ref_type (str): The type of reference (e.g., 'box', 'log', etc.).
609        ref (int): The reference (transaction) number to check for existence.
610
611        Returns:
612        bool: True if the reference exists for the given account and reference type, False otherwise.
613        """
614        if account in self._vault['account']:
615            return ref in self._vault['account'][account][ref_type]
616        return False

Check if a specific reference (transaction) exists in the vault for a given account and reference type.

Parameters: account (str): The account number for which to check the existence of the reference. ref_type (str): The type of reference (e.g., 'box', 'log', etc.). ref (int): The reference (transaction) number to check for existence.

Returns: bool: True if the reference exists for the given account and reference type, False otherwise.

def box_exists(self, account: str, ref: int) -> bool:
618    def box_exists(self, account: str, ref: int) -> bool:
619        """
620        Check if a specific box (transaction) exists in the vault for a given account and reference.
621
622        Parameters:
623        - account (str): The account number for which to check the existence of the box.
624        - ref (int): The reference (transaction) number to check for existence.
625
626        Returns:
627        - bool: True if the box exists for the given account and reference, False otherwise.
628        """
629        return self.ref_exists(account, 'box', ref)

Check if a specific box (transaction) exists in the vault for a given account and reference.

Parameters:

  • account (str): The account number for which to check the existence of the box.
  • ref (int): The reference (transaction) number to check for existence.

Returns:

  • bool: True if the box exists for the given account and reference, False otherwise.
def track( self, value: float = 0, desc: str = '', account: str = 1, logging: bool = True, created: int = None, debug: bool = False) -> int:
631    def track(self, value: float = 0, desc: str = '', account: str = 1, logging: bool = True, created: int = None,
632              debug: bool = False) -> int:
633        """
634        This function tracks a transaction for a specific account.
635
636        Parameters:
637        value (float): The value of the transaction. Default is 0.
638        desc (str): The description of the transaction. Default is an empty string.
639        account (str): The account for which the transaction is being tracked. Default is '1'.
640        logging (bool): Whether to log the transaction. Default is True.
641        created (int): The timestamp of the transaction. If not provided, it will be generated. Default is None.
642        debug (bool): Whether to print debug information. Default is False.
643
644        Returns:
645        int: The timestamp of the transaction.
646
647        This function creates a new account if it doesn't exist, logs the transaction if logging is True, and updates the account's balance and box.
648
649        Raises:
650        ValueError: The log transaction happened again in the same nanosecond time.
651        ValueError: The box transaction happened again in the same nanosecond time.
652        """
653        if created is None:
654            created = self.time()
655        no_lock = self.nolock()
656        self.lock()
657        if not self.account_exists(account):
658            if debug:
659                print(f"account {account} created")
660            self._vault['account'][account] = {
661                'balance': 0,
662                'box': {},
663                'count': 0,
664                'log': {},
665                'hide': False,
666                'zakatable': True,
667            }
668            self._step(Action.CREATE, account)
669        if value == 0:
670            if no_lock:
671                self.free(self.lock())
672            return 0
673        if logging:
674            self._log(value, desc, account, created, debug)
675        if debug:
676            print('create-box', created)
677        if self.box_exists(account, created):
678            raise ValueError(f"The box transaction happened again in the same nanosecond time({created}).")
679        if debug:
680            print('created-box', created)
681        self._vault['account'][account]['box'][created] = {
682            'capital': value,
683            'count': 0,
684            'last': 0,
685            'rest': value,
686            'total': 0,
687        }
688        self._step(Action.TRACK, account, ref=created, value=value)
689        if no_lock:
690            self.free(self.lock())
691        return created

This function tracks a transaction for a specific account.

Parameters: value (float): The value of the transaction. Default is 0. desc (str): The description of the transaction. Default is an empty string. account (str): The account for which the transaction is being tracked. Default is '1'. logging (bool): Whether to log the transaction. Default is True. created (int): The timestamp of the transaction. If not provided, it will be generated. Default is None. debug (bool): Whether to print debug information. Default is False.

Returns: int: The timestamp of the transaction.

This function creates a new account if it doesn't exist, logs the transaction if logging is True, and updates the account's balance and box.

Raises: ValueError: The log transaction happened again in the same nanosecond time. ValueError: The box transaction happened again in the same nanosecond time.

def log_exists(self, account: str, ref: int) -> bool:
693    def log_exists(self, account: str, ref: int) -> bool:
694        """
695        Checks if a specific transaction log entry exists for a given account.
696
697        Parameters:
698        account (str): The account number associated with the transaction log.
699        ref (int): The reference to the transaction log entry.
700
701        Returns:
702        bool: True if the transaction log entry exists, False otherwise.
703        """
704        return self.ref_exists(account, 'log', ref)

Checks if a specific transaction log entry exists for a given account.

Parameters: account (str): The account number associated with the transaction log. ref (int): The reference to the transaction log entry.

Returns: bool: True if the transaction log entry exists, False otherwise.

def exchange( self, account, created: int = None, rate: float = None, description: str = None, debug: bool = False) -> dict:
743    def exchange(self, account, created: int = None, rate: float = None, description: str = None,
744                 debug: bool = False) -> dict:
745        """
746        This method is used to record or retrieve exchange rates for a specific account.
747
748        Parameters:
749        - account (str): The account number for which the exchange rate is being recorded or retrieved.
750        - created (int): The timestamp of the exchange rate. If not provided, the current timestamp will be used.
751        - rate (float): The exchange rate to be recorded. If not provided, the method will retrieve the latest exchange rate.
752        - description (str): A description of the exchange rate.
753
754        Returns:
755        - dict: A dictionary containing the latest exchange rate and its description. If no exchange rate is found,
756        it returns a dictionary with default values for the rate and description.
757        """
758        if created is None:
759            created = self.time()
760        no_lock = self.nolock()
761        self.lock()
762        if rate is not None:
763            if rate <= 0:
764                return dict()
765            if account not in self._vault['exchange']:
766                self._vault['exchange'][account] = {}
767            if len(self._vault['exchange'][account]) == 0 and rate <= 1:
768                return {"rate": 1, "description": None}
769            self._vault['exchange'][account][created] = {"rate": rate, "description": description}
770            self._step(Action.EXCHANGE, account, ref=created, value=rate)
771            if no_lock:
772                self.free(self.lock())
773            if debug:
774                print("exchange-created-1",
775                      f'account: {account}, created: {created}, rate:{rate}, description:{description}')
776
777        if account in self._vault['exchange']:
778            valid_rates = [(ts, r) for ts, r in self._vault['exchange'][account].items() if ts <= created]
779            if valid_rates:
780                latest_rate = max(valid_rates, key=lambda x: x[0])
781                if debug:
782                    print("exchange-read-1",
783                          f'account: {account}, created: {created}, rate:{rate}, description:{description}',
784                          'latest_rate', latest_rate)
785                return latest_rate[1]  # إرجاع قاموس يحتوي على المعدل والوصف
786        if debug:
787            print("exchange-read-0", f'account: {account}, created: {created}, rate:{rate}, description:{description}')
788        return {"rate": 1, "description": None}  # إرجاع القيمة الافتراضية مع وصف فارغ

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

Parameters:

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

Returns:

  • dict: A dictionary containing the latest exchange rate and its description. If no exchange rate is found, it returns a dictionary with default values for the rate and description.
@staticmethod
def exchange_calc(x: float, x_rate: float, y_rate: float) -> float:
790    @staticmethod
791    def exchange_calc(x: float, x_rate: float, y_rate: float) -> float:
792        """
793        This function calculates the exchanged amount of a currency.
794
795        Args:
796            x (float): The original amount of the currency.
797            x_rate (float): The exchange rate of the original currency.
798            y_rate (float): The exchange rate of the target currency.
799
800        Returns:
801            float: The exchanged amount of the target currency.
802        """
803        return (x * x_rate) / y_rate

This function calculates the exchanged amount of a currency.

Args: x (float): The original amount of the currency. x_rate (float): The exchange rate of the original currency. y_rate (float): The exchange rate of the target currency.

Returns: float: The exchanged amount of the target currency.

def exchanges(self) -> dict:
805    def exchanges(self) -> dict:
806        """
807        Retrieve the recorded exchange rates for all accounts.
808
809        Parameters:
810        None
811
812        Returns:
813        dict: A dictionary containing all recorded exchange rates.
814        The keys are account names or numbers, and the values are dictionaries containing the exchange rates.
815        Each exchange rate dictionary has timestamps as keys and exchange rate details as values.
816        """
817        return self._vault['exchange'].copy()

Retrieve the recorded exchange rates for all accounts.

Parameters: None

Returns: dict: A dictionary containing all recorded exchange rates. The keys are account names or numbers, and the values are dictionaries containing the exchange rates. Each exchange rate dictionary has timestamps as keys and exchange rate details as values.

def accounts(self) -> dict:
819    def accounts(self) -> dict:
820        """
821        Returns a dictionary containing account numbers as keys and their respective balances as values.
822
823        Parameters:
824        None
825
826        Returns:
827        dict: A dictionary where keys are account numbers and values are their respective balances.
828        """
829        result = {}
830        for i in self._vault['account']:
831            result[i] = self._vault['account'][i]['balance']
832        return result

Returns a dictionary containing account numbers as keys and their respective balances as values.

Parameters: None

Returns: dict: A dictionary where keys are account numbers and values are their respective balances.

def boxes(self, account) -> dict:
834    def boxes(self, account) -> dict:
835        """
836        Retrieve the boxes (transactions) associated with a specific account.
837
838        Parameters:
839        account (str): The account number for which to retrieve the boxes.
840
841        Returns:
842        dict: A dictionary containing the boxes associated with the given account.
843        If the account does not exist, an empty dictionary is returned.
844        """
845        if self.account_exists(account):
846            return self._vault['account'][account]['box']
847        return {}

Retrieve the boxes (transactions) associated with a specific account.

Parameters: account (str): The account number for which to retrieve the boxes.

Returns: dict: A dictionary containing the boxes associated with the given account. If the account does not exist, an empty dictionary is returned.

def logs(self, account) -> dict:
849    def logs(self, account) -> dict:
850        """
851        Retrieve the logs (transactions) associated with a specific account.
852
853        Parameters:
854        account (str): The account number for which to retrieve the logs.
855
856        Returns:
857        dict: A dictionary containing the logs associated with the given account.
858        If the account does not exist, an empty dictionary is returned.
859        """
860        if self.account_exists(account):
861            return self._vault['account'][account]['log']
862        return {}

Retrieve the logs (transactions) associated with a specific account.

Parameters: account (str): The account number for which to retrieve the logs.

Returns: dict: A dictionary containing the logs associated with the given account. If the account does not exist, an empty dictionary is returned.

def add_file(self, account: str, ref: int, path: str) -> int:
864    def add_file(self, account: str, ref: int, path: str) -> int:
865        """
866        Adds a file reference to a specific transaction log entry in the vault.
867
868        Parameters:
869        account (str): The account number associated with the transaction log.
870        ref (int): The reference to the transaction log entry.
871        path (str): The path of the file to be added.
872
873        Returns:
874        int: The reference of the added file. If the account or transaction log entry does not exist, returns 0.
875        """
876        if self.account_exists(account):
877            if ref in self._vault['account'][account]['log']:
878                file_ref = self.time()
879                self._vault['account'][account]['log'][ref]['file'][file_ref] = path
880                no_lock = self.nolock()
881                self.lock()
882                self._step(Action.ADD_FILE, account, ref=ref, file=file_ref)
883                if no_lock:
884                    self.free(self.lock())
885                return file_ref
886        return 0

Adds a file reference to a specific transaction log entry in the vault.

Parameters: account (str): The account number associated with the transaction log. ref (int): The reference to the transaction log entry. path (str): The path of the file to be added.

Returns: int: The reference of the added file. If the account or transaction log entry does not exist, returns 0.

def remove_file(self, account: str, ref: int, file_ref: int) -> bool:
888    def remove_file(self, account: str, ref: int, file_ref: int) -> bool:
889        """
890        Removes a file reference from a specific transaction log entry in the vault.
891
892        Parameters:
893        account (str): The account number associated with the transaction log.
894        ref (int): The reference to the transaction log entry.
895        file_ref (int): The reference of the file to be removed.
896
897        Returns:
898        bool: True if the file reference is successfully removed, False otherwise.
899        """
900        if self.account_exists(account):
901            if ref in self._vault['account'][account]['log']:
902                if file_ref in self._vault['account'][account]['log'][ref]['file']:
903                    x = self._vault['account'][account]['log'][ref]['file'][file_ref]
904                    del self._vault['account'][account]['log'][ref]['file'][file_ref]
905                    no_lock = self.nolock()
906                    self.lock()
907                    self._step(Action.REMOVE_FILE, account, ref=ref, file=file_ref, value=x)
908                    if no_lock:
909                        self.free(self.lock())
910                    return True
911        return False

Removes a file reference from a specific transaction log entry in the vault.

Parameters: account (str): The account number associated with the transaction log. ref (int): The reference to the transaction log entry. file_ref (int): The reference of the file to be removed.

Returns: bool: True if the file reference is successfully removed, False otherwise.

def balance(self, account: str = 1, cached: bool = True) -> int:
913    def balance(self, account: str = 1, cached: bool = True) -> int:
914        """
915        Calculate and return the balance of a specific account.
916
917        Parameters:
918        account (str): The account number. Default is '1'.
919        cached (bool): If True, use the cached balance. If False, calculate the balance from the box. Default is True.
920
921        Returns:
922        int: The balance of the account.
923
924        Note:
925        If cached is True, the function returns the cached balance.
926        If cached is False, the function calculates the balance from the box by summing up the 'rest' values of all box items.
927        """
928        if cached:
929            return self._vault['account'][account]['balance']
930        x = 0
931        return [x := x + y['rest'] for y in self._vault['account'][account]['box'].values()][-1]

Calculate and return the balance of a specific account.

Parameters: account (str): The account number. Default is '1'. cached (bool): If True, use the cached balance. If False, calculate the balance from the box. Default is True.

Returns: int: The balance of the account.

Note: If cached is True, the function returns the cached balance. If cached is False, the function calculates the balance from the box by summing up the 'rest' values of all box items.

def hide(self, account, status: bool = None) -> bool:
933    def hide(self, account, status: bool = None) -> bool:
934        """
935        Check or set the hide status of a specific account.
936
937        Parameters:
938        account (str): The account number.
939        status (bool, optional): The new hide status. If not provided, the function will return the current status.
940
941        Returns:
942        bool: The current or updated hide status of the account.
943
944        Raises:
945        None
946
947        Example:
948        >>> tracker = ZakatTracker()
949        >>> ref = tracker.track(51, 'desc', 'account1')
950        >>> tracker.hide('account1')  # Set the hide status of 'account1' to True
951        False
952        >>> tracker.hide('account1', True)  # Set the hide status of 'account1' to True
953        True
954        >>> tracker.hide('account1')  # Get the hide status of 'account1' by default
955        True
956        >>> tracker.hide('account1', False)
957        False
958        """
959        if self.account_exists(account):
960            if status is None:
961                return self._vault['account'][account]['hide']
962            self._vault['account'][account]['hide'] = status
963            return status
964        return False

Check or set the hide status of a specific account.

Parameters: account (str): The account number. status (bool, optional): The new hide status. If not provided, the function will return the current status.

Returns: bool: The current or updated hide status of the account.

Raises: None

Example:

>>> tracker = ZakatTracker()
>>> ref = tracker.track(51, 'desc', 'account1')
>>> tracker.hide('account1')  # Set the hide status of 'account1' to True
False
>>> tracker.hide('account1', True)  # Set the hide status of 'account1' to True
True
>>> tracker.hide('account1')  # Get the hide status of 'account1' by default
True
>>> tracker.hide('account1', False)
False
def zakatable(self, account, status: bool = None) -> bool:
966    def zakatable(self, account, status: bool = None) -> bool:
967        """
968        Check or set the zakatable status of a specific account.
969
970        Parameters:
971        account (str): The account number.
972        status (bool, optional): The new zakatable status. If not provided, the function will return the current status.
973
974        Returns:
975        bool: The current or updated zakatable status of the account.
976
977        Raises:
978        None
979
980        Example:
981        >>> tracker = ZakatTracker()
982        >>> ref = tracker.track(51, 'desc', 'account1')
983        >>> tracker.zakatable('account1')  # Set the zakatable status of 'account1' to True
984        True
985        >>> tracker.zakatable('account1', True)  # Set the zakatable status of 'account1' to True
986        True
987        >>> tracker.zakatable('account1')  # Get the zakatable status of 'account1' by default
988        True
989        >>> tracker.zakatable('account1', False)
990        False
991        """
992        if self.account_exists(account):
993            if status is None:
994                return self._vault['account'][account]['zakatable']
995            self._vault['account'][account]['zakatable'] = status
996            return status
997        return False

Check or set the zakatable status of a specific account.

Parameters: account (str): The account number. status (bool, optional): The new zakatable status. If not provided, the function will return the current status.

Returns: bool: The current or updated zakatable status of the account.

Raises: None

Example:

>>> tracker = ZakatTracker()
>>> ref = tracker.track(51, 'desc', 'account1')
>>> tracker.zakatable('account1')  # Set the zakatable status of 'account1' to True
True
>>> tracker.zakatable('account1', True)  # Set the zakatable status of 'account1' to True
True
>>> tracker.zakatable('account1')  # Get the zakatable status of 'account1' by default
True
>>> tracker.zakatable('account1', False)
False
def sub( self, x: float, desc: str = '', account: str = 1, created: int = None, debug: bool = False) -> tuple:
 999    def sub(self, x: float, desc: str = '', account: str = 1, created: int = None, debug: bool = False) -> tuple:
1000        """
1001        Subtracts a specified value from an account's balance.
1002
1003        Parameters:
1004        x (float): The amount to be subtracted.
1005        desc (str): A description for the transaction. Defaults to an empty string.
1006        account (str): The account from which the value will be subtracted. Defaults to '1'.
1007        created (int): The timestamp of the transaction. If not provided, the current timestamp will be used.
1008        debug (bool): A flag indicating whether to print debug information. Defaults to False.
1009
1010        Returns:
1011        tuple: A tuple containing the timestamp of the transaction and a list of tuples representing the age of each transaction.
1012
1013        If the amount to subtract is greater than the account's balance,
1014        the remaining amount will be transferred to a new transaction with a negative value.
1015
1016        Raises:
1017        ValueError: The box transaction happened again in the same nanosecond time.
1018        ValueError: The log transaction happened again in the same nanosecond time.
1019        """
1020        if x < 0:
1021            return tuple()
1022        if x == 0:
1023            ref = self.track(x, '', account)
1024            return ref, ref
1025        if created is None:
1026            created = self.time()
1027        no_lock = self.nolock()
1028        self.lock()
1029        self.track(0, '', account)
1030        self._log(-x, desc, account, created)
1031        ids = sorted(self._vault['account'][account]['box'].keys())
1032        limit = len(ids) + 1
1033        target = x
1034        if debug:
1035            print('ids', ids)
1036        ages = []
1037        for i in range(-1, -limit, -1):
1038            if target == 0:
1039                break
1040            j = ids[i]
1041            if debug:
1042                print('i', i, 'j', j)
1043            rest = self._vault['account'][account]['box'][j]['rest']
1044            if rest >= target:
1045                self._vault['account'][account]['box'][j]['rest'] -= target
1046                self._step(Action.SUB, account, ref=j, value=target)
1047                ages.append((j, target))
1048                target = 0
1049                break
1050            elif target > rest > 0:
1051                chunk = rest
1052                target -= chunk
1053                self._step(Action.SUB, account, ref=j, value=chunk)
1054                ages.append((j, chunk))
1055                self._vault['account'][account]['box'][j]['rest'] = 0
1056        if target > 0:
1057            self.track(-target, desc, account, False, created)
1058            ages.append((created, target))
1059        if no_lock:
1060            self.free(self.lock())
1061        return created, ages

Subtracts a specified value from an account's balance.

Parameters: x (float): The amount to be subtracted. desc (str): A description for the transaction. Defaults to an empty string. account (str): The account from which the value will be subtracted. Defaults to '1'. created (int): The timestamp of the transaction. If not provided, the current timestamp will be used. debug (bool): A flag indicating whether to print debug information. Defaults to False.

Returns: tuple: A tuple containing the timestamp of the transaction and a list of tuples representing the age of each transaction.

If the amount to subtract is greater than the account's balance, the remaining amount will be transferred to a new transaction with a negative value.

Raises: ValueError: The box transaction happened again in the same nanosecond time. ValueError: The log transaction happened again in the same nanosecond time.

def transfer( self, amount: int, from_account: str, to_account: str, desc: str = '', created: int = None, debug: bool = False) -> list[int]:
1063    def transfer(self, amount: int, from_account: str, to_account: str, desc: str = '', created: int = None,
1064                 debug: bool = False) -> list[int]:
1065        """
1066        Transfers a specified value from one account to another.
1067
1068        Parameters:
1069        amount (int): The amount to be transferred.
1070        from_account (str): The account from which the value will be transferred.
1071        to_account (str): The account to which the value will be transferred.
1072        desc (str, optional): A description for the transaction. Defaults to an empty string.
1073        created (int, optional): The timestamp of the transaction. If not provided, the current timestamp will be used.
1074        debug (bool): A flag indicating whether to print debug information. Defaults to False.
1075
1076        Returns:
1077        list[int]: A list of timestamps corresponding to the transactions made during the transfer.
1078
1079        Raises:
1080        ValueError: Transfer to the same account is forbidden.
1081        ValueError: The box transaction happened again in the same nanosecond time.
1082        ValueError: The log transaction happened again in the same nanosecond time.
1083        """
1084        if from_account == to_account:
1085            raise ValueError(f'Transfer to the same account is forbidden. {to_account}')
1086        if amount <= 0:
1087            return []
1088        if created is None:
1089            created = self.time()
1090        (_, ages) = self.sub(amount, desc, from_account, created, debug=debug)
1091        times = []
1092        source_exchange = self.exchange(from_account, created)
1093        target_exchange = self.exchange(to_account, created)
1094
1095        if debug:
1096            print('ages', ages)
1097
1098        for age, value in ages:
1099            target_amount = self.exchange_calc(value, source_exchange['rate'], target_exchange['rate'])
1100            # Perform the transfer
1101            if self.box_exists(to_account, age):
1102                if debug:
1103                    print('box_exists', age)
1104                capital = self._vault['account'][to_account]['box'][age]['capital']
1105                rest = self._vault['account'][to_account]['box'][age]['rest']
1106                if debug:
1107                    print(
1108                        f"Transfer {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).")
1109                selected_age = age
1110                if rest + target_amount > capital:
1111                    self._vault['account'][to_account]['box'][age]['capital'] += target_amount
1112                    selected_age = ZakatTracker.time()
1113                self._vault['account'][to_account]['box'][age]['rest'] += target_amount
1114                self._step(Action.BOX_TRANSFER, to_account, ref=selected_age, value=target_amount)
1115                y = self._log(target_amount, desc=f'TRANSFER {from_account} -> {to_account}', account=to_account,
1116                              debug=debug)
1117                times.append((age, y))
1118                continue
1119            y = self.track(target_amount, desc, to_account, logging=True, created=age, debug=debug)
1120            if debug:
1121                print(
1122                    f"Transferred {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).")
1123            times.append(y)
1124        return times

Transfers a specified value from one account to another.

Parameters: amount (int): The amount to be transferred. from_account (str): The account from which the value will be transferred. to_account (str): The account to which the value will be transferred. desc (str, optional): A description for the transaction. Defaults to an empty string. created (int, optional): The timestamp of the transaction. If not provided, the current timestamp will be used. debug (bool): A flag indicating whether to print debug information. Defaults to False.

Returns: list[int]: A list of timestamps corresponding to the transactions made during the transfer.

Raises: ValueError: Transfer to the same account is forbidden. ValueError: The box transaction happened again in the same nanosecond time. ValueError: The log transaction happened again in the same nanosecond time.

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