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