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.72' 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 vault(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 = float(_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 += float(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 if debug: 1745 random.seed(1234567890) 1746 1747 # sanity check - random forward time 1748 1749 xlist = [] 1750 limit = 1000 1751 for _ in range(limit): 1752 y = ZakatTracker.time() 1753 z = '-' 1754 if y not in xlist: 1755 xlist.append(y) 1756 else: 1757 z = 'x' 1758 if debug: 1759 print(z, y) 1760 xx = len(xlist) 1761 if debug: 1762 print('count', xx, ' - unique: ', (xx / limit) * 100, '%') 1763 assert limit == xx 1764 1765 # sanity check - convert date since 1000AD 1766 1767 for year in range(1000, 9000): 1768 ns = ZakatTracker.time(datetime.datetime.strptime(f"{year}-12-30 18:30:45", "%Y-%m-%d %H:%M:%S")) 1769 date = ZakatTracker.time_to_datetime(ns) 1770 if debug: 1771 print(date) 1772 assert date.year == year 1773 assert date.month == 12 1774 assert date.day == 30 1775 assert date.hour == 18 1776 assert date.minute == 30 1777 assert date.second in [44, 45] 1778 assert self.nolock() 1779 1780 assert self._history() is True 1781 1782 table = { 1783 1: [ 1784 (0, 10, 10, 10, 10, 1, 1), 1785 (0, 20, 30, 30, 30, 2, 2), 1786 (0, 30, 60, 60, 60, 3, 3), 1787 (1, 15, 45, 45, 45, 3, 4), 1788 (1, 50, -5, -5, -5, 4, 5), 1789 (1, 100, -105, -105, -105, 5, 6), 1790 ], 1791 'wallet': [ 1792 (1, 90, -90, -90, -90, 1, 1), 1793 (0, 100, 10, 10, 10, 2, 2), 1794 (1, 190, -180, -180, -180, 3, 3), 1795 (0, 1000, 820, 820, 820, 4, 4), 1796 ], 1797 } 1798 for x in table: 1799 for y in table[x]: 1800 self.lock() 1801 if y[0] == 0: 1802 ref = self.track(y[1], 'test-add', x, True, ZakatTracker.time(), debug) 1803 else: 1804 (ref, z) = self.sub(y[1], 'test-sub', x, ZakatTracker.time()) 1805 if debug: 1806 print('_sub', z, ZakatTracker.time()) 1807 assert ref != 0 1808 assert len(self._vault['account'][x]['log'][ref]['file']) == 0 1809 for i in range(3): 1810 file_ref = self.add_file(x, ref, 'file_' + str(i)) 1811 sleep(0.0000001) 1812 assert file_ref != 0 1813 if debug: 1814 print('ref', ref, 'file', file_ref) 1815 assert len(self._vault['account'][x]['log'][ref]['file']) == i + 1 1816 file_ref = self.add_file(x, ref, 'file_' + str(3)) 1817 assert self.remove_file(x, ref, file_ref) 1818 assert self.balance(x) == y[2] 1819 z = self.balance(x, False) 1820 if debug: 1821 print("debug-1", z, y[3]) 1822 assert z == y[3] 1823 o = self._vault['account'][x]['log'] 1824 z = 0 1825 for i in o: 1826 z += o[i]['value'] 1827 if debug: 1828 print("debug-2", z, type(z)) 1829 print("debug-2", y[4], type(y[4])) 1830 assert z == y[4] 1831 if debug: 1832 print('debug-2 - PASSED') 1833 assert self.box_size(x) == y[5] 1834 assert self.log_size(x) == y[6] 1835 assert not self.nolock() 1836 self.free(self.lock()) 1837 assert self.nolock() 1838 assert self.boxes(x) != {} 1839 assert self.logs(x) != {} 1840 1841 assert not self.hide(x) 1842 assert self.hide(x, False) is False 1843 assert self.hide(x) is False 1844 assert self.hide(x, True) 1845 assert self.hide(x) 1846 1847 assert self.zakatable(x) 1848 assert self.zakatable(x, False) is False 1849 assert self.zakatable(x) is False 1850 assert self.zakatable(x, True) 1851 assert self.zakatable(x) 1852 1853 if restore is True: 1854 count = len(self._vault['history']) 1855 if debug: 1856 print('history-count', count) 1857 assert count == 10 1858 # try mode 1859 for _ in range(count): 1860 assert self.recall(True, debug) 1861 count = len(self._vault['history']) 1862 if debug: 1863 print('history-count', count) 1864 assert count == 10 1865 _accounts = list(table.keys()) 1866 accounts_limit = len(_accounts) + 1 1867 for i in range(-1, -accounts_limit, -1): 1868 account = _accounts[i] 1869 if debug: 1870 print(account, len(table[account])) 1871 transaction_limit = len(table[account]) + 1 1872 for j in range(-1, -transaction_limit, -1): 1873 row = table[account][j] 1874 if debug: 1875 print(row, self.balance(account), self.balance(account, False)) 1876 assert self.balance(account) == self.balance(account, False) 1877 assert self.balance(account) == row[2] 1878 assert self.recall(False, debug) 1879 assert self.recall(False, debug) is False 1880 count = len(self._vault['history']) 1881 if debug: 1882 print('history-count', count) 1883 assert count == 0 1884 self.reset() 1885 1886 def test(self, debug: bool = False) -> bool: 1887 if debug: 1888 print('test', f'debug={debug}') 1889 try: 1890 1891 assert self._history() 1892 1893 # Not allowed for duplicate transactions in the same account and time 1894 1895 created = ZakatTracker.time() 1896 self.track(100, 'test-1', 'same', True, created) 1897 failed = False 1898 try: 1899 self.track(50, 'test-1', 'same', True, created) 1900 except: 1901 failed = True 1902 assert failed is True 1903 1904 self.reset() 1905 1906 # Same account transfer 1907 for x in [1, 'a', True, 1.8, None]: 1908 failed = False 1909 try: 1910 self.transfer(1, x, x, 'same-account', debug=debug) 1911 except: 1912 failed = True 1913 assert failed is True 1914 1915 # Always preserve box age during transfer 1916 1917 series: list[tuple] = [ 1918 (30, 4), 1919 (60, 3), 1920 (90, 2), 1921 ] 1922 case = { 1923 30: { 1924 'series': series, 1925 'rest': 150, 1926 }, 1927 60: { 1928 'series': series, 1929 'rest': 120, 1930 }, 1931 90: { 1932 'series': series, 1933 'rest': 90, 1934 }, 1935 180: { 1936 'series': series, 1937 'rest': 0, 1938 }, 1939 270: { 1940 'series': series, 1941 'rest': -90, 1942 }, 1943 360: { 1944 'series': series, 1945 'rest': -180, 1946 }, 1947 } 1948 1949 selected_time = ZakatTracker.time() - ZakatTracker.TimeCycle() 1950 1951 for total in case: 1952 for x in case[total]['series']: 1953 self.track(x[0], f"test-{x} ages", 'ages', True, selected_time * x[1]) 1954 1955 refs = self.transfer(total, 'ages', 'future', 'Zakat Movement', debug=debug) 1956 1957 if debug: 1958 print('refs', refs) 1959 1960 ages_cache_balance = self.balance('ages') 1961 ages_fresh_balance = self.balance('ages', False) 1962 rest = case[total]['rest'] 1963 if debug: 1964 print('source', ages_cache_balance, ages_fresh_balance, rest) 1965 assert ages_cache_balance == rest 1966 assert ages_fresh_balance == rest 1967 1968 future_cache_balance = self.balance('future') 1969 future_fresh_balance = self.balance('future', False) 1970 if debug: 1971 print('target', future_cache_balance, future_fresh_balance, total) 1972 print('refs', refs) 1973 assert future_cache_balance == total 1974 assert future_fresh_balance == total 1975 1976 for ref in self._vault['account']['ages']['box']: 1977 ages_capital = self._vault['account']['ages']['box'][ref]['capital'] 1978 ages_rest = self._vault['account']['ages']['box'][ref]['rest'] 1979 future_capital = 0 1980 if ref in self._vault['account']['future']['box']: 1981 future_capital = self._vault['account']['future']['box'][ref]['capital'] 1982 future_rest = 0 1983 if ref in self._vault['account']['future']['box']: 1984 future_rest = self._vault['account']['future']['box'][ref]['rest'] 1985 if ages_capital != 0 and future_capital != 0 and future_rest != 0: 1986 if debug: 1987 print('================================================================') 1988 print('ages', ages_capital, ages_rest) 1989 print('future', future_capital, future_rest) 1990 if ages_rest == 0: 1991 assert ages_capital == future_capital 1992 elif ages_rest < 0: 1993 assert -ages_capital == future_capital 1994 elif ages_rest > 0: 1995 assert ages_capital == ages_rest + future_capital 1996 self.reset() 1997 assert len(self._vault['history']) == 0 1998 1999 assert self._history() 2000 assert self._history(False) is False 2001 assert self._history() is False 2002 assert self._history(True) 2003 assert self._history() 2004 2005 self._test_core(True, debug) 2006 self._test_core(False, debug) 2007 2008 transaction = [ 2009 ( 2010 20, 'wallet', 1, 800, 800, 800, 4, 5, 2011 -85, -85, -85, 6, 7, 2012 ), 2013 ( 2014 750, 'wallet', 'safe', 50, 50, 50, 4, 6, 2015 750, 750, 750, 1, 1, 2016 ), 2017 ( 2018 600, 'safe', 'bank', 150, 150, 150, 1, 2, 2019 600, 600, 600, 1, 1, 2020 ), 2021 ] 2022 for z in transaction: 2023 self.lock() 2024 x = z[1] 2025 y = z[2] 2026 self.transfer(z[0], x, y, 'test-transfer', debug=debug) 2027 assert self.balance(x) == z[3] 2028 xx = self.accounts()[x] 2029 assert xx == z[3] 2030 assert self.balance(x, False) == z[4] 2031 assert xx == z[4] 2032 2033 s = 0 2034 log = self._vault['account'][x]['log'] 2035 for i in log: 2036 s += log[i]['value'] 2037 if debug: 2038 print('s', s, 'z[5]', z[5]) 2039 assert s == z[5] 2040 2041 assert self.box_size(x) == z[6] 2042 assert self.log_size(x) == z[7] 2043 2044 yy = self.accounts()[y] 2045 assert self.balance(y) == z[8] 2046 assert yy == z[8] 2047 assert self.balance(y, False) == z[9] 2048 assert yy == z[9] 2049 2050 s = 0 2051 log = self._vault['account'][y]['log'] 2052 for i in log: 2053 s += log[i]['value'] 2054 assert s == z[10] 2055 2056 assert self.box_size(y) == z[11] 2057 assert self.log_size(y) == z[12] 2058 2059 if debug: 2060 pp().pprint(self.check(2.17)) 2061 2062 assert not self.nolock() 2063 history_count = len(self._vault['history']) 2064 if debug: 2065 print('history-count', history_count) 2066 assert history_count == 11 2067 assert not self.free(ZakatTracker.time()) 2068 assert self.free(self.lock()) 2069 assert self.nolock() 2070 assert len(self._vault['history']) == 11 2071 2072 # storage 2073 2074 _path = self.path('test.pickle') 2075 if os.path.exists(_path): 2076 os.remove(_path) 2077 self.save() 2078 assert os.path.getsize(_path) > 0 2079 self.reset() 2080 assert self.recall(False, debug) is False 2081 self.load() 2082 assert self._vault['account'] is not None 2083 2084 # recall 2085 2086 assert self.nolock() 2087 assert len(self._vault['history']) == 11 2088 assert self.recall(False, debug) is True 2089 assert len(self._vault['history']) == 10 2090 assert self.recall(False, debug) is True 2091 assert len(self._vault['history']) == 9 2092 2093 # exchange 2094 2095 self.exchange("cash", 25, 3.75, "2024-06-25") 2096 self.exchange("cash", 22, 3.73, "2024-06-22") 2097 self.exchange("cash", 15, 3.69, "2024-06-15") 2098 self.exchange("cash", 10, 3.66) 2099 2100 for i in range(1, 30): 2101 exchange = self.exchange("cash", i) 2102 rate, description, created = exchange['rate'], exchange['description'], exchange['time'] 2103 if debug: 2104 print(i, rate, description, created) 2105 assert created 2106 if i < 10: 2107 assert rate == 1 2108 assert description is None 2109 elif i == 10: 2110 assert rate == 3.66 2111 assert description is None 2112 elif i < 15: 2113 assert rate == 3.66 2114 assert description is None 2115 elif i == 15: 2116 assert rate == 3.69 2117 assert description is not None 2118 elif i < 22: 2119 assert rate == 3.69 2120 assert description is not None 2121 elif i == 22: 2122 assert rate == 3.73 2123 assert description is not None 2124 elif i >= 25: 2125 assert rate == 3.75 2126 assert description is not None 2127 exchange = self.exchange("bank", i) 2128 rate, description, created = exchange['rate'], exchange['description'], exchange['time'] 2129 if debug: 2130 print(i, rate, description, created) 2131 assert created 2132 assert rate == 1 2133 assert description is None 2134 2135 assert len(self._vault['exchange']) > 0 2136 assert len(self.exchanges()) > 0 2137 self._vault['exchange'].clear() 2138 assert len(self._vault['exchange']) == 0 2139 assert len(self.exchanges()) == 0 2140 2141 # حفظ أسعار الصرف باستخدام التواريخ بالنانو ثانية 2142 self.exchange("cash", ZakatTracker.day_to_time(25), 3.75, "2024-06-25") 2143 self.exchange("cash", ZakatTracker.day_to_time(22), 3.73, "2024-06-22") 2144 self.exchange("cash", ZakatTracker.day_to_time(15), 3.69, "2024-06-15") 2145 self.exchange("cash", ZakatTracker.day_to_time(10), 3.66) 2146 2147 for i in [x * 0.12 for x in range(-15, 21)]: 2148 if i <= 0: 2149 assert len(self.exchange("test", ZakatTracker.time(), i, f"range({i})")) == 0 2150 else: 2151 assert len(self.exchange("test", ZakatTracker.time(), i, f"range({i})")) > 0 2152 2153 # اختبار النتائج باستخدام التواريخ بالنانو ثانية 2154 for i in range(1, 31): 2155 timestamp_ns = ZakatTracker.day_to_time(i) 2156 exchange = self.exchange("cash", timestamp_ns) 2157 rate, description, created = exchange['rate'], exchange['description'], exchange['time'] 2158 if debug: 2159 print(i, rate, description, created) 2160 assert created 2161 if i < 10: 2162 assert rate == 1 2163 assert description is None 2164 elif i == 10: 2165 assert rate == 3.66 2166 assert description is None 2167 elif i < 15: 2168 assert rate == 3.66 2169 assert description is None 2170 elif i == 15: 2171 assert rate == 3.69 2172 assert description is not None 2173 elif i < 22: 2174 assert rate == 3.69 2175 assert description is not None 2176 elif i == 22: 2177 assert rate == 3.73 2178 assert description is not None 2179 elif i >= 25: 2180 assert rate == 3.75 2181 assert description is not None 2182 exchange = self.exchange("bank", i) 2183 rate, description, created = exchange['rate'], exchange['description'], exchange['time'] 2184 if debug: 2185 print(i, rate, description, created) 2186 assert created 2187 assert rate == 1 2188 assert description is None 2189 2190 # csv 2191 2192 csv_count = 1000 2193 2194 for with_rate, path in { 2195 False: 'test-import_csv-no-exchange', 2196 True: 'test-import_csv-with-exchange', 2197 }.items(): 2198 2199 if debug: 2200 print('test_import_csv', with_rate, path) 2201 2202 # csv 2203 2204 csv_path = path + '.csv' 2205 if os.path.exists(csv_path): 2206 os.remove(csv_path) 2207 c = self.generate_random_csv_file(csv_path, csv_count, with_rate, debug) 2208 if debug: 2209 print('generate_random_csv_file', c) 2210 assert c == csv_count 2211 assert os.path.getsize(csv_path) > 0 2212 cache_path = self.import_csv_cache_path() 2213 if os.path.exists(cache_path): 2214 os.remove(cache_path) 2215 self.reset() 2216 (created, found, bad) = self.import_csv(csv_path, debug) 2217 bad_count = len(bad) 2218 if debug: 2219 print(f"csv-imported: ({created}, {found}, {bad_count}) = count({csv_count})") 2220 tmp_size = os.path.getsize(cache_path) 2221 assert tmp_size > 0 2222 assert created + found + bad_count == csv_count 2223 assert created == csv_count 2224 assert bad_count == 0 2225 (created_2, found_2, bad_2) = self.import_csv(csv_path) 2226 bad_2_count = len(bad_2) 2227 if debug: 2228 print(f"csv-imported: ({created_2}, {found_2}, {bad_2_count})") 2229 print(bad) 2230 assert tmp_size == os.path.getsize(cache_path) 2231 assert created_2 + found_2 + bad_2_count == csv_count 2232 assert created == found_2 2233 assert bad_count == bad_2_count 2234 assert found_2 == csv_count 2235 assert bad_2_count == 0 2236 assert created_2 == 0 2237 2238 # payment parts 2239 2240 positive_parts = self.build_payment_parts(100, positive_only=True) 2241 assert self.check_payment_parts(positive_parts) != 0 2242 assert self.check_payment_parts(positive_parts) != 0 2243 all_parts = self.build_payment_parts(300, positive_only=False) 2244 assert self.check_payment_parts(all_parts) != 0 2245 assert self.check_payment_parts(all_parts) != 0 2246 if debug: 2247 pp().pprint(positive_parts) 2248 pp().pprint(all_parts) 2249 # dynamic discount 2250 suite = [] 2251 count = 3 2252 for exceed in [False, True]: 2253 case = [] 2254 for parts in [positive_parts, all_parts]: 2255 part = parts.copy() 2256 demand = part['demand'] 2257 if debug: 2258 print(demand, part['total']) 2259 i = 0 2260 z = demand / count 2261 cp = { 2262 'account': {}, 2263 'demand': demand, 2264 'exceed': exceed, 2265 'total': part['total'], 2266 } 2267 j = '' 2268 for x, y in part['account'].items(): 2269 x_exchange = self.exchange(x) 2270 zz = self.exchange_calc(z, 1, x_exchange['rate']) 2271 if exceed and zz <= demand: 2272 i += 1 2273 y['part'] = zz 2274 if debug: 2275 print(exceed, y) 2276 cp['account'][x] = y 2277 case.append(y) 2278 elif not exceed and y['balance'] >= zz: 2279 i += 1 2280 y['part'] = zz 2281 if debug: 2282 print(exceed, y) 2283 cp['account'][x] = y 2284 case.append(y) 2285 j = x 2286 if i >= count: 2287 break 2288 if len(cp['account'][j]) > 0: 2289 suite.append(cp) 2290 if debug: 2291 print('suite', len(suite)) 2292 # vault = self._vault.copy() 2293 for case in suite: 2294 # self._vault = vault.copy() 2295 if debug: 2296 print('case', case) 2297 result = self.check_payment_parts(case) 2298 if debug: 2299 print('check_payment_parts', result, f'exceed: {exceed}') 2300 assert result == 0 2301 2302 report = self.check(2.17, None, debug) 2303 (valid, brief, plan) = report 2304 if debug: 2305 print('valid', valid) 2306 zakat_result = self.zakat(report, parts=case, debug=debug) 2307 if debug: 2308 print('zakat-result', zakat_result) 2309 assert valid == zakat_result 2310 2311 assert self.save(path + '.pickle') 2312 assert self.export_json(path + '.json') 2313 2314 assert self.export_json("1000-transactions-test.json") 2315 assert self.save("1000-transactions-test.pickle") 2316 2317 self.reset() 2318 2319 # test transfer between accounts with different exchange rate 2320 2321 a_SAR = "Bank (SAR)" 2322 b_USD = "Bank (USD)" 2323 c_SAR = "Safe (SAR)" 2324 # 0: track, 1: check-exchange, 2: do-exchange, 3: transfer 2325 for case in [ 2326 (0, a_SAR, "SAR Gift", 1000, 1000), 2327 (1, a_SAR, 1), 2328 (0, b_USD, "USD Gift", 500, 500), 2329 (1, b_USD, 1), 2330 (2, b_USD, 3.75), 2331 (1, b_USD, 3.75), 2332 (3, 100, b_USD, a_SAR, "100 USD -> SAR", 400, 1375), 2333 (0, c_SAR, "Salary", 750, 750), 2334 (3, 375, c_SAR, b_USD, "375 SAR -> USD", 375, 500), 2335 (3, 3.75, a_SAR, b_USD, "3.75 SAR -> USD", 1371.25, 501), 2336 ]: 2337 match (case[0]): 2338 case 0: # track 2339 _, account, desc, x, balance = case 2340 self.track(value=x, desc=desc, account=account, debug=debug) 2341 2342 cached_value = self.balance(account, cached=True) 2343 fresh_value = self.balance(account, cached=False) 2344 if debug: 2345 print('account', account, 'cached_value', cached_value, 'fresh_value', fresh_value) 2346 assert cached_value == balance 2347 assert fresh_value == balance 2348 case 1: # check-exchange 2349 _, account, expected_rate = case 2350 t_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug) 2351 if debug: 2352 print('t-exchange', t_exchange) 2353 assert t_exchange['rate'] == expected_rate 2354 case 2: # do-exchange 2355 _, account, rate = case 2356 self.exchange(account, rate=rate, debug=debug) 2357 b_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug) 2358 if debug: 2359 print('b-exchange', b_exchange) 2360 assert b_exchange['rate'] == rate 2361 case 3: # transfer 2362 _, x, a, b, desc, a_balance, b_balance = case 2363 self.transfer(x, a, b, desc, debug=debug) 2364 2365 cached_value = self.balance(a, cached=True) 2366 fresh_value = self.balance(a, cached=False) 2367 if debug: 2368 print('account', a, 'cached_value', cached_value, 'fresh_value', fresh_value) 2369 assert cached_value == a_balance 2370 assert fresh_value == a_balance 2371 2372 cached_value = self.balance(b, cached=True) 2373 fresh_value = self.balance(b, cached=False) 2374 if debug: 2375 print('account', b, 'cached_value', cached_value, 'fresh_value', fresh_value) 2376 assert cached_value == b_balance 2377 assert fresh_value == b_balance 2378 2379 # Transfer all in many chunks randomly from B to A 2380 a_SAR_balance = 1371.25 2381 b_USD_balance = 501 2382 b_USD_exchange = self.exchange(b_USD) 2383 amounts = ZakatTracker.create_random_list(b_USD_balance) 2384 if debug: 2385 print('amounts', amounts) 2386 i = 0 2387 for x in amounts: 2388 if debug: 2389 print(f'{i} - transfer-with-exchange({x})') 2390 self.transfer(x, b_USD, a_SAR, f"{x} USD -> SAR", debug=debug) 2391 2392 b_USD_balance -= x 2393 cached_value = self.balance(b_USD, cached=True) 2394 fresh_value = self.balance(b_USD, cached=False) 2395 if debug: 2396 print('account', b_USD, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted', 2397 b_USD_balance) 2398 assert cached_value == b_USD_balance 2399 assert fresh_value == b_USD_balance 2400 2401 a_SAR_balance += x * b_USD_exchange['rate'] 2402 cached_value = self.balance(a_SAR, cached=True) 2403 fresh_value = self.balance(a_SAR, cached=False) 2404 if debug: 2405 print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected', 2406 a_SAR_balance, 'rate', b_USD_exchange['rate']) 2407 assert cached_value == a_SAR_balance 2408 assert fresh_value == a_SAR_balance 2409 i += 1 2410 2411 # Transfer all in many chunks randomly from C to A 2412 c_SAR_balance = 375 2413 amounts = ZakatTracker.create_random_list(c_SAR_balance) 2414 if debug: 2415 print('amounts', amounts) 2416 i = 0 2417 for x in amounts: 2418 if debug: 2419 print(f'{i} - transfer-with-exchange({x})') 2420 self.transfer(x, c_SAR, a_SAR, f"{x} SAR -> a_SAR", debug=debug) 2421 2422 c_SAR_balance -= x 2423 cached_value = self.balance(c_SAR, cached=True) 2424 fresh_value = self.balance(c_SAR, cached=False) 2425 if debug: 2426 print('account', c_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted', 2427 c_SAR_balance) 2428 assert cached_value == c_SAR_balance 2429 assert fresh_value == c_SAR_balance 2430 2431 a_SAR_balance += x 2432 cached_value = self.balance(a_SAR, cached=True) 2433 fresh_value = self.balance(a_SAR, cached=False) 2434 if debug: 2435 print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected', 2436 a_SAR_balance) 2437 assert cached_value == a_SAR_balance 2438 assert fresh_value == a_SAR_balance 2439 i += 1 2440 2441 assert self.export_json("accounts-transfer-with-exchange-rates.json") 2442 assert self.save("accounts-transfer-with-exchange-rates.pickle") 2443 2444 # check & zakat with exchange rates for many cycles 2445 2446 for rate, values in { 2447 1: { 2448 'in': [1000, 2000, 10000], 2449 'exchanged': [1000, 2000, 10000], 2450 'out': [25, 50, 731.40625], 2451 }, 2452 3.75: { 2453 'in': [200, 1000, 5000], 2454 'exchanged': [750, 3750, 18750], 2455 'out': [18.75, 93.75, 1371.38671875], 2456 }, 2457 }.items(): 2458 a, b, c = values['in'] 2459 m, n, o = values['exchanged'] 2460 x, y, z = values['out'] 2461 if debug: 2462 print('rate', rate, 'values', values) 2463 for case in [ 2464 (a, 'safe', ZakatTracker.time() - ZakatTracker.TimeCycle(), [ 2465 {'safe': {0: {'below_nisab': x}}}, 2466 ], False, m), 2467 (b, 'safe', ZakatTracker.time() - ZakatTracker.TimeCycle(), [ 2468 {'safe': {0: {'count': 1, 'total': y}}}, 2469 ], True, n), 2470 (c, 'cave', ZakatTracker.time() - (ZakatTracker.TimeCycle() * 3), [ 2471 {'cave': {0: {'count': 3, 'total': z}}}, 2472 ], True, o), 2473 ]: 2474 if debug: 2475 print(f"############# check(rate: {rate}) #############") 2476 self.reset() 2477 self.exchange(account=case[1], created=case[2], rate=rate) 2478 self.track(value=case[0], desc='test-check', account=case[1], logging=True, created=case[2]) 2479 2480 # assert self.nolock() 2481 # history_size = len(self._vault['history']) 2482 # print('history_size', history_size) 2483 # assert history_size == 2 2484 assert self.lock() 2485 assert not self.nolock() 2486 report = self.check(2.17, None, debug) 2487 (valid, brief, plan) = report 2488 assert valid == case[4] 2489 if debug: 2490 print('brief', brief) 2491 assert case[5] == brief[0] 2492 assert case[5] == brief[1] 2493 2494 if debug: 2495 pp().pprint(plan) 2496 2497 for x in plan: 2498 assert case[1] == x 2499 if 'total' in case[3][0][x][0].keys(): 2500 assert case[3][0][x][0]['total'] == brief[2] 2501 assert plan[x][0]['total'] == case[3][0][x][0]['total'] 2502 assert plan[x][0]['count'] == case[3][0][x][0]['count'] 2503 else: 2504 assert plan[x][0]['below_nisab'] == case[3][0][x][0]['below_nisab'] 2505 if debug: 2506 pp().pprint(report) 2507 result = self.zakat(report, debug=debug) 2508 if debug: 2509 print('zakat-result', result, case[4]) 2510 assert result == case[4] 2511 report = self.check(2.17, None, debug) 2512 (valid, brief, plan) = report 2513 assert valid is False 2514 2515 history_size = len(self._vault['history']) 2516 if debug: 2517 print('history_size', history_size) 2518 assert history_size == 3 2519 assert not self.nolock() 2520 assert self.recall(False, debug) is False 2521 self.free(self.lock()) 2522 assert self.nolock() 2523 2524 for i in range(3, 0, -1): 2525 history_size = len(self._vault['history']) 2526 if debug: 2527 print('history_size', history_size) 2528 assert history_size == i 2529 assert self.recall(False, debug) is True 2530 2531 assert self.nolock() 2532 assert self.recall(False, debug) is False 2533 2534 history_size = len(self._vault['history']) 2535 if debug: 2536 print('history_size', history_size) 2537 assert history_size == 0 2538 2539 account_size = len(self._vault['account']) 2540 if debug: 2541 print('account_size', account_size) 2542 assert account_size == 0 2543 2544 report_size = len(self._vault['report']) 2545 if debug: 2546 print('report_size', report_size) 2547 assert report_size == 0 2548 2549 assert self.nolock() 2550 return True 2551 except: 2552 # pp().pprint(self._vault) 2553 assert self.export_json("test-snapshot.json") 2554 assert self.save("test-snapshot.pickle") 2555 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.72'
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 vault(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 = float(_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 += float(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.
1886 def test(self, debug: bool = False) -> bool: 1887 if debug: 1888 print('test', f'debug={debug}') 1889 try: 1890 1891 assert self._history() 1892 1893 # Not allowed for duplicate transactions in the same account and time 1894 1895 created = ZakatTracker.time() 1896 self.track(100, 'test-1', 'same', True, created) 1897 failed = False 1898 try: 1899 self.track(50, 'test-1', 'same', True, created) 1900 except: 1901 failed = True 1902 assert failed is True 1903 1904 self.reset() 1905 1906 # Same account transfer 1907 for x in [1, 'a', True, 1.8, None]: 1908 failed = False 1909 try: 1910 self.transfer(1, x, x, 'same-account', debug=debug) 1911 except: 1912 failed = True 1913 assert failed is True 1914 1915 # Always preserve box age during transfer 1916 1917 series: list[tuple] = [ 1918 (30, 4), 1919 (60, 3), 1920 (90, 2), 1921 ] 1922 case = { 1923 30: { 1924 'series': series, 1925 'rest': 150, 1926 }, 1927 60: { 1928 'series': series, 1929 'rest': 120, 1930 }, 1931 90: { 1932 'series': series, 1933 'rest': 90, 1934 }, 1935 180: { 1936 'series': series, 1937 'rest': 0, 1938 }, 1939 270: { 1940 'series': series, 1941 'rest': -90, 1942 }, 1943 360: { 1944 'series': series, 1945 'rest': -180, 1946 }, 1947 } 1948 1949 selected_time = ZakatTracker.time() - ZakatTracker.TimeCycle() 1950 1951 for total in case: 1952 for x in case[total]['series']: 1953 self.track(x[0], f"test-{x} ages", 'ages', True, selected_time * x[1]) 1954 1955 refs = self.transfer(total, 'ages', 'future', 'Zakat Movement', debug=debug) 1956 1957 if debug: 1958 print('refs', refs) 1959 1960 ages_cache_balance = self.balance('ages') 1961 ages_fresh_balance = self.balance('ages', False) 1962 rest = case[total]['rest'] 1963 if debug: 1964 print('source', ages_cache_balance, ages_fresh_balance, rest) 1965 assert ages_cache_balance == rest 1966 assert ages_fresh_balance == rest 1967 1968 future_cache_balance = self.balance('future') 1969 future_fresh_balance = self.balance('future', False) 1970 if debug: 1971 print('target', future_cache_balance, future_fresh_balance, total) 1972 print('refs', refs) 1973 assert future_cache_balance == total 1974 assert future_fresh_balance == total 1975 1976 for ref in self._vault['account']['ages']['box']: 1977 ages_capital = self._vault['account']['ages']['box'][ref]['capital'] 1978 ages_rest = self._vault['account']['ages']['box'][ref]['rest'] 1979 future_capital = 0 1980 if ref in self._vault['account']['future']['box']: 1981 future_capital = self._vault['account']['future']['box'][ref]['capital'] 1982 future_rest = 0 1983 if ref in self._vault['account']['future']['box']: 1984 future_rest = self._vault['account']['future']['box'][ref]['rest'] 1985 if ages_capital != 0 and future_capital != 0 and future_rest != 0: 1986 if debug: 1987 print('================================================================') 1988 print('ages', ages_capital, ages_rest) 1989 print('future', future_capital, future_rest) 1990 if ages_rest == 0: 1991 assert ages_capital == future_capital 1992 elif ages_rest < 0: 1993 assert -ages_capital == future_capital 1994 elif ages_rest > 0: 1995 assert ages_capital == ages_rest + future_capital 1996 self.reset() 1997 assert len(self._vault['history']) == 0 1998 1999 assert self._history() 2000 assert self._history(False) is False 2001 assert self._history() is False 2002 assert self._history(True) 2003 assert self._history() 2004 2005 self._test_core(True, debug) 2006 self._test_core(False, debug) 2007 2008 transaction = [ 2009 ( 2010 20, 'wallet', 1, 800, 800, 800, 4, 5, 2011 -85, -85, -85, 6, 7, 2012 ), 2013 ( 2014 750, 'wallet', 'safe', 50, 50, 50, 4, 6, 2015 750, 750, 750, 1, 1, 2016 ), 2017 ( 2018 600, 'safe', 'bank', 150, 150, 150, 1, 2, 2019 600, 600, 600, 1, 1, 2020 ), 2021 ] 2022 for z in transaction: 2023 self.lock() 2024 x = z[1] 2025 y = z[2] 2026 self.transfer(z[0], x, y, 'test-transfer', debug=debug) 2027 assert self.balance(x) == z[3] 2028 xx = self.accounts()[x] 2029 assert xx == z[3] 2030 assert self.balance(x, False) == z[4] 2031 assert xx == z[4] 2032 2033 s = 0 2034 log = self._vault['account'][x]['log'] 2035 for i in log: 2036 s += log[i]['value'] 2037 if debug: 2038 print('s', s, 'z[5]', z[5]) 2039 assert s == z[5] 2040 2041 assert self.box_size(x) == z[6] 2042 assert self.log_size(x) == z[7] 2043 2044 yy = self.accounts()[y] 2045 assert self.balance(y) == z[8] 2046 assert yy == z[8] 2047 assert self.balance(y, False) == z[9] 2048 assert yy == z[9] 2049 2050 s = 0 2051 log = self._vault['account'][y]['log'] 2052 for i in log: 2053 s += log[i]['value'] 2054 assert s == z[10] 2055 2056 assert self.box_size(y) == z[11] 2057 assert self.log_size(y) == z[12] 2058 2059 if debug: 2060 pp().pprint(self.check(2.17)) 2061 2062 assert not self.nolock() 2063 history_count = len(self._vault['history']) 2064 if debug: 2065 print('history-count', history_count) 2066 assert history_count == 11 2067 assert not self.free(ZakatTracker.time()) 2068 assert self.free(self.lock()) 2069 assert self.nolock() 2070 assert len(self._vault['history']) == 11 2071 2072 # storage 2073 2074 _path = self.path('test.pickle') 2075 if os.path.exists(_path): 2076 os.remove(_path) 2077 self.save() 2078 assert os.path.getsize(_path) > 0 2079 self.reset() 2080 assert self.recall(False, debug) is False 2081 self.load() 2082 assert self._vault['account'] is not None 2083 2084 # recall 2085 2086 assert self.nolock() 2087 assert len(self._vault['history']) == 11 2088 assert self.recall(False, debug) is True 2089 assert len(self._vault['history']) == 10 2090 assert self.recall(False, debug) is True 2091 assert len(self._vault['history']) == 9 2092 2093 # exchange 2094 2095 self.exchange("cash", 25, 3.75, "2024-06-25") 2096 self.exchange("cash", 22, 3.73, "2024-06-22") 2097 self.exchange("cash", 15, 3.69, "2024-06-15") 2098 self.exchange("cash", 10, 3.66) 2099 2100 for i in range(1, 30): 2101 exchange = self.exchange("cash", i) 2102 rate, description, created = exchange['rate'], exchange['description'], exchange['time'] 2103 if debug: 2104 print(i, rate, description, created) 2105 assert created 2106 if i < 10: 2107 assert rate == 1 2108 assert description is None 2109 elif i == 10: 2110 assert rate == 3.66 2111 assert description is None 2112 elif i < 15: 2113 assert rate == 3.66 2114 assert description is None 2115 elif i == 15: 2116 assert rate == 3.69 2117 assert description is not None 2118 elif i < 22: 2119 assert rate == 3.69 2120 assert description is not None 2121 elif i == 22: 2122 assert rate == 3.73 2123 assert description is not None 2124 elif i >= 25: 2125 assert rate == 3.75 2126 assert description is not None 2127 exchange = self.exchange("bank", i) 2128 rate, description, created = exchange['rate'], exchange['description'], exchange['time'] 2129 if debug: 2130 print(i, rate, description, created) 2131 assert created 2132 assert rate == 1 2133 assert description is None 2134 2135 assert len(self._vault['exchange']) > 0 2136 assert len(self.exchanges()) > 0 2137 self._vault['exchange'].clear() 2138 assert len(self._vault['exchange']) == 0 2139 assert len(self.exchanges()) == 0 2140 2141 # حفظ أسعار الصرف باستخدام التواريخ بالنانو ثانية 2142 self.exchange("cash", ZakatTracker.day_to_time(25), 3.75, "2024-06-25") 2143 self.exchange("cash", ZakatTracker.day_to_time(22), 3.73, "2024-06-22") 2144 self.exchange("cash", ZakatTracker.day_to_time(15), 3.69, "2024-06-15") 2145 self.exchange("cash", ZakatTracker.day_to_time(10), 3.66) 2146 2147 for i in [x * 0.12 for x in range(-15, 21)]: 2148 if i <= 0: 2149 assert len(self.exchange("test", ZakatTracker.time(), i, f"range({i})")) == 0 2150 else: 2151 assert len(self.exchange("test", ZakatTracker.time(), i, f"range({i})")) > 0 2152 2153 # اختبار النتائج باستخدام التواريخ بالنانو ثانية 2154 for i in range(1, 31): 2155 timestamp_ns = ZakatTracker.day_to_time(i) 2156 exchange = self.exchange("cash", timestamp_ns) 2157 rate, description, created = exchange['rate'], exchange['description'], exchange['time'] 2158 if debug: 2159 print(i, rate, description, created) 2160 assert created 2161 if i < 10: 2162 assert rate == 1 2163 assert description is None 2164 elif i == 10: 2165 assert rate == 3.66 2166 assert description is None 2167 elif i < 15: 2168 assert rate == 3.66 2169 assert description is None 2170 elif i == 15: 2171 assert rate == 3.69 2172 assert description is not None 2173 elif i < 22: 2174 assert rate == 3.69 2175 assert description is not None 2176 elif i == 22: 2177 assert rate == 3.73 2178 assert description is not None 2179 elif i >= 25: 2180 assert rate == 3.75 2181 assert description is not None 2182 exchange = self.exchange("bank", i) 2183 rate, description, created = exchange['rate'], exchange['description'], exchange['time'] 2184 if debug: 2185 print(i, rate, description, created) 2186 assert created 2187 assert rate == 1 2188 assert description is None 2189 2190 # csv 2191 2192 csv_count = 1000 2193 2194 for with_rate, path in { 2195 False: 'test-import_csv-no-exchange', 2196 True: 'test-import_csv-with-exchange', 2197 }.items(): 2198 2199 if debug: 2200 print('test_import_csv', with_rate, path) 2201 2202 # csv 2203 2204 csv_path = path + '.csv' 2205 if os.path.exists(csv_path): 2206 os.remove(csv_path) 2207 c = self.generate_random_csv_file(csv_path, csv_count, with_rate, debug) 2208 if debug: 2209 print('generate_random_csv_file', c) 2210 assert c == csv_count 2211 assert os.path.getsize(csv_path) > 0 2212 cache_path = self.import_csv_cache_path() 2213 if os.path.exists(cache_path): 2214 os.remove(cache_path) 2215 self.reset() 2216 (created, found, bad) = self.import_csv(csv_path, debug) 2217 bad_count = len(bad) 2218 if debug: 2219 print(f"csv-imported: ({created}, {found}, {bad_count}) = count({csv_count})") 2220 tmp_size = os.path.getsize(cache_path) 2221 assert tmp_size > 0 2222 assert created + found + bad_count == csv_count 2223 assert created == csv_count 2224 assert bad_count == 0 2225 (created_2, found_2, bad_2) = self.import_csv(csv_path) 2226 bad_2_count = len(bad_2) 2227 if debug: 2228 print(f"csv-imported: ({created_2}, {found_2}, {bad_2_count})") 2229 print(bad) 2230 assert tmp_size == os.path.getsize(cache_path) 2231 assert created_2 + found_2 + bad_2_count == csv_count 2232 assert created == found_2 2233 assert bad_count == bad_2_count 2234 assert found_2 == csv_count 2235 assert bad_2_count == 0 2236 assert created_2 == 0 2237 2238 # payment parts 2239 2240 positive_parts = self.build_payment_parts(100, positive_only=True) 2241 assert self.check_payment_parts(positive_parts) != 0 2242 assert self.check_payment_parts(positive_parts) != 0 2243 all_parts = self.build_payment_parts(300, positive_only=False) 2244 assert self.check_payment_parts(all_parts) != 0 2245 assert self.check_payment_parts(all_parts) != 0 2246 if debug: 2247 pp().pprint(positive_parts) 2248 pp().pprint(all_parts) 2249 # dynamic discount 2250 suite = [] 2251 count = 3 2252 for exceed in [False, True]: 2253 case = [] 2254 for parts in [positive_parts, all_parts]: 2255 part = parts.copy() 2256 demand = part['demand'] 2257 if debug: 2258 print(demand, part['total']) 2259 i = 0 2260 z = demand / count 2261 cp = { 2262 'account': {}, 2263 'demand': demand, 2264 'exceed': exceed, 2265 'total': part['total'], 2266 } 2267 j = '' 2268 for x, y in part['account'].items(): 2269 x_exchange = self.exchange(x) 2270 zz = self.exchange_calc(z, 1, x_exchange['rate']) 2271 if exceed and zz <= demand: 2272 i += 1 2273 y['part'] = zz 2274 if debug: 2275 print(exceed, y) 2276 cp['account'][x] = y 2277 case.append(y) 2278 elif not exceed and y['balance'] >= zz: 2279 i += 1 2280 y['part'] = zz 2281 if debug: 2282 print(exceed, y) 2283 cp['account'][x] = y 2284 case.append(y) 2285 j = x 2286 if i >= count: 2287 break 2288 if len(cp['account'][j]) > 0: 2289 suite.append(cp) 2290 if debug: 2291 print('suite', len(suite)) 2292 # vault = self._vault.copy() 2293 for case in suite: 2294 # self._vault = vault.copy() 2295 if debug: 2296 print('case', case) 2297 result = self.check_payment_parts(case) 2298 if debug: 2299 print('check_payment_parts', result, f'exceed: {exceed}') 2300 assert result == 0 2301 2302 report = self.check(2.17, None, debug) 2303 (valid, brief, plan) = report 2304 if debug: 2305 print('valid', valid) 2306 zakat_result = self.zakat(report, parts=case, debug=debug) 2307 if debug: 2308 print('zakat-result', zakat_result) 2309 assert valid == zakat_result 2310 2311 assert self.save(path + '.pickle') 2312 assert self.export_json(path + '.json') 2313 2314 assert self.export_json("1000-transactions-test.json") 2315 assert self.save("1000-transactions-test.pickle") 2316 2317 self.reset() 2318 2319 # test transfer between accounts with different exchange rate 2320 2321 a_SAR = "Bank (SAR)" 2322 b_USD = "Bank (USD)" 2323 c_SAR = "Safe (SAR)" 2324 # 0: track, 1: check-exchange, 2: do-exchange, 3: transfer 2325 for case in [ 2326 (0, a_SAR, "SAR Gift", 1000, 1000), 2327 (1, a_SAR, 1), 2328 (0, b_USD, "USD Gift", 500, 500), 2329 (1, b_USD, 1), 2330 (2, b_USD, 3.75), 2331 (1, b_USD, 3.75), 2332 (3, 100, b_USD, a_SAR, "100 USD -> SAR", 400, 1375), 2333 (0, c_SAR, "Salary", 750, 750), 2334 (3, 375, c_SAR, b_USD, "375 SAR -> USD", 375, 500), 2335 (3, 3.75, a_SAR, b_USD, "3.75 SAR -> USD", 1371.25, 501), 2336 ]: 2337 match (case[0]): 2338 case 0: # track 2339 _, account, desc, x, balance = case 2340 self.track(value=x, desc=desc, account=account, debug=debug) 2341 2342 cached_value = self.balance(account, cached=True) 2343 fresh_value = self.balance(account, cached=False) 2344 if debug: 2345 print('account', account, 'cached_value', cached_value, 'fresh_value', fresh_value) 2346 assert cached_value == balance 2347 assert fresh_value == balance 2348 case 1: # check-exchange 2349 _, account, expected_rate = case 2350 t_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug) 2351 if debug: 2352 print('t-exchange', t_exchange) 2353 assert t_exchange['rate'] == expected_rate 2354 case 2: # do-exchange 2355 _, account, rate = case 2356 self.exchange(account, rate=rate, debug=debug) 2357 b_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug) 2358 if debug: 2359 print('b-exchange', b_exchange) 2360 assert b_exchange['rate'] == rate 2361 case 3: # transfer 2362 _, x, a, b, desc, a_balance, b_balance = case 2363 self.transfer(x, a, b, desc, debug=debug) 2364 2365 cached_value = self.balance(a, cached=True) 2366 fresh_value = self.balance(a, cached=False) 2367 if debug: 2368 print('account', a, 'cached_value', cached_value, 'fresh_value', fresh_value) 2369 assert cached_value == a_balance 2370 assert fresh_value == a_balance 2371 2372 cached_value = self.balance(b, cached=True) 2373 fresh_value = self.balance(b, cached=False) 2374 if debug: 2375 print('account', b, 'cached_value', cached_value, 'fresh_value', fresh_value) 2376 assert cached_value == b_balance 2377 assert fresh_value == b_balance 2378 2379 # Transfer all in many chunks randomly from B to A 2380 a_SAR_balance = 1371.25 2381 b_USD_balance = 501 2382 b_USD_exchange = self.exchange(b_USD) 2383 amounts = ZakatTracker.create_random_list(b_USD_balance) 2384 if debug: 2385 print('amounts', amounts) 2386 i = 0 2387 for x in amounts: 2388 if debug: 2389 print(f'{i} - transfer-with-exchange({x})') 2390 self.transfer(x, b_USD, a_SAR, f"{x} USD -> SAR", debug=debug) 2391 2392 b_USD_balance -= x 2393 cached_value = self.balance(b_USD, cached=True) 2394 fresh_value = self.balance(b_USD, cached=False) 2395 if debug: 2396 print('account', b_USD, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted', 2397 b_USD_balance) 2398 assert cached_value == b_USD_balance 2399 assert fresh_value == b_USD_balance 2400 2401 a_SAR_balance += x * b_USD_exchange['rate'] 2402 cached_value = self.balance(a_SAR, cached=True) 2403 fresh_value = self.balance(a_SAR, cached=False) 2404 if debug: 2405 print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected', 2406 a_SAR_balance, 'rate', b_USD_exchange['rate']) 2407 assert cached_value == a_SAR_balance 2408 assert fresh_value == a_SAR_balance 2409 i += 1 2410 2411 # Transfer all in many chunks randomly from C to A 2412 c_SAR_balance = 375 2413 amounts = ZakatTracker.create_random_list(c_SAR_balance) 2414 if debug: 2415 print('amounts', amounts) 2416 i = 0 2417 for x in amounts: 2418 if debug: 2419 print(f'{i} - transfer-with-exchange({x})') 2420 self.transfer(x, c_SAR, a_SAR, f"{x} SAR -> a_SAR", debug=debug) 2421 2422 c_SAR_balance -= x 2423 cached_value = self.balance(c_SAR, cached=True) 2424 fresh_value = self.balance(c_SAR, cached=False) 2425 if debug: 2426 print('account', c_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted', 2427 c_SAR_balance) 2428 assert cached_value == c_SAR_balance 2429 assert fresh_value == c_SAR_balance 2430 2431 a_SAR_balance += x 2432 cached_value = self.balance(a_SAR, cached=True) 2433 fresh_value = self.balance(a_SAR, cached=False) 2434 if debug: 2435 print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected', 2436 a_SAR_balance) 2437 assert cached_value == a_SAR_balance 2438 assert fresh_value == a_SAR_balance 2439 i += 1 2440 2441 assert self.export_json("accounts-transfer-with-exchange-rates.json") 2442 assert self.save("accounts-transfer-with-exchange-rates.pickle") 2443 2444 # check & zakat with exchange rates for many cycles 2445 2446 for rate, values in { 2447 1: { 2448 'in': [1000, 2000, 10000], 2449 'exchanged': [1000, 2000, 10000], 2450 'out': [25, 50, 731.40625], 2451 }, 2452 3.75: { 2453 'in': [200, 1000, 5000], 2454 'exchanged': [750, 3750, 18750], 2455 'out': [18.75, 93.75, 1371.38671875], 2456 }, 2457 }.items(): 2458 a, b, c = values['in'] 2459 m, n, o = values['exchanged'] 2460 x, y, z = values['out'] 2461 if debug: 2462 print('rate', rate, 'values', values) 2463 for case in [ 2464 (a, 'safe', ZakatTracker.time() - ZakatTracker.TimeCycle(), [ 2465 {'safe': {0: {'below_nisab': x}}}, 2466 ], False, m), 2467 (b, 'safe', ZakatTracker.time() - ZakatTracker.TimeCycle(), [ 2468 {'safe': {0: {'count': 1, 'total': y}}}, 2469 ], True, n), 2470 (c, 'cave', ZakatTracker.time() - (ZakatTracker.TimeCycle() * 3), [ 2471 {'cave': {0: {'count': 3, 'total': z}}}, 2472 ], True, o), 2473 ]: 2474 if debug: 2475 print(f"############# check(rate: {rate}) #############") 2476 self.reset() 2477 self.exchange(account=case[1], created=case[2], rate=rate) 2478 self.track(value=case[0], desc='test-check', account=case[1], logging=True, created=case[2]) 2479 2480 # assert self.nolock() 2481 # history_size = len(self._vault['history']) 2482 # print('history_size', history_size) 2483 # assert history_size == 2 2484 assert self.lock() 2485 assert not self.nolock() 2486 report = self.check(2.17, None, debug) 2487 (valid, brief, plan) = report 2488 assert valid == case[4] 2489 if debug: 2490 print('brief', brief) 2491 assert case[5] == brief[0] 2492 assert case[5] == brief[1] 2493 2494 if debug: 2495 pp().pprint(plan) 2496 2497 for x in plan: 2498 assert case[1] == x 2499 if 'total' in case[3][0][x][0].keys(): 2500 assert case[3][0][x][0]['total'] == brief[2] 2501 assert plan[x][0]['total'] == case[3][0][x][0]['total'] 2502 assert plan[x][0]['count'] == case[3][0][x][0]['count'] 2503 else: 2504 assert plan[x][0]['below_nisab'] == case[3][0][x][0]['below_nisab'] 2505 if debug: 2506 pp().pprint(report) 2507 result = self.zakat(report, debug=debug) 2508 if debug: 2509 print('zakat-result', result, case[4]) 2510 assert result == case[4] 2511 report = self.check(2.17, None, debug) 2512 (valid, brief, plan) = report 2513 assert valid is False 2514 2515 history_size = len(self._vault['history']) 2516 if debug: 2517 print('history_size', history_size) 2518 assert history_size == 3 2519 assert not self.nolock() 2520 assert self.recall(False, debug) is False 2521 self.free(self.lock()) 2522 assert self.nolock() 2523 2524 for i in range(3, 0, -1): 2525 history_size = len(self._vault['history']) 2526 if debug: 2527 print('history_size', history_size) 2528 assert history_size == i 2529 assert self.recall(False, debug) is True 2530 2531 assert self.nolock() 2532 assert self.recall(False, debug) is False 2533 2534 history_size = len(self._vault['history']) 2535 if debug: 2536 print('history_size', history_size) 2537 assert history_size == 0 2538 2539 account_size = len(self._vault['account']) 2540 if debug: 2541 print('account_size', account_size) 2542 assert account_size == 0 2543 2544 report_size = len(self._vault['report']) 2545 if debug: 2546 print('report_size', report_size) 2547 assert report_size == 0 2548 2549 assert self.nolock() 2550 return True 2551 except: 2552 # pp().pprint(self._vault) 2553 assert self.export_json("test-snapshot.json") 2554 assert self.save("test-snapshot.pickle") 2555 raise
2558def test(debug: bool = False): 2559 ledger = ZakatTracker() 2560 start = ZakatTracker.time() 2561 assert ledger.test(debug=debug) 2562 if debug: 2563 print("#########################") 2564 print("######## TEST DONE ########") 2565 print("#########################") 2566 print(ZakatTracker.duration_from_nanoseconds(ZakatTracker.time() - start)) 2567 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}")