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