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

Returns the current version of the software.

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

Returns: str: The current version of the software.

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

Calculates the Zakat amount due on an asset.

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

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

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

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

Calculates the approximate duration of a lunar year in nanoseconds.

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

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

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

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

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

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

Parameters:

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

Returns:

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

Set or get the database path.

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

Returns: str: The current database path.

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

Reset the internal data structure to its initial state.

Parameters: None

Returns: None

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

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

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

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

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

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

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

Returns: datetime: The corresponding datetime object.

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

Check if the vault lock is currently not set.

Returns

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

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

Acquires a lock on the ZakatTracker instance.

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

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

Returns a copy of the internal vault dictionary.

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

Returns

A copy of the internal vault dictionary.

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

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

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

Returns

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

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

Releases the lock on the database.

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

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

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

Check if the given account exists in the vault.

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

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

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

Calculate the size of the box for a specific account.

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

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

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

Get the size of the log for a specific account.

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

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

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

Revert the last operation.

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

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

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

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

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

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

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

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

Parameters:

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

Returns:

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

This function tracks a transaction for a specific account.

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

Returns: int: The timestamp of the transaction.

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

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

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

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

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

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

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

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

Parameters:

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

Returns:

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

This function calculates the exchanged amount of a currency.

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

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

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

Retrieve the recorded exchange rates for all accounts.

Parameters: None

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

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

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

Parameters: None

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Calculate and return the balance of a specific account.

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

Returns: int: The balance of the account.

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

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

Check or set the hide status of a specific account.

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

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

Raises: None

Example:

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

Check or set the zakatable status of a specific account.

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

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

Raises: None

Example:

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

Subtracts a specified value from an account's balance.

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

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

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

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

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

Transfers a specified value from one account to another.

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

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

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

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