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