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