zakat
xxx

_____ _ _ _____ _
|__ /__ _| | ____ _| |_ |_ _| __ __ _ ___| | _____ _ __ / // _| |/ / _ | __| | || '__/ _` |/ __| |/ / _ \ '__| / /| (_| | < (_| | |_ | || | | (_| | (__| < __/ |
/______,_|_|___,_|__| |_||_| __,_|___|_|____|_|

"رَبَّنَا افْتَحْ بَيْنَنَا وَبَيْنَ قَوْمِنَا بِالْحَقِّ وَأَنتَ خَيْرُ الْفَاتِحِينَ (89)" -- سورة الأعراف ... Never Trust, Always Verify ...

This module provides the ZakatTracker class for tracking and calculating Zakat.

 1"""
 2 _____     _         _     _____               _             
 3|__  /__ _| | ____ _| |_  |_   _| __ __ _  ___| | _____ _ __ 
 4  / // _` | |/ / _` | __|   | || '__/ _` |/ __| |/ / _ \ '__|
 5 / /| (_| |   < (_| | |_    | || | | (_| | (__|   <  __/ |   
 6/____\__,_|_|\_\__,_|\__|   |_||_|  \__,_|\___|_|\_\___|_|   
 7
 8"رَبَّنَا افْتَحْ بَيْنَنَا وَبَيْنَ قَوْمِنَا بِالْحَقِّ وَأَنتَ خَيْرُ الْفَاتِحِينَ (89)" -- سورة الأعراف
 9... Never Trust, Always Verify ...
10
11This module provides the ZakatTracker class for tracking and calculating Zakat.
12"""
13# Importing necessary classes and functions from the main module
14from .zakat_tracker import (
15    ZakatTracker,
16    Action,
17    JSONEncoder,
18    MathOperation,
19)
20
21# Version information for the module
22__version__ = ZakatTracker.Version()
23__all__ = [
24    "ZakatTracker",
25    "Action",
26    "JSONEncoder",
27    "MathOperation",
28]
class ZakatTracker:
  89class ZakatTracker:
  90	"""
  91    A class for tracking and calculating Zakat.
  92
  93    This class provides functionalities for recording transactions, calculating Zakat due,
  94    and managing account balances. It also offers features like importing transactions from
  95    CSV files, exporting data to JSON format, and saving/loading the tracker state.
  96
  97    The `ZakatTracker` class is designed to handle both positive and negative transactions,
  98    allowing for flexible tracking of financial activities related to Zakat. It also supports
  99    the concept of a "nisab" (minimum threshold for Zakat) and a "haul" (complete one year for Transaction) can calculate Zakat due
 100    based on the current silver price.
 101
 102    The class uses a pickle file as its database to persist the tracker state,
 103    ensuring data integrity across sessions. It also provides options for enabling or
 104    disabling history tracking, allowing users to choose their preferred level of detail.
 105
 106    In addition, the `ZakatTracker` class includes various helper methods like
 107    `time`, `time_to_datetime`, `lock`, `free`, `recall`, `export_json`,
 108    and more. These methods provide additional functionalities and flexibility
 109    for interacting with and managing the Zakat tracker.
 110
 111    Attributes:
 112        ZakatCut (function): A function to calculate the Zakat percentage.
 113        TimeCycle (function): A function to determine the time cycle for Zakat.
 114        Nisab (function): A function to calculate the Nisab based on the silver price.
 115        Version (str): The version of the ZakatTracker class.
 116
 117	Data Structure:
 118        The ZakatTracker class utilizes a nested dictionary structure called "_vault" to store and manage data.
 119
 120        _vault (dict):
 121            - account (dict):
 122                - {account_number} (dict):
 123                    - balance (int): The current balance of the account.
 124                    - box (dict): A dictionary storing transaction details.
 125                        - {timestamp} (dict):
 126                            - capital (int): The initial amount of the transaction.
 127                            - count (int): The number of times Zakat has been calculated for this transaction.
 128                            - last (int): The timestamp of the last Zakat calculation.
 129                            - rest (int): The remaining amount after Zakat deductions and withdrawal.
 130                            - total (int): The total Zakat deducted from this transaction.
 131                    - count (int): The total number of transactions for the account.
 132                    - log (dict): A dictionary storing transaction logs.
 133                        - {timestamp} (dict):
 134                            - value (int): The transaction amount (positive or negative).
 135                            - desc (str): The description of the transaction.
 136                            - file (dict): A dictionary storing file references associated with the transaction.
 137                    - zakatable (bool): Indicates whether the account is subject to Zakat.
 138            - history (dict):
 139                - {timestamp} (list): A list of dictionaries storing the history of actions performed.
 140                    - {action_dict} (dict):
 141                        - action (Action): The type of action (CREATE, TRACK, LOG, SUB, ADD_FILE, REMOVE_FILE, REPORT, ZAKAT).
 142                        - account (str): The account number associated with the action.
 143                        - ref (int): The reference number of the transaction.
 144                        - file (int): The reference number of the file (if applicable).
 145                        - key (str): The key associated with the action (e.g., 'rest', 'total').
 146                        - value (int): The value associated with the action.
 147                        - math (MathOperation): The mathematical operation performed (if applicable).
 148            - lock (int or None): The timestamp indicating the current lock status (None if not locked).
 149            - report (dict):
 150                - {timestamp} (tuple): A tuple storing Zakat report details.
 151
 152    """
 153
 154	# Hybrid Constants
 155	ZakatCut	= lambda x: 0.025*x # Zakat Cut in one Lunar Year
 156	TimeCycle	= lambda  : int(60*60*24*354.367056*1e9) # Lunar Year in nanoseconds
 157	Nisab		= lambda x: 585*x # Silver Price in Local currency value
 158	Version		= lambda  : '0.2.2'
 159
 160	def __init__(self, db_path: str = "zakat.pickle", history_mode: bool = True):
 161		"""
 162        Initialize ZakatTracker with database path and history mode.
 163
 164        Parameters:
 165        db_path (str): The path to the database file. Default is "zakat.pickle".
 166        history_mode (bool): The mode for tracking history. Default is True.
 167
 168        Returns:
 169        None
 170        """
 171		self.reset()
 172		self._history(history_mode)
 173		self.path(db_path)
 174		self.load()
 175
 176	def path(self, _path: str = None) -> str:
 177		"""
 178        Set or get the database path.
 179
 180        Parameters:
 181        _path (str): The path to the database file. If not provided, it returns the current path.
 182
 183        Returns:
 184        str: The current database path.
 185        """
 186		if _path is not None:
 187			self._vault_path = _path
 188		return self._vault_path
 189
 190	def _history(self, status: bool = None) -> bool:
 191		"""
 192        Enable or disable history tracking.
 193
 194        Parameters:
 195        status (bool): The status of history tracking. Default is True.
 196
 197        Returns:
 198        None
 199        """
 200		if status is not None:
 201			self._history_mode = status
 202		return self._history_mode
 203
 204	def reset(self) -> None:
 205		"""
 206        Reset the internal data structure to its initial state.
 207
 208        Parameters:
 209        None
 210
 211        Returns:
 212        None
 213        """
 214		self._vault = {}
 215		self._vault['account'] = {}
 216		self._vault['history'] = {}
 217		self._vault['lock'] = None
 218		self._vault['report'] = {}
 219
 220	@staticmethod
 221	def time(now: datetime = None) -> int:
 222		"""
 223        Generates a timestamp based on the provided datetime object or the current datetime.
 224
 225        Parameters:
 226        now (datetime, optional): The datetime object to generate the timestamp from. If not provided, the current datetime is used.
 227
 228        Returns:
 229        int: The timestamp in positive nanoseconds since the Unix epoch (January 1, 1970), before 1970 will return in negative until 1000AD.
 230        """
 231		if now is None:
 232			now = datetime.datetime.now()
 233		ordinal_day = now.toordinal()
 234		ns_in_day = (now - now.replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() * 10**9
 235		return int((ordinal_day - 719_163) * 86_400_000_000_000 + ns_in_day)
 236
 237	@staticmethod
 238	def time_to_datetime(ordinal_ns: int) -> datetime:
 239		"""
 240        Converts an ordinal number (number of days since 1000-01-01) to a datetime object.
 241
 242        Parameters:
 243        ordinal_ns (int): The ordinal number of days since 1000-01-01.
 244
 245        Returns:
 246        datetime: The corresponding datetime object.
 247        """
 248		ordinal_day = ordinal_ns // 86_400_000_000_000 + 719_163
 249		ns_in_day = ordinal_ns % 86_400_000_000_000
 250		d = datetime.datetime.fromordinal(ordinal_day)
 251		t = datetime.timedelta(seconds=ns_in_day // 10**9)
 252		return datetime.datetime.combine(d, datetime.time()) + t
 253
 254	def _step(self, action: Action = None, account = None, ref: int = None, file: int = None, value: int = None, key: str = None, math_operation: MathOperation = None) -> int:
 255		"""
 256		This method is responsible for recording the actions performed on the ZakatTracker.
 257
 258		Parameters:
 259		- action (Action): The type of action performed.
 260		- account (str): The account number on which the action was performed.
 261		- ref (int): The reference number of the action.
 262		- file (int): The file reference number of the action.
 263		- value (int): The value associated with the action.
 264		- key (str): The key associated with the action.
 265		- math_operation (MathOperation): The mathematical operation performed during the action.
 266
 267		Returns:
 268		- int: The lock time of the recorded action. If no lock was performed, it returns 0.
 269		"""
 270		if not self._history():
 271			return 0
 272		_lock = self._vault['lock']
 273		if self.nolock():
 274			_lock = self._vault['lock'] = ZakatTracker.time()
 275			self._vault['history'][_lock] = []
 276		if action is None:
 277			return _lock
 278		self._vault['history'][_lock].append({
 279			'action': action,
 280			'account': account,
 281			'ref': ref,
 282			'file': file,
 283			'key': key,
 284			'value': value,
 285			'math': math_operation,
 286		})
 287
 288	def nolock(self) -> bool:
 289		"""
 290        Check if the vault lock is currently not set.
 291
 292        :return: True if the vault lock is not set, False otherwise.
 293        """
 294		return self._vault['lock'] is None
 295
 296	def lock(self) -> int:
 297		"""
 298        Acquires a lock on the ZakatTracker instance.
 299
 300        Returns:
 301        int: The lock ID. This ID can be used to release the lock later.
 302        """
 303		return self._step()
 304
 305	def box(self) -> dict:
 306		"""
 307        Returns a copy of the internal vault dictionary.
 308
 309        This method is used to retrieve the current state of the ZakatTracker object.
 310        It provides a snapshot of the internal data structure, allowing for further
 311        processing or analysis.
 312
 313        :return: A copy of the internal vault dictionary.
 314        """
 315		return self._vault.copy()
 316
 317	def steps(self) -> dict:
 318		"""
 319        Returns a copy of the history of steps taken in the ZakatTracker.
 320
 321        The history is a dictionary where each key is a unique identifier for a step,
 322        and the corresponding value is a dictionary containing information about the step.
 323
 324        :return: A copy of the history of steps taken in the ZakatTracker.
 325        """
 326		return self._vault['history'].copy()
 327
 328	def free(self, _lock: int, auto_save: bool = True) -> bool:
 329		"""
 330        Releases the lock on the database.
 331
 332        Parameters:
 333        _lock (int): The lock ID to be released.
 334        auto_save (bool): Whether to automatically save the database after releasing the lock.
 335
 336        Returns:
 337        bool: True if the lock is successfully released and (optionally) saved, False otherwise.
 338        """
 339		if _lock == self._vault['lock']:
 340			self._vault['lock'] = None
 341			if auto_save:
 342				return self.save(self.path())
 343			return True
 344		return False
 345
 346	def account_exists(self, account) -> bool:
 347		"""
 348        Check if the given account exists in the vault.
 349
 350        Parameters:
 351        account (str): The account number to check.
 352
 353        Returns:
 354        bool: True if the account exists, False otherwise.
 355        """
 356		return account in self._vault['account']
 357
 358	def box_size(self, account) -> int:
 359		"""
 360		Calculate the size of the box for a specific account.
 361
 362		Parameters:
 363		account (str): The account number for which the box size needs to be calculated.
 364
 365		Returns:
 366		int: The size of the box for the given account. If the account does not exist, -1 is returned.
 367		"""
 368		if self.account_exists(account):
 369			return len(self._vault['account'][account]['box'])
 370		return -1
 371
 372	def log_size(self, account) -> int:
 373		"""
 374        Get the size of the log for a specific account.
 375
 376        Parameters:
 377        account (str): The account number for which the log size needs to be calculated.
 378
 379        Returns:
 380        int: The size of the log for the given account. If the account does not exist, -1 is returned.
 381        """
 382		if self.account_exists(account):
 383			return len(self._vault['account'][account]['log'])
 384		return -1
 385
 386	def recall(self, dry = True, debug = False) -> bool:
 387		"""
 388		Revert the last operation.
 389
 390		Parameters:
 391		dry (bool): If True, the function will not modify the data, but will simulate the operation. Default is True.
 392		debug (bool): If True, the function will print debug information. Default is False.
 393
 394		Returns:
 395		bool: True if the operation was successful, False otherwise.
 396		"""
 397		if not self.nolock() or len(self._vault['history']) == 0:
 398			return False
 399		if len(self._vault['history']) <= 0:
 400			return
 401		ref = sorted(self._vault['history'].keys())[-1]
 402		if debug:
 403			print('recall', ref)
 404		memory = self._vault['history'][ref]
 405		if debug:
 406			print(type(memory), 'memory', memory)
 407
 408		limit = len(memory) + 1
 409		sub_positive_log_negative = 0
 410		for i in range(-1, -limit, -1):
 411			x = memory[i]
 412			if debug:
 413				print(type(x), x)
 414			match x['action']:
 415				case Action.CREATE:
 416					if x['account'] is not None:
 417						if self.account_exists(x['account']):
 418							if debug:
 419								print('account', self._vault['account'][x['account']])
 420							assert len(self._vault['account'][x['account']]['box']) == 0
 421							assert self._vault['account'][x['account']]['balance'] == 0
 422							assert self._vault['account'][x['account']]['count'] == 0
 423							if dry:
 424								continue
 425							del self._vault['account'][x['account']]
 426
 427				case Action.TRACK:
 428					if x['account'] is not None:
 429						if self.account_exists(x['account']):
 430							if dry:
 431								continue
 432							self._vault['account'][x['account']]['balance'] -= x['value']
 433							self._vault['account'][x['account']]['count'] -= 1
 434							del self._vault['account'][x['account']]['box'][x['ref']]
 435
 436				case Action.LOG:
 437					if x['account'] is not None:
 438						if self.account_exists(x['account']):
 439							if x['ref'] in self._vault['account'][x['account']]['log']:
 440								if dry:
 441									continue
 442								if sub_positive_log_negative == -x['value']:
 443									self._vault['account'][x['account']]['count'] -= 1
 444									sub_positive_log_negative = 0
 445								del self._vault['account'][x['account']]['log'][x['ref']]
 446
 447				case Action.SUB:
 448					if x['account'] is not None:
 449						if self.account_exists(x['account']):
 450							if x['ref'] in self._vault['account'][x['account']]['box']:
 451								if dry:
 452									continue
 453								self._vault['account'][x['account']]['box'][x['ref']]['rest'] += x['value']
 454								self._vault['account'][x['account']]['balance'] += x['value']
 455								sub_positive_log_negative = x['value']
 456
 457				case Action.ADD_FILE:
 458					if x['account'] is not None:
 459						if self.account_exists(x['account']):
 460							if x['ref'] in self._vault['account'][x['account']]['log']:
 461								if x['file'] in self._vault['account'][x['account']]['log'][x['ref']]['file']:
 462									if dry:
 463										continue
 464									del self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']]
 465
 466				case Action.REMOVE_FILE:
 467					if x['account'] is not None:
 468						if self.account_exists(x['account']):
 469							if x['ref'] in self._vault['account'][x['account']]['log']:
 470								if dry:
 471									continue
 472								self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']] = x['value']
 473
 474				case Action.REPORT:
 475					if x['ref'] in self._vault['report']:
 476						if dry:
 477							continue
 478						del self._vault['report'][x['ref']]
 479
 480				case Action.ZAKAT:
 481					if x['account'] is not None:
 482						if self.account_exists(x['account']):
 483							if x['ref'] in self._vault['account'][x['account']]['box']:
 484								if x['key'] in self._vault['account'][x['account']]['box'][x['ref']]:
 485									if dry:
 486										continue
 487									match x['math']:
 488										case MathOperation.ADDITION:
 489											self._vault['account'][x['account']]['box'][x['ref']][x['key']] -= x['value']
 490										case MathOperation.EQUAL:
 491											self._vault['account'][x['account']]['box'][x['ref']][x['key']] = x['value']
 492										case MathOperation.SUBTRACTION:
 493											self._vault['account'][x['account']]['box'][x['ref']][x['key']] += x['value']
 494
 495		if not dry:
 496			del self._vault['history'][ref]
 497		return True
 498
 499	def track(self, value: int = 0, desc: str = '', account: str = 1, logging: bool = True, created: int = None, debug: bool = False) -> int:
 500		"""
 501        This function tracks a transaction for a specific account.
 502
 503        Parameters:
 504        value (int): The value of the transaction. Default is 0.
 505        desc (str): The description of the transaction. Default is an empty string.
 506        account (str): The account for which the transaction is being tracked. Default is '1'.
 507        logging (bool): Whether to log the transaction. Default is True.
 508        created (int): The timestamp of the transaction. If not provided, it will be generated. Default is None.
 509        debug (bool): Whether to print debug information. Default is False.
 510
 511        Returns:
 512        int: The timestamp of the transaction.
 513
 514        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.
 515
 516		Raises:
 517		ValueError: If the transction happend again in the same time.
 518        """
 519		if created is None:
 520			created = ZakatTracker.time()
 521		_nolock = self.nolock(); self.lock()
 522		if not self.account_exists(account):
 523			if debug:
 524				print(f"account {account} created")
 525			self._vault['account'][account] = {
 526				'balance': 0,
 527				'box': {},
 528				'count': 0,
 529				'log': {},
 530				'zakatable': True,
 531			}
 532			self._step(Action.CREATE, account)
 533		if value == 0:
 534			if _nolock: self.free(self.lock())
 535			return 0
 536		if logging:
 537			self._log(value, desc, account, created)
 538		assert created not in self._vault['account'][account]['box']
 539		self._vault['account'][account]['box'][created] = {
 540			'capital': value,
 541			'count': 0,
 542			'last': 0,
 543			'rest': value,
 544			'total': 0,
 545		}
 546		self._step(Action.TRACK, account, ref=created, value=value)
 547		if _nolock: self.free(self.lock())
 548		return created
 549
 550	def _log(self, value: int, desc: str = '', account: str = 1, created: int = None) -> int:
 551		"""
 552		Log a transaction into the account's log.
 553
 554		Parameters:
 555		value (int): The value of the transaction.
 556		desc (str): The description of the transaction.
 557		account (str): The account to log the transaction into. Default is '1'.
 558		created (int): The timestamp of the transaction. If not provided, it will be generated.
 559
 560		Returns:
 561		int: The timestamp of the logged transaction.
 562
 563		This method updates the account's balance, count, and log with the transaction details.
 564		It also creates a step in the history of the transaction.
 565
 566		Raises:
 567		ValueError: If the transction happend again in the same time.
 568		"""
 569		if created is None:
 570			created = ZakatTracker.time()
 571		self._vault['account'][account]['balance'] += value
 572		self._vault['account'][account]['count'] += 1
 573		assert created not in self._vault['account'][account]['log']
 574		self._vault['account'][account]['log'][created] = {
 575			'value': value,
 576			'desc': desc,
 577			'file': {},
 578		}
 579		self._step(Action.LOG, account, ref=created, value=value)
 580		return created
 581
 582	def accounts(self) -> dict:
 583		"""
 584        Returns a dictionary containing account numbers as keys and their respective balances as values.
 585
 586        Parameters:
 587        None
 588
 589        Returns:
 590        dict: A dictionary where keys are account numbers and values are their respective balances.
 591        """
 592		result = {}
 593		for i in self._vault['account']:
 594			result[i] = self._vault['account'][i]['balance']
 595		return result
 596
 597	def boxes(self, account) -> dict:
 598		"""
 599        Retrieve the boxes (transactions) associated with a specific account.
 600
 601        Parameters:
 602        account (str): The account number for which to retrieve the boxes.
 603
 604        Returns:
 605        dict: A dictionary containing the boxes associated with the given account.
 606        If the account does not exist, an empty dictionary is returned.
 607        """
 608		if self.account_exists(account):
 609			return self._vault['account'][account]['box']
 610		return {}
 611
 612	def logs(self, account) -> dict:
 613		"""
 614		Retrieve the logs (transactions) associated with a specific account.
 615
 616		Parameters:
 617		account (str): The account number for which to retrieve the logs.
 618
 619		Returns:
 620		dict: A dictionary containing the logs associated with the given account.
 621		If the account does not exist, an empty dictionary is returned.
 622		"""
 623		if self.account_exists(account):
 624			return self._vault['account'][account]['log']
 625		return {}
 626
 627	def add_file(self, account: str, ref: int, path: str) -> int:
 628		"""
 629		Adds a file reference to a specific transaction log entry in the vault.
 630
 631		Parameters:
 632		account (str): The account number associated with the transaction log.
 633		ref (int): The reference to the transaction log entry.
 634		path (str): The path of the file to be added.
 635
 636		Returns:
 637		int: The reference of the added file. If the account or transaction log entry does not exist, returns 0.
 638		"""
 639		if self.account_exists(account):
 640			if ref in self._vault['account'][account]['log']:
 641				file_ref = ZakatTracker.time()
 642				self._vault['account'][account]['log'][ref]['file'][file_ref] = path
 643				_nolock = self.nolock(); self.lock()
 644				self._step(Action.ADD_FILE, account, ref=ref, file=file_ref)
 645				if _nolock: self.free(self.lock())
 646				return file_ref
 647		return 0
 648
 649	def remove_file(self, account: str, ref: int, file_ref: int) -> bool:
 650		"""
 651		Removes a file reference from a specific transaction log entry in the vault.
 652
 653		Parameters:
 654		account (str): The account number associated with the transaction log.
 655		ref (int): The reference to the transaction log entry.
 656		file_ref (int): The reference of the file to be removed.
 657
 658		Returns:
 659		bool: True if the file reference is successfully removed, False otherwise.
 660		"""
 661		if self.account_exists(account):
 662			if ref in self._vault['account'][account]['log']:
 663				if file_ref in self._vault['account'][account]['log'][ref]['file']:
 664					x = self._vault['account'][account]['log'][ref]['file'][file_ref]
 665					del self._vault['account'][account]['log'][ref]['file'][file_ref]
 666					_nolock = self.nolock(); self.lock()
 667					self._step(Action.REMOVE_FILE, account, ref=ref, file=file_ref, value=x)
 668					if _nolock: self.free(self.lock())
 669					return True
 670		return False
 671
 672	def balance(self, account: str = 1, cached: bool = True) -> int:
 673		"""
 674		Calculate and return the balance of a specific account.
 675
 676		Parameters:
 677		account (str): The account number. Default is '1'.
 678		cached (bool): If True, use the cached balance. If False, calculate the balance from the box. Default is True.
 679
 680		Returns:
 681		int: The balance of the account.
 682
 683		Note:
 684		If cached is True, the function returns the cached balance.
 685		If cached is False, the function calculates the balance from the box by summing up the 'rest' values of all box items.
 686		"""
 687		if cached:
 688			return self._vault['account'][account]['balance']
 689		x = 0
 690		return [x := x + y['rest'] for y in self._vault['account'][account]['box'].values()][-1]
 691
 692	def zakatable(self, account, status: bool = None) -> bool:
 693		"""
 694        Check or set the zakatable status of a specific account.
 695
 696        Parameters:
 697        account (str): The account number.
 698        status (bool, optional): The new zakatable status. If not provided, the function will return the current status.
 699
 700        Returns:
 701        bool: The current or updated zakatable status of the account.
 702
 703        Raises:
 704        None
 705
 706        Example:
 707        >>> tracker = ZakatTracker()
 708        >>> tracker.zakatable('account1', True)  # Set the zakatable status of 'account1' to True
 709        True
 710        >>> tracker.zakatable('account1')  # Get the zakatable status of 'account1'
 711        True
 712        """
 713		if self.account_exists(account):
 714			if status is None:
 715				return self._vault['account'][account]['zakatable']
 716			self._vault['account'][account]['zakatable'] = status
 717			return status
 718		return False
 719
 720	def sub(self, x: int, desc: str = '', account: str = 1, created: int = None, debug: bool = False) -> int:
 721		"""
 722		Subtracts a specified amount from the account's balance.
 723
 724		Parameters:
 725		x (int): The amount to subtract.
 726		desc (str): The description of the transaction.
 727		account (str): The account from which to subtract.
 728		created (int): The timestamp of the transaction.
 729		debug (bool): A flag to enable debug mode.
 730
 731		Returns:
 732		int: The timestamp of the transaction.
 733
 734		If the amount to subtract is greater than the account's balance,
 735		the remaining amount will be transferred to a new transaction with a negative value.
 736
 737		Raises:
 738		ValueError: If the transction happend again in the same time.
 739		"""
 740		if x < 0:
 741			return
 742		if x == 0:
 743			return self.track(x, '', account)
 744		if created is None:
 745			created = ZakatTracker.time()
 746		_nolock = self.nolock(); self.lock()
 747		self.track(0, '', account)
 748		self._log(-x, desc, account, created)
 749		ids = sorted(self._vault['account'][account]['box'].keys())
 750		limit = len(ids) + 1
 751		target = x
 752		if debug:
 753			print('ids', ids)
 754		ages = []
 755		for i in range(-1, -limit, -1):
 756			if target == 0:
 757				break
 758			j = ids[i]
 759			if debug:
 760				print('i', i, 'j', j)
 761			if self._vault['account'][account]['box'][j]['rest'] >= target:
 762				self._vault['account'][account]['box'][j]['rest'] -= target
 763				self._step(Action.SUB, account, ref=j, value=target)
 764				ages.append((j, target))
 765				target = 0
 766				break
 767			elif self._vault['account'][account]['box'][j]['rest'] < target and self._vault['account'][account]['box'][j]['rest'] > 0:
 768				chunk = self._vault['account'][account]['box'][j]['rest']
 769				target -= chunk
 770				self._step(Action.SUB, account, ref=j, value=chunk)
 771				ages.append((j, chunk))
 772				self._vault['account'][account]['box'][j]['rest'] = 0
 773		if target > 0:
 774			self.track(-target, desc, account, False, created)
 775			ages.append((created, target))
 776		if _nolock: self.free(self.lock())
 777		return (created, ages)
 778
 779	def transfer(self, value: int, from_account: str, to_account: str, desc: str = '', created: int = None) -> list[int]:
 780		"""
 781		Transfers a specified value from one account to another.
 782
 783		Parameters:
 784		value (int): The amount to be transferred.
 785		from_account (str): The account from which the value will be transferred.
 786		to_account (str): The account to which the value will be transferred.
 787		desc (str, optional): A description for the transaction. Defaults to an empty string.
 788		created (int, optional): The timestamp of the transaction. If not provided, the current timestamp will be used.
 789
 790		Returns:
 791		list[int]: A list of timestamps corresponding to the transactions made during the transfer.
 792
 793		Raises:
 794		ValueError: If the transction happend again in the same time.
 795		"""
 796		if created is None:
 797			created = ZakatTracker.time()
 798		(_, ages) = self.sub(value, desc, from_account, created)
 799		times = []
 800		for age in ages:
 801			y = self.track(age[1], desc, to_account, logging=True, created=age[0])
 802			times.append(y)
 803		return times
 804
 805	def check(self, silver_gram_price: float, nisab: float = None, debug: bool = False, now: int = None, cycle: float = None) -> tuple:
 806		"""
 807		Check the eligibility for Zakat based on the given parameters.
 808
 809		Parameters:
 810		silver_gram_price (float): The price of a gram of silver.
 811		nisab (float): The minimum amount of wealth required for Zakat. If not provided, it will be calculated based on the silver_gram_price.
 812		debug (bool): Flag to enable debug mode.
 813		now (int): The current timestamp. If not provided, it will be calculated using ZakatTracker.time().
 814		cycle (float): The time cycle for Zakat. If not provided, it will be calculated using ZakatTracker.TimeCycle().
 815
 816		Returns:
 817		tuple: A tuple containing a boolean indicating the eligibility for Zakat, a list of brief statistics, and a dictionary containing the Zakat plan.
 818		"""
 819		if now is None:
 820			now = ZakatTracker.time()
 821		if cycle is None:
 822			cycle = ZakatTracker.TimeCycle()
 823		if nisab is None:
 824			nisab = ZakatTracker.Nisab(silver_gram_price)
 825		plan = {}
 826		below_nisab = 0
 827		brief = [0, 0, 0]
 828		valid = False
 829		for x in self._vault['account']:
 830			if not self._vault['account'][x]['zakatable']:
 831				continue
 832			_box = self._vault['account'][x]['box']
 833			limit = len(_box) + 1
 834			ids = sorted(self._vault['account'][x]['box'].keys())
 835			for i in range(-1, -limit, -1):
 836				j = ids[i]
 837				if _box[j]['rest'] <= 0:
 838					continue
 839				brief[0] += _box[j]['rest']
 840				index = limit + i - 1
 841				epoch = (now - j) / cycle
 842				if debug:
 843					print(f"Epoch: {epoch}", _box[j])
 844				if _box[j]['last'] > 0:
 845					epoch = (now - _box[j]['last']) / cycle
 846				if debug:
 847					print(f"Epoch: {epoch}")
 848				epoch = floor(epoch)
 849				if debug:
 850					print(f"Epoch: {epoch}", type(epoch), epoch == 0, 1-epoch, epoch)
 851				if epoch == 0:
 852					continue
 853				if debug:
 854					print("Epoch - PASSED")
 855				brief[1] += _box[j]['rest']
 856				if _box[j]['rest'] >= nisab:
 857					total = 0
 858					for _ in range(epoch):
 859						total += ZakatTracker.ZakatCut(_box[j]['rest'] - total)
 860					if total > 0:
 861						if x not in plan:
 862							plan[x] = {}
 863						valid = True
 864						brief[2] += total
 865						plan[x][index] = {'total': total, 'count': epoch}
 866				else:
 867					chunk = ZakatTracker.ZakatCut(_box[j]['rest'])
 868					if chunk > 0:
 869						if x not in plan:
 870							plan[x] = {}
 871						if j not in plan[x].keys():
 872							plan[x][index] = {}
 873						below_nisab += _box[j]['rest']
 874						brief[2] += chunk
 875						plan[x][index]['below_nisab'] = chunk
 876		valid = valid or below_nisab >= nisab
 877		if debug:
 878			print(f"below_nisab({below_nisab}) >= nisab({nisab})")
 879		return (valid, brief, plan)
 880
 881	def build_payment_parts(self, demand: float, positive_only: bool = True) -> dict:
 882		"""
 883		Build payment parts for the zakat distribution.
 884
 885		Parameters:
 886		demand (float): The total demand for payment.
 887		positive_only (bool): If True, only consider accounts with positive balance. Default is True.
 888
 889		Returns:
 890		dict: A dictionary containing the payment parts for each account. The dictionary has the following structure:
 891		{
 892			'account': {
 893				'account_id': {'balance': float, 'part': float},
 894				...
 895			},
 896			'exceed': bool,
 897			'demand': float,
 898			'total': float,
 899		}
 900		"""
 901		total = 0
 902		parts = {
 903			'account': {},
 904			'exceed': False,
 905			'demand': demand,
 906		}
 907		for x, y in self.accounts().items():
 908			if positive_only and y <= 0:
 909				continue
 910			total += y
 911			parts['account'][x] = {'balance': y, 'part': 0}
 912		parts['total'] = total
 913		return parts
 914
 915	def check_payment_parts(self, parts: dict) -> int:
 916		"""
 917        Checks the validity of payment parts.
 918
 919        Parameters:
 920        parts (dict): A dictionary containing payment parts information.
 921
 922        Returns:
 923        int: Returns 0 if the payment parts are valid, otherwise returns the error code.
 924
 925        Error Codes:
 926        1: 'demand', 'account', 'total', or 'exceed' key is missing in parts.
 927        2: 'balance' or 'part' key is missing in parts['account'][x].
 928        3: 'part' value in parts['account'][x] is less than or equal to 0.
 929        4: If 'exceed' is False, 'balance' value in parts['account'][x] is less than or equal to 0.
 930        5: 'part' value in parts['account'][x] is less than 0.
 931        6: If 'exceed' is False, 'part' value in parts['account'][x] is greater than 'balance' value.
 932        7: The sum of 'part' values in parts['account'] does not match with 'demand' value.
 933        """
 934		for i in ['demand', 'account', 'total', 'exceed']:
 935			if not i in parts:
 936				return 1
 937		exceed = parts['exceed']
 938		for x in parts['account']:
 939			for j in ['balance', 'part']:
 940				if not j in parts['account'][x]:
 941					return 2
 942				if parts['account'][x]['part'] <= 0:
 943					return 3
 944				if not exceed and parts['account'][x]['balance'] <= 0:
 945					return 4
 946		demand = parts['demand']
 947		z = 0
 948		for _, y in parts['account'].items():
 949			if y['part'] < 0:
 950				return 5
 951			if not exceed and y['part'] > y['balance']:
 952				return 6
 953			z += y['part']
 954		if z != demand:
 955			return 7
 956		return 0
 957
 958	def zakat(self, report: tuple, parts: dict = None, debug: bool = False) -> bool:
 959		"""
 960		Perform Zakat calculation based on the given report and optional parts.
 961
 962		Parameters:
 963		report (tuple): A tuple containing the validity of the report, the report data, and the zakat plan.
 964		parts (dict): A dictionary containing the payment parts for the zakat.
 965		debug (bool): A flag indicating whether to print debug information.
 966
 967		Returns:
 968		bool: True if the zakat calculation is successful, False otherwise.
 969		"""
 970		(valid, _, plan) = report
 971		if not valid:
 972			return valid
 973		parts_exist = parts is not None
 974		if parts_exist:
 975			for part in parts:
 976				if self.check_payment_parts(part) != 0:
 977					return False
 978		if debug:
 979			print('######### zakat #######')
 980			print('parts_exist', parts_exist)
 981		_nolock = self.nolock(); self.lock()
 982		report_time = ZakatTracker.time()
 983		self._vault['report'][report_time] = report
 984		self._step(Action.REPORT, ref=report_time)
 985		created = ZakatTracker.time()
 986		for x in plan:
 987			if debug:
 988				print(plan[x])
 989				print('-------------')
 990				print(self._vault['account'][x]['box'])
 991			ids = sorted(self._vault['account'][x]['box'].keys())
 992			if debug:
 993				print('plan[x]', plan[x])
 994			for i in plan[x].keys():
 995				j = ids[i]
 996				if debug:
 997					print('i', i, 'j', j)
 998				self._step(Action.ZAKAT, x, j, value=self._vault['account'][x]['box'][j]['last'], key='last', math_operation=MathOperation.EQUAL)
 999				self._vault['account'][x]['box'][j]['last'] = created
1000				self._vault['account'][x]['box'][j]['total'] += plan[x][i]['total']
1001				self._step(Action.ZAKAT, x, j, value=plan[x][i]['total'], key='total', math_operation=MathOperation.ADDITION)
1002				self._vault['account'][x]['box'][j]['count'] += plan[x][i]['count']
1003				self._step(Action.ZAKAT, x, j, value=plan[x][i]['count'], key='count', math_operation=MathOperation.ADDITION)
1004				if not parts_exist:
1005					self._vault['account'][x]['box'][j]['rest'] -= plan[x][i]['total']
1006					self._step(Action.ZAKAT, x, j, value=plan[x][i]['total'], key='rest', math_operation=MathOperation.SUBTRACTION)
1007		if parts_exist:
1008			for transaction in parts:
1009				for account, part in transaction['account'].items():
1010					if debug:
1011						print('zakat-part', account, part['part'])
1012					self.sub(part['part'], 'zakat-part', account, debug=debug)
1013		if _nolock: self.free(self.lock())
1014		return True
1015
1016	def export_json(self, path: str = "data.json") -> bool:
1017		"""
1018        Exports the current state of the ZakatTracker object to a JSON file.
1019
1020        Parameters:
1021        path (str): The path where the JSON file will be saved. Default is "data.json".
1022
1023        Returns:
1024        bool: True if the export is successful, False otherwise.
1025
1026        Raises:
1027        No specific exceptions are raised by this method.
1028        """
1029		with open(path, "w") as file:
1030			json.dump(self._vault, file, indent=4, cls=JSONEncoder)
1031			return True
1032		return False
1033
1034	def save(self, path: str = None) -> bool:
1035		"""
1036		Save the current state of the ZakatTracker object to a pickle file.
1037
1038		Parameters:
1039		path (str): The path where the pickle file will be saved. If not provided, it will use the default path.
1040
1041		Returns:
1042		bool: True if the save operation is successful, False otherwise.
1043		"""
1044		if path is None:
1045			path = self.path()
1046		with open(path, "wb") as f:
1047			pickle.dump(self._vault, f)
1048			return True
1049		return False
1050
1051	def load(self, path: str = None) -> bool:
1052		"""
1053		Load the current state of the ZakatTracker object from a pickle file.
1054
1055		Parameters:
1056		path (str): The path where the pickle file is located. If not provided, it will use the default path.
1057
1058		Returns:
1059		bool: True if the load operation is successful, False otherwise.
1060		"""
1061		if path is None:
1062			path = self.path()
1063		if os.path.exists(path):
1064			with open(path, "rb") as f:
1065				self._vault = pickle.load(f)
1066				return True
1067		return False
1068
1069	def import_csv(self, path: str = 'file.csv') -> tuple:
1070		"""
1071        Import transactions from a CSV file.
1072
1073        Parameters:
1074        path (str): The path to the CSV file. Default is 'file.csv'.
1075
1076        Returns:
1077        tuple: A tuple containing the number of transactions created, the number of transactions found in the cache, and a dictionary of bad transactions.
1078
1079        The CSV file should have the following format:
1080        account, desc, value, date
1081        For example:
1082        safe-45, "Some text", 34872, 1988-06-30 00:00:00
1083
1084        The function reads the CSV file, checks for duplicate transactions, and creates the transactions in the system.
1085        """
1086		cache = []
1087		tmp = "tmp"
1088		try:
1089			with open(tmp, "rb") as f:
1090				cache = pickle.load(f)
1091		except:
1092			pass
1093		date_formats = [
1094			"%Y-%m-%d %H:%M:%S",
1095			"%Y-%m-%dT%H:%M:%S",
1096			"%Y-%m-%dT%H%M%S",
1097			"%Y-%m-%d",
1098		]
1099		created, found, bad = 0, 0, {}
1100		with open(path, newline='', encoding="utf-8") as f:
1101			i = 0
1102			for row in csv.reader(f, delimiter=','):
1103				i += 1
1104				hashed = hash(tuple(row))
1105				if hashed in cache:
1106					found += 1
1107					continue
1108				account = row[0]
1109				desc = row[1]
1110				value = float(row[2])
1111				date = 0
1112				for time_format in date_formats:
1113					try:
1114						date = self.time(datetime.datetime.strptime(row[3], time_format))
1115						break
1116					except:
1117						pass
1118				# TODO: not allowed for negative dates
1119				if date == 0 or value == 0:
1120					bad[i] = row
1121					continue
1122				if value > 0:
1123					self.track(value, desc, account, True, date)
1124				elif value < 0:
1125					self.sub(-value, desc, account, date)
1126				created += 1
1127				cache.append(hashed)
1128		with open(tmp, "wb") as f:
1129				pickle.dump(cache, f)
1130		return (created, found, bad)
1131
1132	########
1133	# TESTS #
1134	#######
1135
1136	@staticmethod
1137	def DurationFromNanoSeconds(ns: int) -> tuple:
1138		"""REF https://github.com/JayRizzo/Random_Scripts/blob/master/time_measure.py#L106
1139			Convert NanoSeconds to Human Readable Time Format.
1140			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.
1141			Its symbol is μs, sometimes simplified to us when Unicode is not available.
1142			A microsecond is equal to 1000 nanoseconds or 1⁄1,000 of a millisecond.
1143
1144			INPUT : ms (AKA: MilliSeconds)
1145			OUTPUT: tuple(string TIMELAPSED, string SPOKENTIME) like format.
1146			OUTPUT Variables: TIMELAPSED, SPOKENTIME
1147
1148			Example  Input: DurationFromNanoSeconds(ns)
1149			**"Millennium:Century:Years:Days:Hours:Minutes:Seconds:MilliSeconds:MicroSeconds:NanoSeconds"**
1150			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')
1151			DurationFromNanoSeconds(1234567890123456789012)
1152		"""
1153		μs, ns      = divmod(ns, 1000)
1154		ms, μs      = divmod(μs, 1000)
1155		s, ms       = divmod(ms, 1000)
1156		m, s        = divmod(s, 60)
1157		h, m        = divmod(m, 60)
1158		d, h        = divmod(h, 24)
1159		y, d        = divmod(d, 365)
1160		c, y        = divmod(y, 100)
1161		n, c        = divmod(c, 10)
1162		TIMELAPSED  = 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}::{μs:03.0f}::{ns:03.0f}"
1163		SPOKENTIME  = 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, {μs: 3d} MicroSeconds, {ns: 3d} NanoSeconds"
1164		return TIMELAPSED, SPOKENTIME
1165
1166	@staticmethod
1167	def generate_random_date(start_date: datetime.datetime, end_date: datetime.datetime) -> datetime.datetime:
1168		"""
1169		Generate a random date between two given dates.
1170
1171		Parameters:
1172		start_date (datetime.datetime): The start date from which to generate a random date.
1173		end_date (datetime.datetime): The end date until which to generate a random date.
1174
1175		Returns:
1176		datetime.datetime: A random date between the start_date and end_date.
1177		"""
1178		time_between_dates = end_date - start_date
1179		days_between_dates = time_between_dates.days
1180		random_number_of_days = random.randrange(days_between_dates)
1181		return start_date + datetime.timedelta(days=random_number_of_days)
1182
1183	@staticmethod
1184	def generate_random_csv_file(path: str = "data.csv", count: int = 1000) -> None:
1185		"""
1186		Generate a random CSV file with specified parameters.
1187
1188		Parameters:
1189		path (str): The path where the CSV file will be saved. Default is "data.csv".
1190		count (int): The number of rows to generate in the CSV file. Default is 1000.
1191
1192		Returns:
1193		None. The function generates a CSV file at the specified path with the given count of rows.
1194		Each row contains a randomly generated account, description, value, and date.
1195		The value is randomly generated between 1000 and 100000, and the date is randomly generated between 1950-01-01 and 2023-12-31.
1196		If the row number is not divisible by 13, the value is multiplied by -1.
1197		"""
1198		with open(path, "w", newline="") as csvfile:
1199			writer = csv.writer(csvfile)
1200			for i in range(count):
1201				account = f"acc-{random.randint(1, 1000)}"
1202				desc = f"Some text {random.randint(1, 1000)}"
1203				value = random.randint(1000, 100000)
1204				date = ZakatTracker.generate_random_date(datetime.datetime(1950, 1, 1), datetime.datetime(2023, 12, 31)).strftime("%Y-%m-%d %H:%M:%S")
1205				if not i % 13 == 0:
1206					value *= -1
1207				writer.writerow([account, desc, value, date])
1208
1209	def _test_core(self, restore = False, debug = False):
1210		
1211		random.seed(1234567890)
1212		
1213		# sanity check - random forward time
1214		
1215		xlist = []
1216		limit = 1000
1217		for _ in range(limit):
1218			y = ZakatTracker.time()
1219			z = '-'
1220			if not y in xlist:
1221				xlist.append(y)
1222			else:
1223				z = 'x'
1224			if debug:
1225				print(z, y)
1226		xx = len(xlist)
1227		if debug:
1228			print('count', xx, ' - unique: ', (xx/limit)*100, '%')
1229		assert limit == xx
1230		
1231		# sanity check - convert date since 1000AD
1232		
1233		for year in range(1000, 9000):
1234			ns = ZakatTracker.time(datetime.datetime.strptime(f"{year}-12-30 18:30:45","%Y-%m-%d %H:%M:%S"))
1235			date = ZakatTracker.time_to_datetime(ns)
1236			if debug:
1237				print(date)
1238			assert date.year == year
1239			assert date.month == 12
1240			assert date.day == 30
1241			assert date.hour == 18
1242			assert date.minute == 30
1243			assert date.second in [44, 45]
1244		assert self.nolock()
1245
1246		assert self._history() is True
1247		
1248		table = {
1249			1: [
1250				(0,  10,   10,   10,   10, 1, 1),
1251				(0,  20,   30,   30,   30, 2, 2),
1252				(0,  30,   60,   60,   60, 3, 3),
1253				(1,  15,   45,   45,   45, 3, 4),
1254				(1,  50,   -5,   -5,   -5, 4, 5),
1255				(1, 100, -105, -105, -105, 5, 6),
1256			],
1257			'wallet': [
1258				(1,   90,  -90,  -90,  -90, 1, 1),
1259				(0,  100,   10,   10,   10, 2, 2),
1260				(1,  190, -180, -180, -180, 3, 3),
1261				(0, 1000,  820,  820,  820, 4, 4),
1262			],
1263		}
1264		for x in table:
1265			for y in table[x]:
1266				self.lock()
1267				ref = 0
1268				if y[0] == 0:
1269					ref = self.track(y[1], 'test-add', x, True, ZakatTracker.time(), debug)
1270				else:
1271					(ref, z) = self.sub(y[1], 'test-sub', x, ZakatTracker.time())
1272					if debug:
1273						print('_sub', z, ZakatTracker.time())
1274				assert ref != 0
1275				assert len(self._vault['account'][x]['log'][ref]['file']) == 0
1276				for i in range(3):
1277					file_ref = self.add_file(x, ref, 'file_' + str(i))
1278					sleep(0.0000001)
1279					assert file_ref != 0
1280					if debug:
1281						print('ref', ref, 'file', file_ref)
1282					assert len(self._vault['account'][x]['log'][ref]['file']) == i+1
1283				file_ref = self.add_file(x, ref, 'file_' + str(3))
1284				assert self.remove_file(x, ref, file_ref)
1285				assert self.balance(x) == y[2]
1286				z = self.balance(x, False)
1287				if debug:
1288					print("debug-1", z, y[3])
1289				assert z == y[3]
1290				l = self._vault['account'][x]['log']
1291				z = 0
1292				for i in l:
1293					z += l[i]['value']
1294				if debug:
1295					print("debug-2", z, type(z))
1296					print("debug-2", y[4], type(y[4]))
1297				assert z == y[4]
1298				if debug:
1299					print('debug-2 - PASSED')
1300				assert self.box_size(x) == y[5]
1301				assert self.log_size(x) == y[6]
1302				assert not self.nolock()
1303				self.free(self.lock())
1304				assert self.nolock()
1305			assert self.boxes(x) != {}
1306			assert self.logs(x) != {}
1307
1308			assert self.zakatable(x)
1309			assert self.zakatable(x, False) is False
1310			assert self.zakatable(x) is False
1311			assert self.zakatable(x, True)
1312			assert self.zakatable(x)
1313
1314		if restore is True:
1315			count = len(self._vault['history'])
1316			if debug:
1317				print('history-count', count)
1318			assert count == 10
1319			# try mode
1320			for _ in range(count):
1321				assert self.recall(True, debug)
1322			count = len(self._vault['history'])
1323			if debug:
1324				print('history-count', count)
1325			assert count == 10
1326			_accounts = list(table.keys())
1327			accounts_limit = len(_accounts) + 1
1328			for i in range(-1, -accounts_limit, -1):
1329				account = _accounts[i]
1330				if debug:
1331					print(account, len(table[account]))
1332				transaction_limit = len(table[account]) + 1
1333				for j in range(-1, -transaction_limit, -1):
1334					row = table[account][j]
1335					if debug:
1336						print(row, self.balance(account), self.balance(account, False))
1337					assert self.balance(account) == self.balance(account, False)
1338					assert self.balance(account) == row[2]
1339					assert self.recall(False, debug)
1340			assert self.recall(False, debug) is False
1341			count = len(self._vault['history'])
1342			if debug:
1343				print('history-count', count)
1344			assert count == 0
1345			self.reset()
1346
1347	def test(self, debug: bool = False):
1348
1349		try:
1350
1351			assert self._history()
1352
1353			# Not allowed for duplicate transactions in the same account and time
1354
1355			created = ZakatTracker.time()
1356			self.track(100, 'test-1', 'same', True, created)
1357			failed = False
1358			try:
1359				self.track(50, 'test-1', 'same', True, created)
1360			except:
1361				failed = True
1362			assert failed is True
1363
1364			self.reset()
1365
1366			# Always preserve box age during transfer
1367
1368			created = ZakatTracker.time()
1369			series = [
1370				(30, 4),
1371				(60, 3),
1372				(90, 2),
1373			]
1374			case = {
1375				30: {
1376					'series': series,
1377					'rest' : 150,
1378				},
1379				60: {
1380					'series': series,
1381					'rest' : 120,
1382				},
1383				90: {
1384					'series': series,
1385					'rest' : 90,
1386				},
1387				180: {
1388					'series': series,
1389					'rest' : 0,
1390				},
1391				270: {
1392					'series': series,
1393					'rest' : -90,
1394				},
1395				360: {
1396					'series': series,
1397					'rest' : -180,
1398				},
1399			}
1400
1401			selected_time = ZakatTracker.time() - ZakatTracker.TimeCycle()
1402				
1403			for total in case:
1404				for x in case[total]['series']:
1405					self.track(x[0], f"test-{x} ages", 'ages', True, selected_time * x[1])
1406
1407				refs = self.transfer(total, 'ages', 'future', 'Zakat Movement')
1408
1409				if debug:
1410					print('refs', refs)
1411
1412				ages_cache_balance = self.balance('ages')
1413				ages_fresh_balance = self.balance('ages', False)
1414				rest = case[total]['rest']
1415				if debug:
1416					print('source', ages_cache_balance, ages_fresh_balance, rest)
1417				assert ages_cache_balance == rest
1418				assert ages_fresh_balance == rest
1419
1420				future_cache_balance = self.balance('future')
1421				future_fresh_balance = self.balance('future', False)
1422				if debug:
1423					print('target', future_cache_balance, future_fresh_balance, total)
1424					print('refs', refs)
1425				assert future_cache_balance == total
1426				assert future_fresh_balance == total
1427
1428				for ref in self._vault['account']['ages']['box']:
1429					ages_capital = self._vault['account']['ages']['box'][ref]['capital']
1430					ages_rest = self._vault['account']['ages']['box'][ref]['rest']
1431					future_capital = 0
1432					if ref in self._vault['account']['future']['box']:
1433						future_capital = self._vault['account']['future']['box'][ref]['capital']
1434					future_rest = 0
1435					if ref in self._vault['account']['future']['box']:
1436						future_rest = self._vault['account']['future']['box'][ref]['rest']
1437					if ages_capital != 0 and future_capital != 0 and future_rest != 0:
1438						if debug:
1439							print('================================================================')
1440							print('ages', ages_capital, ages_rest)
1441							print('future', future_capital, future_rest)
1442						if ages_rest == 0:
1443							assert ages_capital == future_capital
1444						elif ages_rest < 0:
1445							assert -ages_capital == future_capital
1446						elif ages_rest > 0:
1447							assert ages_capital == ages_rest + future_capital
1448				self.reset()
1449				assert len(self._vault['history']) == 0
1450
1451			assert self._history()
1452			assert self._history(False) is False
1453			assert self._history() is False
1454			assert self._history(True)
1455			assert self._history()
1456
1457			self._test_core(True, debug)
1458			self._test_core(False, debug)
1459
1460			transaction = [
1461				(
1462					20, 'wallet', 1, 800, 800, 800, 4, 5,
1463									-85, -85, -85, 6, 7,
1464				),
1465				(
1466					750, 'wallet', 'safe',  50,   50,  50, 4, 6,
1467											750, 750, 750, 1, 1,
1468				),
1469				(
1470					600, 'safe', 'bank', 150, 150, 150, 1, 2,
1471										600, 600, 600, 1, 1,
1472				),
1473			]
1474			for z in transaction:
1475				self.lock()
1476				x = z[1]
1477				y = z[2]
1478				self.transfer(z[0], x, y, 'test-transfer')
1479				assert self.balance(x) == z[3]
1480				xx = self.accounts()[x]
1481				assert xx == z[3]
1482				assert self.balance(x, False) == z[4]
1483				assert xx == z[4]
1484
1485				l = self._vault['account'][x]['log']
1486				s = 0
1487				for i in l:
1488					s += l[i]['value']
1489				if debug:
1490					print('s', s, 'z[5]', z[5])
1491				assert s == z[5]
1492
1493				assert self.box_size(x) == z[6]
1494				assert self.log_size(x) == z[7]
1495
1496				yy = self.accounts()[y]
1497				assert self.balance(y) == z[8]
1498				assert yy == z[8]
1499				assert self.balance(y, False) == z[9]
1500				assert yy == z[9]
1501
1502				l = self._vault['account'][y]['log']
1503				s = 0
1504				for i in l:
1505					s += l[i]['value']
1506				assert s == z[10]
1507
1508				assert self.box_size(y) == z[11]
1509				assert self.log_size(y) == z[12]
1510
1511			if debug:
1512				pp().pprint(self.check(2.17))
1513
1514			assert not self.nolock()
1515			history_count = len(self._vault['history'])
1516			if debug:
1517				print('history-count', history_count)
1518			assert history_count == 11
1519			assert not self.free(ZakatTracker.time())
1520			assert self.free(self.lock())
1521			assert self.nolock()
1522			assert len(self._vault['history']) == 11
1523
1524			# storage
1525
1526			_path = self.path('test.pickle')
1527			if os.path.exists(_path):
1528				os.remove(_path)
1529			self.save()
1530			assert os.path.getsize(_path) > 0
1531			self.reset()
1532			assert self.recall(False, debug) is False
1533			self.load()
1534			assert self._vault['account'] is not None
1535
1536			# recall
1537
1538			assert self.nolock()
1539			assert len(self._vault['history']) == 11
1540			assert self.recall(False, debug) is True
1541			assert len(self._vault['history']) == 10
1542			assert self.recall(False, debug) is True
1543			assert len(self._vault['history']) == 9
1544
1545			# csv
1546			
1547			_path = "test.csv"
1548			count = 1000
1549			if os.path.exists(_path):
1550				os.remove(_path)
1551			self.generate_random_csv_file(_path, count)
1552			assert os.path.getsize(_path) > 0
1553			tmp = "tmp"
1554			if os.path.exists(tmp):
1555				os.remove(tmp)
1556			(created, found, bad) = self.import_csv(_path)
1557			bad_count = len(bad)
1558			if debug:
1559				print(f"csv-imported: ({created}, {found}, {bad_count})")
1560			tmp_size = os.path.getsize(tmp)
1561			assert tmp_size > 0
1562			assert created + found + bad_count == count
1563			assert created == count
1564			assert bad_count == 0
1565			(created_2, found_2, bad_2) = self.import_csv(_path)
1566			bad_2_count = len(bad_2)
1567			if debug:
1568				print(f"csv-imported: ({created_2}, {found_2}, {bad_2_count})")
1569				print(bad)
1570			assert tmp_size == os.path.getsize(tmp)
1571			assert created_2 + found_2 + bad_2_count == count
1572			assert created == found_2
1573			assert bad_count == bad_2_count
1574			assert found_2 == count
1575			assert bad_2_count == 0
1576			assert created_2 == 0
1577
1578			# payment parts
1579
1580			positive_parts = self.build_payment_parts(100, positive_only=True)
1581			assert self.check_payment_parts(positive_parts) != 0
1582			assert self.check_payment_parts(positive_parts) != 0
1583			all_parts = self.build_payment_parts(300, positive_only= False)
1584			assert self.check_payment_parts(all_parts) != 0
1585			assert self.check_payment_parts(all_parts) != 0
1586			if debug:
1587				pp().pprint(positive_parts)
1588				pp().pprint(all_parts)
1589			# dynamic discount
1590			suite = []
1591			count = 3
1592			for exceed in [False, True]:
1593				case = []
1594				for parts in [positive_parts, all_parts]:
1595					part = parts.copy()
1596					demand = part['demand']
1597					if debug:
1598						print(demand, part['total'])
1599					i = 0
1600					z = demand / count
1601					cp = {
1602						'account': {},
1603						'demand': demand,
1604						'exceed': exceed,
1605						'total': part['total'],
1606					}
1607					for x, y in part['account'].items():
1608						if exceed and z <= demand:
1609							i += 1
1610							y['part'] = z
1611							if debug:
1612								print(exceed, y)
1613							cp['account'][x] = y
1614							case.append(y)
1615						elif not exceed and y['balance'] >= z:
1616								i += 1
1617								y['part'] = z
1618								if debug:
1619									print(exceed, y)
1620								cp['account'][x] = y
1621								case.append(y)
1622						if i >= count:
1623								break
1624					if len(cp['account'][x]) > 0:
1625						suite.append(cp)
1626			if debug:
1627				print('suite', len(suite))
1628			for case in suite:
1629				if debug:
1630					print(case)
1631				result = self.check_payment_parts(case)
1632				if debug:
1633					print('check_payment_parts', result)
1634				assert result == 0
1635				
1636			report = self.check(2.17, None, debug)
1637			(valid, brief, plan) = report
1638			if debug:
1639				print('valid', valid)
1640			assert self.zakat(report, parts=suite, debug=debug)
1641
1642			assert self.export_json("1000-transactions-test.json")
1643			assert self.save("1000-transactions-test.pickle")
1644
1645			# check & zakat
1646
1647			cases = [
1648				(1000, 'safe', ZakatTracker.time()-ZakatTracker.TimeCycle(), [
1649					{'safe': {0: {'below_nisab': 25}}},
1650				], False),
1651				(2000, 'safe', ZakatTracker.time()-ZakatTracker.TimeCycle(), [
1652					{'safe': {0: {'count': 1, 'total': 50}}},
1653				], True),
1654				(10000, 'cave', ZakatTracker.time()-(ZakatTracker.TimeCycle()*3), [
1655					{'cave': {0: {'count': 3, 'total': 731.40625}}},
1656				], True),
1657			]
1658			for case in cases:
1659				if debug:
1660					print("############# check #############")
1661				self.reset()
1662				self.track(case[0], 'test-check', case[1], True, case[2])
1663
1664				assert self.nolock()
1665				assert len(self._vault['history']) == 1
1666				assert self.lock()
1667				assert not self.nolock()
1668				report = self.check(2.17, None, debug)
1669				(valid, brief, plan) = report
1670				assert valid == case[4]
1671				if debug:
1672					print(brief)
1673				assert case[0] == brief[0]
1674				assert case[0] == brief[1]
1675
1676				if debug:
1677					pp().pprint(plan)
1678
1679				for x in plan:
1680					assert case[1] == x
1681					if 'total' in case[3][0][x][0].keys():
1682						assert case[3][0][x][0]['total'] == brief[2]
1683						assert plan[x][0]['total'] == case[3][0][x][0]['total']
1684						assert plan[x][0]['count'] == case[3][0][x][0]['count']
1685					else:
1686						assert plan[x][0]['below_nisab'] == case[3][0][x][0]['below_nisab']
1687				if debug:
1688					pp().pprint(report)
1689				result = self.zakat(report, debug=debug)
1690				if debug:
1691					print('zakat-result', result, case[4])
1692				assert result == case[4]
1693				report = self.check(2.17, None, debug)
1694				(valid, brief, plan) = report
1695				assert valid is False
1696
1697			assert len(self._vault['history']) == 2
1698			assert not self.nolock()
1699			assert self.recall(False, debug) is False
1700			self.free(self.lock())
1701			assert self.nolock()
1702			assert len(self._vault['history']) == 2
1703			assert self.recall(False, debug) is True
1704			assert len(self._vault['history']) == 1
1705
1706			assert self.nolock()
1707			assert len(self._vault['history']) == 1
1708
1709			assert self.recall(False, debug) is True
1710			assert len(self._vault['history']) == 0
1711
1712			assert len(self._vault['account']) == 0
1713			assert len(self._vault['history']) == 0
1714			assert len(self._vault['report']) == 0
1715			assert self.nolock()
1716		except:
1717			pp().pprint(self._vault)
1718			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.
            - zakatable (bool): Indicates whether the account is subject to Zakat.
    - 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, 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.
ZakatTracker(db_path: str = 'zakat.pickle', history_mode: bool = True)
160	def __init__(self, db_path: str = "zakat.pickle", history_mode: bool = True):
161		"""
162        Initialize ZakatTracker with database path and history mode.
163
164        Parameters:
165        db_path (str): The path to the database file. Default is "zakat.pickle".
166        history_mode (bool): The mode for tracking history. Default is True.
167
168        Returns:
169        None
170        """
171		self.reset()
172		self._history(history_mode)
173		self.path(db_path)
174		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

def ZakatCut(x):
155	ZakatCut	= lambda x: 0.025*x # Zakat Cut in one Lunar Year
def TimeCycle():
156	TimeCycle	= lambda  : int(60*60*24*354.367056*1e9) # Lunar Year in nanoseconds
def Nisab(x):
157	Nisab		= lambda x: 585*x # Silver Price in Local currency value
def Version():
158	Version		= lambda  : '0.2.2'
def path(self, _path: str = None) -> str:
176	def path(self, _path: str = None) -> str:
177		"""
178        Set or get the database path.
179
180        Parameters:
181        _path (str): The path to the database file. If not provided, it returns the current path.
182
183        Returns:
184        str: The current database path.
185        """
186		if _path is not None:
187			self._vault_path = _path
188		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.

def reset(self) -> None:
204	def reset(self) -> None:
205		"""
206        Reset the internal data structure to its initial state.
207
208        Parameters:
209        None
210
211        Returns:
212        None
213        """
214		self._vault = {}
215		self._vault['account'] = {}
216		self._vault['history'] = {}
217		self._vault['lock'] = None
218		self._vault['report'] = {}

Reset the internal data structure to its initial state.

Parameters: None

Returns: None

@staticmethod
def time( now: <module 'datetime' from '/opt/hostedtoolcache/Python/3.12.10/x64/lib/python3.12/datetime.py'> = None) -> int:
220	@staticmethod
221	def time(now: datetime = None) -> int:
222		"""
223        Generates a timestamp based on the provided datetime object or the current datetime.
224
225        Parameters:
226        now (datetime, optional): The datetime object to generate the timestamp from. If not provided, the current datetime is used.
227
228        Returns:
229        int: The timestamp in positive nanoseconds since the Unix epoch (January 1, 1970), before 1970 will return in negative until 1000AD.
230        """
231		if now is None:
232			now = datetime.datetime.now()
233		ordinal_day = now.toordinal()
234		ns_in_day = (now - now.replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() * 10**9
235		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.

@staticmethod
def time_to_datetime( ordinal_ns: int) -> <module 'datetime' from '/opt/hostedtoolcache/Python/3.12.10/x64/lib/python3.12/datetime.py'>:
237	@staticmethod
238	def time_to_datetime(ordinal_ns: int) -> datetime:
239		"""
240        Converts an ordinal number (number of days since 1000-01-01) to a datetime object.
241
242        Parameters:
243        ordinal_ns (int): The ordinal number of days since 1000-01-01.
244
245        Returns:
246        datetime: The corresponding datetime object.
247        """
248		ordinal_day = ordinal_ns // 86_400_000_000_000 + 719_163
249		ns_in_day = ordinal_ns % 86_400_000_000_000
250		d = datetime.datetime.fromordinal(ordinal_day)
251		t = datetime.timedelta(seconds=ns_in_day // 10**9)
252		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.

def nolock(self) -> bool:
288	def nolock(self) -> bool:
289		"""
290        Check if the vault lock is currently not set.
291
292        :return: True if the vault lock is not set, False otherwise.
293        """
294		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.

def lock(self) -> int:
296	def lock(self) -> int:
297		"""
298        Acquires a lock on the ZakatTracker instance.
299
300        Returns:
301        int: The lock ID. This ID can be used to release the lock later.
302        """
303		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.

def box(self) -> dict:
305	def box(self) -> dict:
306		"""
307        Returns a copy of the internal vault dictionary.
308
309        This method is used to retrieve the current state of the ZakatTracker object.
310        It provides a snapshot of the internal data structure, allowing for further
311        processing or analysis.
312
313        :return: A copy of the internal vault dictionary.
314        """
315		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.

def steps(self) -> dict:
317	def steps(self) -> dict:
318		"""
319        Returns a copy of the history of steps taken in the ZakatTracker.
320
321        The history is a dictionary where each key is a unique identifier for a step,
322        and the corresponding value is a dictionary containing information about the step.
323
324        :return: A copy of the history of steps taken in the ZakatTracker.
325        """
326		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.

def free(self, _lock: int, auto_save: bool = True) -> bool:
328	def free(self, _lock: int, auto_save: bool = True) -> bool:
329		"""
330        Releases the lock on the database.
331
332        Parameters:
333        _lock (int): The lock ID to be released.
334        auto_save (bool): Whether to automatically save the database after releasing the lock.
335
336        Returns:
337        bool: True if the lock is successfully released and (optionally) saved, False otherwise.
338        """
339		if _lock == self._vault['lock']:
340			self._vault['lock'] = None
341			if auto_save:
342				return self.save(self.path())
343			return True
344		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.

def account_exists(self, account) -> bool:
346	def account_exists(self, account) -> bool:
347		"""
348        Check if the given account exists in the vault.
349
350        Parameters:
351        account (str): The account number to check.
352
353        Returns:
354        bool: True if the account exists, False otherwise.
355        """
356		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.

def box_size(self, account) -> int:
358	def box_size(self, account) -> int:
359		"""
360		Calculate the size of the box for a specific account.
361
362		Parameters:
363		account (str): The account number for which the box size needs to be calculated.
364
365		Returns:
366		int: The size of the box for the given account. If the account does not exist, -1 is returned.
367		"""
368		if self.account_exists(account):
369			return len(self._vault['account'][account]['box'])
370		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.

def log_size(self, account) -> int:
372	def log_size(self, account) -> int:
373		"""
374        Get the size of the log for a specific account.
375
376        Parameters:
377        account (str): The account number for which the log size needs to be calculated.
378
379        Returns:
380        int: The size of the log for the given account. If the account does not exist, -1 is returned.
381        """
382		if self.account_exists(account):
383			return len(self._vault['account'][account]['log'])
384		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.

def recall(self, dry=True, debug=False) -> bool:
386	def recall(self, dry = True, debug = False) -> bool:
387		"""
388		Revert the last operation.
389
390		Parameters:
391		dry (bool): If True, the function will not modify the data, but will simulate the operation. Default is True.
392		debug (bool): If True, the function will print debug information. Default is False.
393
394		Returns:
395		bool: True if the operation was successful, False otherwise.
396		"""
397		if not self.nolock() or len(self._vault['history']) == 0:
398			return False
399		if len(self._vault['history']) <= 0:
400			return
401		ref = sorted(self._vault['history'].keys())[-1]
402		if debug:
403			print('recall', ref)
404		memory = self._vault['history'][ref]
405		if debug:
406			print(type(memory), 'memory', memory)
407
408		limit = len(memory) + 1
409		sub_positive_log_negative = 0
410		for i in range(-1, -limit, -1):
411			x = memory[i]
412			if debug:
413				print(type(x), x)
414			match x['action']:
415				case Action.CREATE:
416					if x['account'] is not None:
417						if self.account_exists(x['account']):
418							if debug:
419								print('account', self._vault['account'][x['account']])
420							assert len(self._vault['account'][x['account']]['box']) == 0
421							assert self._vault['account'][x['account']]['balance'] == 0
422							assert self._vault['account'][x['account']]['count'] == 0
423							if dry:
424								continue
425							del self._vault['account'][x['account']]
426
427				case Action.TRACK:
428					if x['account'] is not None:
429						if self.account_exists(x['account']):
430							if dry:
431								continue
432							self._vault['account'][x['account']]['balance'] -= x['value']
433							self._vault['account'][x['account']]['count'] -= 1
434							del self._vault['account'][x['account']]['box'][x['ref']]
435
436				case Action.LOG:
437					if x['account'] is not None:
438						if self.account_exists(x['account']):
439							if x['ref'] in self._vault['account'][x['account']]['log']:
440								if dry:
441									continue
442								if sub_positive_log_negative == -x['value']:
443									self._vault['account'][x['account']]['count'] -= 1
444									sub_positive_log_negative = 0
445								del self._vault['account'][x['account']]['log'][x['ref']]
446
447				case Action.SUB:
448					if x['account'] is not None:
449						if self.account_exists(x['account']):
450							if x['ref'] in self._vault['account'][x['account']]['box']:
451								if dry:
452									continue
453								self._vault['account'][x['account']]['box'][x['ref']]['rest'] += x['value']
454								self._vault['account'][x['account']]['balance'] += x['value']
455								sub_positive_log_negative = x['value']
456
457				case Action.ADD_FILE:
458					if x['account'] is not None:
459						if self.account_exists(x['account']):
460							if x['ref'] in self._vault['account'][x['account']]['log']:
461								if x['file'] in self._vault['account'][x['account']]['log'][x['ref']]['file']:
462									if dry:
463										continue
464									del self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']]
465
466				case Action.REMOVE_FILE:
467					if x['account'] is not None:
468						if self.account_exists(x['account']):
469							if x['ref'] in self._vault['account'][x['account']]['log']:
470								if dry:
471									continue
472								self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']] = x['value']
473
474				case Action.REPORT:
475					if x['ref'] in self._vault['report']:
476						if dry:
477							continue
478						del self._vault['report'][x['ref']]
479
480				case Action.ZAKAT:
481					if x['account'] is not None:
482						if self.account_exists(x['account']):
483							if x['ref'] in self._vault['account'][x['account']]['box']:
484								if x['key'] in self._vault['account'][x['account']]['box'][x['ref']]:
485									if dry:
486										continue
487									match x['math']:
488										case MathOperation.ADDITION:
489											self._vault['account'][x['account']]['box'][x['ref']][x['key']] -= x['value']
490										case MathOperation.EQUAL:
491											self._vault['account'][x['account']]['box'][x['ref']][x['key']] = x['value']
492										case MathOperation.SUBTRACTION:
493											self._vault['account'][x['account']]['box'][x['ref']][x['key']] += x['value']
494
495		if not dry:
496			del self._vault['history'][ref]
497		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.

def track( self, value: int = 0, desc: str = '', account: str = 1, logging: bool = True, created: int = None, debug: bool = False) -> int:
499	def track(self, value: int = 0, desc: str = '', account: str = 1, logging: bool = True, created: int = None, debug: bool = False) -> int:
500		"""
501        This function tracks a transaction for a specific account.
502
503        Parameters:
504        value (int): The value of the transaction. Default is 0.
505        desc (str): The description of the transaction. Default is an empty string.
506        account (str): The account for which the transaction is being tracked. Default is '1'.
507        logging (bool): Whether to log the transaction. Default is True.
508        created (int): The timestamp of the transaction. If not provided, it will be generated. Default is None.
509        debug (bool): Whether to print debug information. Default is False.
510
511        Returns:
512        int: The timestamp of the transaction.
513
514        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.
515
516		Raises:
517		ValueError: If the transction happend again in the same time.
518        """
519		if created is None:
520			created = ZakatTracker.time()
521		_nolock = self.nolock(); self.lock()
522		if not self.account_exists(account):
523			if debug:
524				print(f"account {account} created")
525			self._vault['account'][account] = {
526				'balance': 0,
527				'box': {},
528				'count': 0,
529				'log': {},
530				'zakatable': True,
531			}
532			self._step(Action.CREATE, account)
533		if value == 0:
534			if _nolock: self.free(self.lock())
535			return 0
536		if logging:
537			self._log(value, desc, account, created)
538		assert created not in self._vault['account'][account]['box']
539		self._vault['account'][account]['box'][created] = {
540			'capital': value,
541			'count': 0,
542			'last': 0,
543			'rest': value,
544			'total': 0,
545		}
546		self._step(Action.TRACK, account, ref=created, value=value)
547		if _nolock: self.free(self.lock())
548		return created

This function tracks a transaction for a specific account.

Parameters: value (int): 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: If the transction happend again in the same time.
def accounts(self) -> dict:
582	def accounts(self) -> dict:
583		"""
584        Returns a dictionary containing account numbers as keys and their respective balances as values.
585
586        Parameters:
587        None
588
589        Returns:
590        dict: A dictionary where keys are account numbers and values are their respective balances.
591        """
592		result = {}
593		for i in self._vault['account']:
594			result[i] = self._vault['account'][i]['balance']
595		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.

def boxes(self, account) -> dict:
597	def boxes(self, account) -> dict:
598		"""
599        Retrieve the boxes (transactions) associated with a specific account.
600
601        Parameters:
602        account (str): The account number for which to retrieve the boxes.
603
604        Returns:
605        dict: A dictionary containing the boxes associated with the given account.
606        If the account does not exist, an empty dictionary is returned.
607        """
608		if self.account_exists(account):
609			return self._vault['account'][account]['box']
610		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.

def logs(self, account) -> dict:
612	def logs(self, account) -> dict:
613		"""
614		Retrieve the logs (transactions) associated with a specific account.
615
616		Parameters:
617		account (str): The account number for which to retrieve the logs.
618
619		Returns:
620		dict: A dictionary containing the logs associated with the given account.
621		If the account does not exist, an empty dictionary is returned.
622		"""
623		if self.account_exists(account):
624			return self._vault['account'][account]['log']
625		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.

def add_file(self, account: str, ref: int, path: str) -> int:
627	def add_file(self, account: str, ref: int, path: str) -> int:
628		"""
629		Adds a file reference to a specific transaction log entry in the vault.
630
631		Parameters:
632		account (str): The account number associated with the transaction log.
633		ref (int): The reference to the transaction log entry.
634		path (str): The path of the file to be added.
635
636		Returns:
637		int: The reference of the added file. If the account or transaction log entry does not exist, returns 0.
638		"""
639		if self.account_exists(account):
640			if ref in self._vault['account'][account]['log']:
641				file_ref = ZakatTracker.time()
642				self._vault['account'][account]['log'][ref]['file'][file_ref] = path
643				_nolock = self.nolock(); self.lock()
644				self._step(Action.ADD_FILE, account, ref=ref, file=file_ref)
645				if _nolock: self.free(self.lock())
646				return file_ref
647		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.

def remove_file(self, account: str, ref: int, file_ref: int) -> bool:
649	def remove_file(self, account: str, ref: int, file_ref: int) -> bool:
650		"""
651		Removes a file reference from a specific transaction log entry in the vault.
652
653		Parameters:
654		account (str): The account number associated with the transaction log.
655		ref (int): The reference to the transaction log entry.
656		file_ref (int): The reference of the file to be removed.
657
658		Returns:
659		bool: True if the file reference is successfully removed, False otherwise.
660		"""
661		if self.account_exists(account):
662			if ref in self._vault['account'][account]['log']:
663				if file_ref in self._vault['account'][account]['log'][ref]['file']:
664					x = self._vault['account'][account]['log'][ref]['file'][file_ref]
665					del self._vault['account'][account]['log'][ref]['file'][file_ref]
666					_nolock = self.nolock(); self.lock()
667					self._step(Action.REMOVE_FILE, account, ref=ref, file=file_ref, value=x)
668					if _nolock: self.free(self.lock())
669					return True
670		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.

def balance(self, account: str = 1, cached: bool = True) -> int:
672	def balance(self, account: str = 1, cached: bool = True) -> int:
673		"""
674		Calculate and return the balance of a specific account.
675
676		Parameters:
677		account (str): The account number. Default is '1'.
678		cached (bool): If True, use the cached balance. If False, calculate the balance from the box. Default is True.
679
680		Returns:
681		int: The balance of the account.
682
683		Note:
684		If cached is True, the function returns the cached balance.
685		If cached is False, the function calculates the balance from the box by summing up the 'rest' values of all box items.
686		"""
687		if cached:
688			return self._vault['account'][account]['balance']
689		x = 0
690		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.

def zakatable(self, account, status: bool = None) -> bool:
692	def zakatable(self, account, status: bool = None) -> bool:
693		"""
694        Check or set the zakatable status of a specific account.
695
696        Parameters:
697        account (str): The account number.
698        status (bool, optional): The new zakatable status. If not provided, the function will return the current status.
699
700        Returns:
701        bool: The current or updated zakatable status of the account.
702
703        Raises:
704        None
705
706        Example:
707        >>> tracker = ZakatTracker()
708        >>> tracker.zakatable('account1', True)  # Set the zakatable status of 'account1' to True
709        True
710        >>> tracker.zakatable('account1')  # Get the zakatable status of 'account1'
711        True
712        """
713		if self.account_exists(account):
714			if status is None:
715				return self._vault['account'][account]['zakatable']
716			self._vault['account'][account]['zakatable'] = status
717			return status
718		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()
>>> tracker.zakatable('account1', True)  # Set the zakatable status of 'account1' to True
True
>>> tracker.zakatable('account1')  # Get the zakatable status of 'account1'
True
def sub( self, x: int, desc: str = '', account: str = 1, created: int = None, debug: bool = False) -> int:
720	def sub(self, x: int, desc: str = '', account: str = 1, created: int = None, debug: bool = False) -> int:
721		"""
722		Subtracts a specified amount from the account's balance.
723
724		Parameters:
725		x (int): The amount to subtract.
726		desc (str): The description of the transaction.
727		account (str): The account from which to subtract.
728		created (int): The timestamp of the transaction.
729		debug (bool): A flag to enable debug mode.
730
731		Returns:
732		int: The timestamp of the transaction.
733
734		If the amount to subtract is greater than the account's balance,
735		the remaining amount will be transferred to a new transaction with a negative value.
736
737		Raises:
738		ValueError: If the transction happend again in the same time.
739		"""
740		if x < 0:
741			return
742		if x == 0:
743			return self.track(x, '', account)
744		if created is None:
745			created = ZakatTracker.time()
746		_nolock = self.nolock(); self.lock()
747		self.track(0, '', account)
748		self._log(-x, desc, account, created)
749		ids = sorted(self._vault['account'][account]['box'].keys())
750		limit = len(ids) + 1
751		target = x
752		if debug:
753			print('ids', ids)
754		ages = []
755		for i in range(-1, -limit, -1):
756			if target == 0:
757				break
758			j = ids[i]
759			if debug:
760				print('i', i, 'j', j)
761			if self._vault['account'][account]['box'][j]['rest'] >= target:
762				self._vault['account'][account]['box'][j]['rest'] -= target
763				self._step(Action.SUB, account, ref=j, value=target)
764				ages.append((j, target))
765				target = 0
766				break
767			elif self._vault['account'][account]['box'][j]['rest'] < target and self._vault['account'][account]['box'][j]['rest'] > 0:
768				chunk = self._vault['account'][account]['box'][j]['rest']
769				target -= chunk
770				self._step(Action.SUB, account, ref=j, value=chunk)
771				ages.append((j, chunk))
772				self._vault['account'][account]['box'][j]['rest'] = 0
773		if target > 0:
774			self.track(-target, desc, account, False, created)
775			ages.append((created, target))
776		if _nolock: self.free(self.lock())
777		return (created, ages)

Subtracts a specified amount from the account's balance.

Parameters: x (int): The amount to subtract. desc (str): The description of the transaction. account (str): The account from which to subtract. created (int): The timestamp of the transaction. debug (bool): A flag to enable debug mode.

Returns: int: The timestamp of the 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: If the transction happend again in the same time.

def transfer( self, value: int, from_account: str, to_account: str, desc: str = '', created: int = None) -> list[int]:
779	def transfer(self, value: int, from_account: str, to_account: str, desc: str = '', created: int = None) -> list[int]:
780		"""
781		Transfers a specified value from one account to another.
782
783		Parameters:
784		value (int): The amount to be transferred.
785		from_account (str): The account from which the value will be transferred.
786		to_account (str): The account to which the value will be transferred.
787		desc (str, optional): A description for the transaction. Defaults to an empty string.
788		created (int, optional): The timestamp of the transaction. If not provided, the current timestamp will be used.
789
790		Returns:
791		list[int]: A list of timestamps corresponding to the transactions made during the transfer.
792
793		Raises:
794		ValueError: If the transction happend again in the same time.
795		"""
796		if created is None:
797			created = ZakatTracker.time()
798		(_, ages) = self.sub(value, desc, from_account, created)
799		times = []
800		for age in ages:
801			y = self.track(age[1], desc, to_account, logging=True, created=age[0])
802			times.append(y)
803		return times

Transfers a specified value from one account to another.

Parameters: value (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.

Returns: list[int]: A list of timestamps corresponding to the transactions made during the transfer.

Raises: ValueError: If the transction happend again in the same time.

def check( self, silver_gram_price: float, nisab: float = None, debug: bool = False, now: int = None, cycle: float = None) -> tuple:
805	def check(self, silver_gram_price: float, nisab: float = None, debug: bool = False, now: int = None, cycle: float = None) -> tuple:
806		"""
807		Check the eligibility for Zakat based on the given parameters.
808
809		Parameters:
810		silver_gram_price (float): The price of a gram of silver.
811		nisab (float): The minimum amount of wealth required for Zakat. If not provided, it will be calculated based on the silver_gram_price.
812		debug (bool): Flag to enable debug mode.
813		now (int): The current timestamp. If not provided, it will be calculated using ZakatTracker.time().
814		cycle (float): The time cycle for Zakat. If not provided, it will be calculated using ZakatTracker.TimeCycle().
815
816		Returns:
817		tuple: A tuple containing a boolean indicating the eligibility for Zakat, a list of brief statistics, and a dictionary containing the Zakat plan.
818		"""
819		if now is None:
820			now = ZakatTracker.time()
821		if cycle is None:
822			cycle = ZakatTracker.TimeCycle()
823		if nisab is None:
824			nisab = ZakatTracker.Nisab(silver_gram_price)
825		plan = {}
826		below_nisab = 0
827		brief = [0, 0, 0]
828		valid = False
829		for x in self._vault['account']:
830			if not self._vault['account'][x]['zakatable']:
831				continue
832			_box = self._vault['account'][x]['box']
833			limit = len(_box) + 1
834			ids = sorted(self._vault['account'][x]['box'].keys())
835			for i in range(-1, -limit, -1):
836				j = ids[i]
837				if _box[j]['rest'] <= 0:
838					continue
839				brief[0] += _box[j]['rest']
840				index = limit + i - 1
841				epoch = (now - j) / cycle
842				if debug:
843					print(f"Epoch: {epoch}", _box[j])
844				if _box[j]['last'] > 0:
845					epoch = (now - _box[j]['last']) / cycle
846				if debug:
847					print(f"Epoch: {epoch}")
848				epoch = floor(epoch)
849				if debug:
850					print(f"Epoch: {epoch}", type(epoch), epoch == 0, 1-epoch, epoch)
851				if epoch == 0:
852					continue
853				if debug:
854					print("Epoch - PASSED")
855				brief[1] += _box[j]['rest']
856				if _box[j]['rest'] >= nisab:
857					total = 0
858					for _ in range(epoch):
859						total += ZakatTracker.ZakatCut(_box[j]['rest'] - total)
860					if total > 0:
861						if x not in plan:
862							plan[x] = {}
863						valid = True
864						brief[2] += total
865						plan[x][index] = {'total': total, 'count': epoch}
866				else:
867					chunk = ZakatTracker.ZakatCut(_box[j]['rest'])
868					if chunk > 0:
869						if x not in plan:
870							plan[x] = {}
871						if j not in plan[x].keys():
872							plan[x][index] = {}
873						below_nisab += _box[j]['rest']
874						brief[2] += chunk
875						plan[x][index]['below_nisab'] = chunk
876		valid = valid or below_nisab >= nisab
877		if debug:
878			print(f"below_nisab({below_nisab}) >= nisab({nisab})")
879		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.

def build_payment_parts(self, demand: float, positive_only: bool = True) -> dict:
881	def build_payment_parts(self, demand: float, positive_only: bool = True) -> dict:
882		"""
883		Build payment parts for the zakat distribution.
884
885		Parameters:
886		demand (float): The total demand for payment.
887		positive_only (bool): If True, only consider accounts with positive balance. Default is True.
888
889		Returns:
890		dict: A dictionary containing the payment parts for each account. The dictionary has the following structure:
891		{
892			'account': {
893				'account_id': {'balance': float, 'part': float},
894				...
895			},
896			'exceed': bool,
897			'demand': float,
898			'total': float,
899		}
900		"""
901		total = 0
902		parts = {
903			'account': {},
904			'exceed': False,
905			'demand': demand,
906		}
907		for x, y in self.accounts().items():
908			if positive_only and y <= 0:
909				continue
910			total += y
911			parts['account'][x] = {'balance': y, 'part': 0}
912		parts['total'] = total
913		return parts

Build payment parts for the zakat distribution.

Parameters: demand (float): The total demand for payment. 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, 'part': float}, ... }, 'exceed': bool, 'demand': float, 'total': float, }

def check_payment_parts(self, parts: dict) -> int:
915	def check_payment_parts(self, parts: dict) -> int:
916		"""
917        Checks the validity of payment parts.
918
919        Parameters:
920        parts (dict): A dictionary containing payment parts information.
921
922        Returns:
923        int: Returns 0 if the payment parts are valid, otherwise returns the error code.
924
925        Error Codes:
926        1: 'demand', 'account', 'total', or 'exceed' key is missing in parts.
927        2: 'balance' or 'part' key is missing in parts['account'][x].
928        3: 'part' value in parts['account'][x] is less than or equal to 0.
929        4: If 'exceed' is False, 'balance' value in parts['account'][x] is less than or equal to 0.
930        5: 'part' value in parts['account'][x] is less than 0.
931        6: If 'exceed' is False, 'part' value in parts['account'][x] is greater than 'balance' value.
932        7: The sum of 'part' values in parts['account'] does not match with 'demand' value.
933        """
934		for i in ['demand', 'account', 'total', 'exceed']:
935			if not i in parts:
936				return 1
937		exceed = parts['exceed']
938		for x in parts['account']:
939			for j in ['balance', 'part']:
940				if not j in parts['account'][x]:
941					return 2
942				if parts['account'][x]['part'] <= 0:
943					return 3
944				if not exceed and parts['account'][x]['balance'] <= 0:
945					return 4
946		demand = parts['demand']
947		z = 0
948		for _, y in parts['account'].items():
949			if y['part'] < 0:
950				return 5
951			if not exceed and y['part'] > y['balance']:
952				return 6
953			z += y['part']
954		if z != demand:
955			return 7
956		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' 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.

def zakat(self, report: tuple, parts: dict = None, debug: bool = False) -> bool:
 958	def zakat(self, report: tuple, parts: dict = None, debug: bool = False) -> bool:
 959		"""
 960		Perform Zakat calculation based on the given report and optional parts.
 961
 962		Parameters:
 963		report (tuple): A tuple containing the validity of the report, the report data, and the zakat plan.
 964		parts (dict): A dictionary containing the payment parts for the zakat.
 965		debug (bool): A flag indicating whether to print debug information.
 966
 967		Returns:
 968		bool: True if the zakat calculation is successful, False otherwise.
 969		"""
 970		(valid, _, plan) = report
 971		if not valid:
 972			return valid
 973		parts_exist = parts is not None
 974		if parts_exist:
 975			for part in parts:
 976				if self.check_payment_parts(part) != 0:
 977					return False
 978		if debug:
 979			print('######### zakat #######')
 980			print('parts_exist', parts_exist)
 981		_nolock = self.nolock(); self.lock()
 982		report_time = ZakatTracker.time()
 983		self._vault['report'][report_time] = report
 984		self._step(Action.REPORT, ref=report_time)
 985		created = ZakatTracker.time()
 986		for x in plan:
 987			if debug:
 988				print(plan[x])
 989				print('-------------')
 990				print(self._vault['account'][x]['box'])
 991			ids = sorted(self._vault['account'][x]['box'].keys())
 992			if debug:
 993				print('plan[x]', plan[x])
 994			for i in plan[x].keys():
 995				j = ids[i]
 996				if debug:
 997					print('i', i, 'j', j)
 998				self._step(Action.ZAKAT, x, j, value=self._vault['account'][x]['box'][j]['last'], key='last', math_operation=MathOperation.EQUAL)
 999				self._vault['account'][x]['box'][j]['last'] = created
1000				self._vault['account'][x]['box'][j]['total'] += plan[x][i]['total']
1001				self._step(Action.ZAKAT, x, j, value=plan[x][i]['total'], key='total', math_operation=MathOperation.ADDITION)
1002				self._vault['account'][x]['box'][j]['count'] += plan[x][i]['count']
1003				self._step(Action.ZAKAT, x, j, value=plan[x][i]['count'], key='count', math_operation=MathOperation.ADDITION)
1004				if not parts_exist:
1005					self._vault['account'][x]['box'][j]['rest'] -= plan[x][i]['total']
1006					self._step(Action.ZAKAT, x, j, value=plan[x][i]['total'], key='rest', math_operation=MathOperation.SUBTRACTION)
1007		if parts_exist:
1008			for transaction in parts:
1009				for account, part in transaction['account'].items():
1010					if debug:
1011						print('zakat-part', account, part['part'])
1012					self.sub(part['part'], 'zakat-part', account, debug=debug)
1013		if _nolock: self.free(self.lock())
1014		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.

def export_json(self, path: str = 'data.json') -> bool:
1016	def export_json(self, path: str = "data.json") -> bool:
1017		"""
1018        Exports the current state of the ZakatTracker object to a JSON file.
1019
1020        Parameters:
1021        path (str): The path where the JSON file will be saved. Default is "data.json".
1022
1023        Returns:
1024        bool: True if the export is successful, False otherwise.
1025
1026        Raises:
1027        No specific exceptions are raised by this method.
1028        """
1029		with open(path, "w") as file:
1030			json.dump(self._vault, file, indent=4, cls=JSONEncoder)
1031			return True
1032		return False

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.

def save(self, path: str = None) -> bool:
1034	def save(self, path: str = None) -> bool:
1035		"""
1036		Save the current state of the ZakatTracker object to a pickle file.
1037
1038		Parameters:
1039		path (str): The path where the pickle file will be saved. If not provided, it will use the default path.
1040
1041		Returns:
1042		bool: True if the save operation is successful, False otherwise.
1043		"""
1044		if path is None:
1045			path = self.path()
1046		with open(path, "wb") as f:
1047			pickle.dump(self._vault, f)
1048			return True
1049		return False

Save the current state of the ZakatTracker object to a pickle file.

Parameters: path (str): The path where the pickle file will be saved. If not provided, it will use the default path.

Returns: bool: True if the save operation is successful, False otherwise.

def load(self, path: str = None) -> bool:
1051	def load(self, path: str = None) -> bool:
1052		"""
1053		Load the current state of the ZakatTracker object from a pickle file.
1054
1055		Parameters:
1056		path (str): The path where the pickle file is located. If not provided, it will use the default path.
1057
1058		Returns:
1059		bool: True if the load operation is successful, False otherwise.
1060		"""
1061		if path is None:
1062			path = self.path()
1063		if os.path.exists(path):
1064			with open(path, "rb") as f:
1065				self._vault = pickle.load(f)
1066				return True
1067		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.

def import_csv(self, path: str = 'file.csv') -> tuple:
1069	def import_csv(self, path: str = 'file.csv') -> tuple:
1070		"""
1071        Import transactions from a CSV file.
1072
1073        Parameters:
1074        path (str): The path to the CSV file. Default is 'file.csv'.
1075
1076        Returns:
1077        tuple: A tuple containing the number of transactions created, the number of transactions found in the cache, and a dictionary of bad transactions.
1078
1079        The CSV file should have the following format:
1080        account, desc, value, date
1081        For example:
1082        safe-45, "Some text", 34872, 1988-06-30 00:00:00
1083
1084        The function reads the CSV file, checks for duplicate transactions, and creates the transactions in the system.
1085        """
1086		cache = []
1087		tmp = "tmp"
1088		try:
1089			with open(tmp, "rb") as f:
1090				cache = pickle.load(f)
1091		except:
1092			pass
1093		date_formats = [
1094			"%Y-%m-%d %H:%M:%S",
1095			"%Y-%m-%dT%H:%M:%S",
1096			"%Y-%m-%dT%H%M%S",
1097			"%Y-%m-%d",
1098		]
1099		created, found, bad = 0, 0, {}
1100		with open(path, newline='', encoding="utf-8") as f:
1101			i = 0
1102			for row in csv.reader(f, delimiter=','):
1103				i += 1
1104				hashed = hash(tuple(row))
1105				if hashed in cache:
1106					found += 1
1107					continue
1108				account = row[0]
1109				desc = row[1]
1110				value = float(row[2])
1111				date = 0
1112				for time_format in date_formats:
1113					try:
1114						date = self.time(datetime.datetime.strptime(row[3], time_format))
1115						break
1116					except:
1117						pass
1118				# TODO: not allowed for negative dates
1119				if date == 0 or value == 0:
1120					bad[i] = row
1121					continue
1122				if value > 0:
1123					self.track(value, desc, account, True, date)
1124				elif value < 0:
1125					self.sub(-value, desc, account, date)
1126				created += 1
1127				cache.append(hashed)
1128		with open(tmp, "wb") as f:
1129				pickle.dump(cache, f)
1130		return (created, found, bad)

Import transactions from a CSV file.

Parameters: path (str): The path to the CSV file. Default is 'file.csv'.

Returns: tuple: A tuple containing the number of transactions created, the number of transactions found in the cache, and a dictionary of bad transactions.

The CSV file should have the following format: account, desc, value, date For example: safe-45, "Some text", 34872, 1988-06-30 00:00:00

The function reads the CSV file, checks for duplicate transactions, and creates the transactions in the system.

@staticmethod
def DurationFromNanoSeconds(ns: int) -> tuple:
1136	@staticmethod
1137	def DurationFromNanoSeconds(ns: int) -> tuple:
1138		"""REF https://github.com/JayRizzo/Random_Scripts/blob/master/time_measure.py#L106
1139			Convert NanoSeconds to Human Readable Time Format.
1140			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.
1141			Its symbol is μs, sometimes simplified to us when Unicode is not available.
1142			A microsecond is equal to 1000 nanoseconds or 1⁄1,000 of a millisecond.
1143
1144			INPUT : ms (AKA: MilliSeconds)
1145			OUTPUT: tuple(string TIMELAPSED, string SPOKENTIME) like format.
1146			OUTPUT Variables: TIMELAPSED, SPOKENTIME
1147
1148			Example  Input: DurationFromNanoSeconds(ns)
1149			**"Millennium:Century:Years:Days:Hours:Minutes:Seconds:MilliSeconds:MicroSeconds:NanoSeconds"**
1150			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')
1151			DurationFromNanoSeconds(1234567890123456789012)
1152		"""
1153		μs, ns      = divmod(ns, 1000)
1154		ms, μs      = divmod(μs, 1000)
1155		s, ms       = divmod(ms, 1000)
1156		m, s        = divmod(s, 60)
1157		h, m        = divmod(m, 60)
1158		d, h        = divmod(h, 24)
1159		y, d        = divmod(d, 365)
1160		c, y        = divmod(y, 100)
1161		n, c        = divmod(c, 10)
1162		TIMELAPSED  = 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}::{μs:03.0f}::{ns:03.0f}"
1163		SPOKENTIME  = 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, {μs: 3d} MicroSeconds, {ns: 3d} NanoSeconds"
1164		return TIMELAPSED, SPOKENTIME

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 TIMELAPSED, string SPOKENTIME) like format. OUTPUT Variables: TIMELAPSED, SPOKENTIME

