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