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