Example Input: DurationFromNanoSeconds(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') DurationFromNanoSeconds(1234567890123456789012)

@staticmethod
def generate_random_date( start_date: datetime.datetime, end_date: datetime.datetime) -> datetime.datetime:
1166	@staticmethod
1167	def generate_random_date(start_date: datetime.datetime, end_date: datetime.datetime) -> datetime.datetime:
1168		"""
1169		Generate a random date between two given dates.
1170
1171		Parameters:
1172		start_date (datetime.datetime): The start date from which to generate a random date.
1173		end_date (datetime.datetime): The end date until which to generate a random date.
1174
1175		Returns:
1176		datetime.datetime: A random date between the start_date and end_date.
1177		"""
1178		time_between_dates = end_date - start_date
1179		days_between_dates = time_between_dates.days
1180		random_number_of_days = random.randrange(days_between_dates)
1181		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.

@staticmethod
def generate_random_csv_file(path: str = 'data.csv', count: int = 1000) -> None:
1183	@staticmethod
1184	def generate_random_csv_file(path: str = "data.csv", count: int = 1000) -> None:
1185		"""
1186		Generate a random CSV file with specified parameters.
1187
1188		Parameters:
1189		path (str): The path where the CSV file will be saved. Default is "data.csv".
1190		count (int): The number of rows to generate in the CSV file. Default is 1000.
1191
1192		Returns:
1193		None. The function generates a CSV file at the specified path with the given count of rows.
1194		Each row contains a randomly generated account, description, value, and date.
1195		The value is randomly generated between 1000 and 100000, and the date is randomly generated between 1950-01-01 and 2023-12-31.
1196		If the row number is not divisible by 13, the value is multiplied by -1.
1197		"""
1198		with open(path, "w", newline="") as csvfile:
1199			writer = csv.writer(csvfile)
1200			for i in range(count):
1201				account = f"acc-{random.randint(1, 1000)}"
1202				desc = f"Some text {random.randint(1, 1000)}"
1203				value = random.randint(1000, 100000)
1204				date = ZakatTracker.generate_random_date(datetime.datetime(1950, 1, 1), datetime.datetime(2023, 12, 31)).strftime("%Y-%m-%d %H:%M:%S")
1205				if not i % 13 == 0:
1206					value *= -1
1207				writer.writerow([account, desc, value, date])

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.

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.

def test(self, debug: bool = False):
1347	def test(self, debug: bool = False):
1348
1349		try:
1350
1351			assert self._history()
1352
1353			# Not allowed for duplicate transactions in the same account and time
1354
1355			created = ZakatTracker.time()
1356			self.track(100, 'test-1', 'same', True, created)
1357			failed = False
1358			try:
1359				self.track(50, 'test-1', 'same', True, created)
1360			except:
1361				failed = True
1362			assert failed is True
1363
1364			self.reset()
1365
1366			# Always preserve box age during transfer
1367
1368			created = ZakatTracker.time()
1369			series = [
1370				(30, 4),
1371				(60, 3),
1372				(90, 2),
1373			]
1374			case = {
1375				30: {
1376					'series': series,
1377					'rest' : 150,
1378				},
1379				60: {
1380					'series': series,
1381					'rest' : 120,
1382				},
1383				90: {
1384					'series': series,
1385					'rest' : 90,
1386				},
1387				180: {
1388					'series': series,
1389					'rest' : 0,
1390				},
1391				270: {
1392					'series': series,
1393					'rest' : -90,
1394				},
1395				360: {
1396					'series': series,
1397					'rest' : -180,
1398				},
1399			}
1400
1401			selected_time = ZakatTracker.time() - ZakatTracker.TimeCycle()
1402				
1403			for total in case:
1404				for x in case[total]['series']:
1405					self.track(x[0], f"test-{x} ages", 'ages', True, selected_time * x[1])
1406
1407				refs = self.transfer(total, 'ages', 'future', 'Zakat Movement')
1408
1409				if debug:
1410					print('refs', refs)
1411
1412				ages_cache_balance = self.balance('ages')
1413				ages_fresh_balance = self.balance('ages', False)
1414				rest = case[total]['rest']
1415				if debug:
1416					print('source', ages_cache_balance, ages_fresh_balance, rest)
1417				assert ages_cache_balance == rest
1418				assert ages_fresh_balance == rest
1419
1420				future_cache_balance = self.balance('future')
1421				future_fresh_balance = self.balance('future', False)
1422				if debug:
1423					print('target', future_cache_balance, future_fresh_balance, total)
1424					print('refs', refs)
1425				assert future_cache_balance == total
1426				assert future_fresh_balance == total
1427
1428				for ref in self._vault['account']['ages']['box']:
1429					ages_capital = self._vault['account']['ages']['box'][ref]['capital']
1430					ages_rest = self._vault['account']['ages']['box'][ref]['rest']
1431					future_capital = 0
1432					if ref in self._vault['account']['future']['box']:
1433						future_capital = self._vault['account']['future']['box'][ref]['capital']
1434					future_rest = 0
1435					if ref in self._vault['account']['future']['box']:
1436						future_rest = self._vault['account']['future']['box'][ref]['rest']
1437					if ages_capital != 0 and future_capital != 0 and future_rest != 0:
1438						if debug:
1439							print('================================================================')
1440							print('ages', ages_capital, ages_rest)
1441							print('future', future_capital, future_rest)
1442						if ages_rest == 0:
1443							assert ages_capital == future_capital
1444						elif ages_rest < 0:
1445							assert -ages_capital == future_capital
1446						elif ages_rest > 0:
1447							assert ages_capital == ages_rest + future_capital
1448				self.reset()
1449				assert len(self._vault['history']) == 0
1450
1451			assert self._history()
1452			assert self._history(False) is False
1453			assert self._history() is False
1454			assert self._history(True)
1455			assert self._history()
1456
1457			self._test_core(True, debug)
1458			self._test_core(False, debug)
1459
1460			transaction = [
1461				(
1462					20, 'wallet', 1, 800, 800, 800, 4, 5,
1463									-85, -85, -85, 6, 7,
1464				),
1465				(
1466					750, 'wallet', 'safe',  50,   50,  50, 4, 6,
1467											750, 750, 750, 1, 1,
1468				),
1469				(
1470					600, 'safe', 'bank', 150, 150, 150, 1, 2,
1471										600, 600, 600, 1, 1,
1472				),
1473			]
1474			for z in transaction:
1475				self.lock()
1476				x = z[1]
1477				y = z[2]
1478				self.transfer(z[0], x, y, 'test-transfer')
1479				assert self.balance(x) == z[3]
1480				xx = self.accounts()[x]
1481				assert xx == z[3]
1482				assert self.balance(x, False) == z[4]
1483				assert xx == z[4]
1484
1485				l = self._vault['account'][x]['log']
1486				s = 0
1487				for i in l:
1488					s += l[i]['value']
1489				if debug:
1490					print('s', s, 'z[5]', z[5])
1491				assert s == z[5]
1492
1493				assert self.box_size(x) == z[6]
1494				assert self.log_size(x) == z[7]
1495
1496				yy = self.accounts()[y]
1497				assert self.balance(y) == z[8]
1498				assert yy == z[8]
1499				assert self.balance(y, False) == z[9]
1500				assert yy == z[9]
1501
1502				l = self._vault['account'][y]['log']
1503				s = 0
1504				for i in l:
1505					s += l[i]['value']
1506				assert s == z[10]
1507
1508				assert self.box_size(y) == z[11]
1509				assert self.log_size(y) == z[12]
1510
1511			if debug:
1512				pp().pprint(self.check(2.17))
1513
1514			assert not self.nolock()
1515			history_count = len(self._vault['history'])
1516			if debug:
1517				print('history-count', history_count)
1518			assert history_count == 11
1519			assert not self.free(ZakatTracker.time())
1520			assert self.free(self.lock())
1521			assert self.nolock()
1522			assert len(self._vault['history']) == 11
1523
1524			# storage
1525
1526			_path = self.path('test.pickle')
1527			if os.path.exists(_path):
1528				os.remove(_path)
1529			self.save()
1530			assert os.path.getsize(_path) > 0
1531			self.reset()
1532			assert self.recall(False, debug) is False
1533			self.load()
1534			assert self._vault['account'] is not None
1535
1536			# recall
1537
1538			assert self.nolock()
1539			assert len(self._vault['history']) == 11
1540			assert self.recall(False, debug) is True
1541			assert len(self._vault['history']) == 10
1542			assert self.recall(False, debug) is True
1543			assert len(self._vault['history']) == 9
1544
1545			# csv
1546			
1547			_path = "test.csv"
1548			count = 1000
1549			if os.path.exists(_path):
1550				os.remove(_path)
1551			self.generate_random_csv_file(_path, count)
1552			assert os.path.getsize(_path) > 0
1553			tmp = "tmp"
1554			if os.path.exists(tmp):
1555				os.remove(tmp)
1556			(created, found, bad) = self.import_csv(_path)
1557			bad_count = len(bad)
1558			if debug:
1559				print(f"csv-imported: ({created}, {found}, {bad_count})")
1560			tmp_size = os.path.getsize(tmp)
1561			assert tmp_size > 0
1562			assert created + found + bad_count == count
1563			assert created == count
1564			assert bad_count == 0
1565			(created_2, found_2, bad_2) = self.import_csv(_path)
1566			bad_2_count = len(bad_2)
1567			if debug:
1568				print(f"csv-imported: ({created_2}, {found_2}, {bad_2_count})")
1569				print(bad)
1570			assert tmp_size == os.path.getsize(tmp)
1571			assert created_2 + found_2 + bad_2_count == count
1572			assert created == found_2
1573			assert bad_count == bad_2_count
1574			assert found_2 == count
1575			assert bad_2_count == 0
1576			assert created_2 == 0
1577
1578			# payment parts
1579
1580			positive_parts = self.build_payment_parts(100, positive_only=True)
1581			assert self.check_payment_parts(positive_parts) != 0
1582			assert self.check_payment_parts(positive_parts) != 0
1583			all_parts = self.build_payment_parts(300, positive_only= False)
1584			assert self.check_payment_parts(all_parts) != 0
1585			assert self.check_payment_parts(all_parts) != 0
1586			if debug:
1587				pp().pprint(positive_parts)
1588				pp().pprint(all_parts)
1589			# dynamic discount
1590			suite = []
1591			count = 3
1592			for exceed in [False, True]:
1593				case = []
1594				for parts in [positive_parts, all_parts]:
1595					part = parts.copy()
1596					demand = part['demand']
1597					if debug:
1598						print(demand, part['total'])
1599					i = 0
1600					z = demand / count
1601					cp = {
1602						'account': {},
1603						'demand': demand,
1604						'exceed': exceed,
1605						'total': part['total'],
1606					}
1607					for x, y in part['account'].items():
1608						if exceed and z <= demand:
1609							i += 1
1610							y['part'] = z
1611							if debug:
1612								print(exceed, y)
1613							cp['account'][x] = y
1614							case.append(y)
1615						elif not exceed and y['balance'] >= z:
1616								i += 1
1617								y['part'] = z
1618								if debug:
1619									print(exceed, y)
1620								cp['account'][x] = y
1621								case.append(y)
1622						if i >= count:
1623								break
1624					if len(cp['account'][x]) > 0:
1625						suite.append(cp)
1626			if debug:
1627				print('suite', len(suite))
1628			for case in suite:
1629				if debug:
1630					print(case)
1631				result = self.check_payment_parts(case)
1632				if debug:
1633					print('check_payment_parts', result)
1634				assert result == 0
1635				
1636			report = self.check(2.17, None, debug)
1637			(valid, brief, plan) = report
1638			if debug:
1639				print('valid', valid)
1640			assert self.zakat(report, parts=suite, debug=debug)
1641
1642			assert self.export_json("1000-transactions-test.json")
1643			assert self.save("1000-transactions-test.pickle")
1644
1645			# check & zakat
1646
1647			cases = [
1648				(1000, 'safe', ZakatTracker.time()-ZakatTracker.TimeCycle(), [
1649					{'safe': {0: {'below_nisab': 25}}},
1650				], False),
1651				(2000, 'safe', ZakatTracker.time()-ZakatTracker.TimeCycle(), [
1652					{'safe': {0: {'count': 1, 'total': 50}}},
1653				], True),
1654				(10000, 'cave', ZakatTracker.time()-(ZakatTracker.TimeCycle()*3), [
1655					{'cave': {0: {'count': 3, 'total': 731.40625}}},
1656				], True),
1657			]
1658			for case in cases:
1659				if debug:
1660					print("############# check #############")
1661				self.reset()
1662				self.track(case[0], 'test-check', case[1], True, case[2])
1663
1664				assert self.nolock()
1665				assert len(self._vault['history']) == 1
1666				assert self.lock()
1667				assert not self.nolock()
1668				report = self.check(2.17, None, debug)
1669				(valid, brief, plan) = report
1670				assert valid == case[4]
1671				if debug:
1672					print(brief)
1673				assert case[0] == brief[0]
1674				assert case[0] == brief[1]
1675
1676				if debug:
1677					pp().pprint(plan)
1678
1679				for x in plan:
1680					assert case[1] == x
1681					if 'total' in case[3][0][x][0].keys():
1682						assert case[3][0][x][0]['total'] == brief[2]
1683						assert plan[x][0]['total'] == case[3][0][x][0]['total']
1684						assert plan[x][0]['count'] == case[3][0][x][0]['count']
1685					else:
1686						assert plan[x][0]['below_nisab'] == case[3][0][x][0]['below_nisab']
1687				if debug:
1688					pp().pprint(report)
1689				result = self.zakat(report, debug=debug)
1690				if debug:
1691					print('zakat-result', result, case[4])
1692				assert result == case[4]
1693				report = self.check(2.17, None, debug)
1694				(valid, brief, plan) = report
1695				assert valid is False
1696
1697			assert len(self._vault['history']) == 2
1698			assert not self.nolock()
1699			assert self.recall(False, debug) is False
1700			self.free(self.lock())
1701			assert self.nolock()
1702			assert len(self._vault['history']) == 2
1703			assert self.recall(False, debug) is True
1704			assert len(self._vault['history']) == 1
1705
1706			assert self.nolock()
1707			assert len(self._vault['history']) == 1
1708
1709			assert self.recall(False, debug) is True
1710			assert len(self._vault['history']) == 0
1711
1712			assert len(self._vault['account']) == 0
1713			assert len(self._vault['history']) == 0
1714			assert len(self._vault['report']) == 0
1715			assert self.nolock()
1716		except:
1717			pp().pprint(self._vault)
1718			raise
class Action(enum.Enum):
68class Action(Enum):
69	CREATE = auto()
70	TRACK = auto()
71	LOG = auto()
72	SUB = auto()
73	ADD_FILE = auto()
74	REMOVE_FILE = auto()
75	REPORT = auto()
76	ZAKAT = auto()
CREATE = <Action.CREATE: 1>
TRACK = <Action.TRACK: 2>
LOG = <Action.LOG: 3>
SUB = <Action.SUB: 4>
ADD_FILE = <Action.ADD_FILE: 5>
REMOVE_FILE = <Action.REMOVE_FILE: 6>
REPORT = <Action.REPORT: 7>
ZAKAT = <Action.ZAKAT: 8>
class JSONEncoder(json.encoder.JSONEncoder):
78class JSONEncoder(json.JSONEncoder):
79	def default(self, obj):
80		if isinstance(obj, Action) or isinstance(obj, MathOperation):
81			return obj.name  # Serialize as the enum member's name
82		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).

def default(self, obj):
79	def default(self, obj):
80		if isinstance(obj, Action) or isinstance(obj, MathOperation):
81			return obj.name  # Serialize as the enum member's name
82		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)
class MathOperation(enum.Enum):
84class MathOperation(Enum):
85	ADDITION = auto()
86	EQUAL = auto()
87	SUBTRACTION = auto()
ADDITION = <MathOperation.ADDITION: 1>
EQUAL = <MathOperation.EQUAL: 2>
SUBTRACTION = <MathOperation.SUBTRACTION: 3>