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