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