Refactored DataCache, again. Implemented more advance cache management. All DataCache tests pass.
This commit is contained in:
parent
8361efd965
commit
a16cc542d2
|
|
@ -1,7 +1,7 @@
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from Users import Users
|
from Users import Users
|
||||||
from DataCache_v2 import DataCache
|
from DataCache_v3 import DataCache
|
||||||
from Strategies import Strategies
|
from Strategies import Strategies
|
||||||
from backtesting import Backtester
|
from backtesting import Backtester
|
||||||
from candles import Candles
|
from candles import Candles
|
||||||
|
|
@ -503,8 +503,6 @@ class BrighterTrades:
|
||||||
print(f'ERROR SETTING VALUE')
|
print(f'ERROR SETTING VALUE')
|
||||||
print(f'The string received by the server was: /n{params}')
|
print(f'The string received by the server was: /n{params}')
|
||||||
|
|
||||||
# Save any changes to storage
|
|
||||||
self.config.config_and_states('save')
|
|
||||||
# Now that the state is changed reload price history.
|
# Now that the state is changed reload price history.
|
||||||
self.candles.set_cache(user_name=user_name)
|
self.candles.set_cache(user_name=user_name)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -94,10 +94,35 @@ class DataCache:
|
||||||
self.db = Database()
|
self.db = Database()
|
||||||
self.exchanges = exchanges
|
self.exchanges = exchanges
|
||||||
# Single DataFrame for all cached data
|
# Single DataFrame for all cached data
|
||||||
self.cache = pd.DataFrame(columns=['key', 'data']) # Assuming 'key' and 'data' are necessary
|
self.caches = {}
|
||||||
logger.info("DataCache initialized.")
|
logger.info("DataCache initialized.")
|
||||||
|
|
||||||
def fetch_cached_rows(self, table: str, filter_vals: Tuple[str, Any]) -> pd.DataFrame | None:
|
def set_cache(self, data: Any, key: str, do_not_overwrite: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Sets or updates an entry in the cache with the provided key. If the key already exists, the existing entry
|
||||||
|
is replaced unless `do_not_overwrite` is True. In that case, the existing entry is preserved.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
data: The data to be cached. This can be of any type.
|
||||||
|
key: The unique key used to identify the cached data.
|
||||||
|
do_not_overwrite : The default is False, meaning that the existing entry will be replaced.
|
||||||
|
"""
|
||||||
|
if do_not_overwrite and key in self.cache['key'].values:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Construct a new DataFrame row with the key and data
|
||||||
|
new_row = pd.DataFrame({'key': [key], 'data': [data]})
|
||||||
|
|
||||||
|
# If the key already exists in the cache, remove the old entry
|
||||||
|
self.cache = self.cache[self.cache['key'] != key]
|
||||||
|
|
||||||
|
# Append the new row to the cache
|
||||||
|
self.cache = pd.concat([self.cache, new_row], ignore_index=True)
|
||||||
|
|
||||||
|
print(f'Current Cache: {self.cache}')
|
||||||
|
logger.debug(f'Cache set for key: {key}')
|
||||||
|
|
||||||
|
def get_or_fetch_rows(self, table: str, filter_vals: Tuple[str, Any]) -> pd.DataFrame | None:
|
||||||
"""
|
"""
|
||||||
Retrieves rows from the cache if available; otherwise, queries the database and caches the result.
|
Retrieves rows from the cache if available; otherwise, queries the database and caches the result.
|
||||||
|
|
||||||
|
|
@ -170,10 +195,10 @@ class DataCache:
|
||||||
:return: True if the attribute is already taken, False otherwise.
|
:return: True if the attribute is already taken, False otherwise.
|
||||||
"""
|
"""
|
||||||
# Fetch rows from the specified table where the attribute matches the given value
|
# Fetch rows from the specified table where the attribute matches the given value
|
||||||
result = self.fetch_cached_rows(table=table, filter_vals=(attr, val))
|
result = self.get_or_fetch_rows(table=table, filter_vals=(attr, val))
|
||||||
return result is not None and not result.empty
|
return result is not None and not result.empty
|
||||||
|
|
||||||
def fetch_cached_item(self, item_name: str, table_name: str, filter_vals: Tuple[str, Any]) -> Any:
|
def fetch_item(self, item_name: str, table_name: str, filter_vals: Tuple[str, Any]) -> Any:
|
||||||
"""
|
"""
|
||||||
Retrieves a specific item from the cache or database, caching the result if necessary.
|
Retrieves a specific item from the cache or database, caching the result if necessary.
|
||||||
|
|
||||||
|
|
@ -183,16 +208,16 @@ class DataCache:
|
||||||
:return: The value of the requested item.
|
:return: The value of the requested item.
|
||||||
:raises ValueError: If the item is not found in either the cache or the database.
|
:raises ValueError: If the item is not found in either the cache or the database.
|
||||||
"""
|
"""
|
||||||
# Fetch the relevant rows
|
# Fetch the relevant rows from the cache or database
|
||||||
rows = self.fetch_cached_rows(table_name, filter_vals)
|
rows = self.get_or_fetch_rows(table_name, filter_vals)
|
||||||
if rows is not None and not rows.empty:
|
if rows is not None and not rows.empty:
|
||||||
# Return the specific item from the first matching row.
|
# Return the specific item from the first matching row.
|
||||||
return rows.iloc[0][item_name]
|
return rows.iloc[0][item_name]
|
||||||
|
|
||||||
# If the item is not found, raise an error.
|
# If the item is not found, raise an error.
|
||||||
raise ValueError(f"Item {item_name} not found in {table_name} where {filter_vals[0]} = {filter_vals[1]}")
|
raise ValueError(f"Item '{item_name}' not found in '{table_name}' where {filter_vals[0]} = {filter_vals[1]}")
|
||||||
|
|
||||||
def modify_cached_row(self, table: str, filter_vals: Tuple[str, Any], field_name: str, new_data: Any) -> None:
|
def modify_item(self, table: str, filter_vals: Tuple[str, Any], field_name: str, new_data: Any) -> None:
|
||||||
"""
|
"""
|
||||||
Modifies a specific field in a row within the cache and updates the database accordingly.
|
Modifies a specific field in a row within the cache and updates the database accordingly.
|
||||||
|
|
||||||
|
|
@ -202,7 +227,7 @@ class DataCache:
|
||||||
:param new_data: The new data to be set.
|
:param new_data: The new data to be set.
|
||||||
"""
|
"""
|
||||||
# Retrieve the row from the cache or database
|
# Retrieve the row from the cache or database
|
||||||
row = self.fetch_cached_rows(table, filter_vals)
|
row = self.get_or_fetch_rows(table, filter_vals)
|
||||||
|
|
||||||
if row is None or row.empty:
|
if row is None or row.empty:
|
||||||
raise ValueError(f"Row not found in cache or database for {filter_vals[0]} = {filter_vals[1]}")
|
raise ValueError(f"Row not found in cache or database for {filter_vals[0]} = {filter_vals[1]}")
|
||||||
|
|
@ -223,7 +248,7 @@ class DataCache:
|
||||||
# Update the database with the modified row
|
# Update the database with the modified row
|
||||||
self.db.insert_dataframe(row.drop(columns='id'), table)
|
self.db.insert_dataframe(row.drop(columns='id'), table)
|
||||||
|
|
||||||
def insert_data(self, df: pd.DataFrame, table: str, skip_cache: bool = False) -> None:
|
def insert_df(self, df: pd.DataFrame, table: str, skip_cache: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Inserts data into the specified table in the database, with an option to skip cache insertion.
|
Inserts data into the specified table in the database, with an option to skip cache insertion.
|
||||||
|
|
||||||
|
|
@ -569,22 +594,6 @@ class DataCache:
|
||||||
else:
|
else:
|
||||||
raise KeyError(f"Cache key '{cache_key}' not found.")
|
raise KeyError(f"Cache key '{cache_key}' not found.")
|
||||||
|
|
||||||
def set_cache(self, data: Any, key: str, do_not_overwrite: bool = False) -> None:
|
|
||||||
if do_not_overwrite and key in self.cache['key'].values:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Corrected construction of the new row
|
|
||||||
new_row = pd.DataFrame({'key': [key], 'data': [data]})
|
|
||||||
|
|
||||||
# If the key already exists, drop the old entry
|
|
||||||
self.cache = self.cache[self.cache['key'] != key]
|
|
||||||
|
|
||||||
# Append the new row to the cache
|
|
||||||
self.cache = pd.concat([self.cache, new_row], ignore_index=True)
|
|
||||||
|
|
||||||
print(f'Current Cache: {self.cache}')
|
|
||||||
logger.debug(f'Cache set for key: {key}')
|
|
||||||
|
|
||||||
def _fetch_candles_from_exchange(self, symbol: str, interval: str, exchange_name: str, user_name: str,
|
def _fetch_candles_from_exchange(self, symbol: str, interval: str, exchange_name: str, user_name: str,
|
||||||
start_datetime: dt.datetime = None,
|
start_datetime: dt.datetime = None,
|
||||||
end_datetime: dt.datetime = None) -> pd.DataFrame:
|
end_datetime: dt.datetime = None) -> pd.DataFrame:
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -133,29 +133,41 @@ class Database:
|
||||||
print(f"Error querying table '{table}' for column '{filter_vals[0]}': {e}")
|
print(f"Error querying table '{table}' for column '{filter_vals[0]}': {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def insert_dataframe(self, df: pd.DataFrame, table: str) -> None:
|
def insert_dataframe(self, df: pd.DataFrame, table: str) -> int:
|
||||||
"""
|
"""
|
||||||
Inserts a DataFrame into a specified table.
|
Inserts a DataFrame into a specified table and returns the last inserted row's ID.
|
||||||
|
|
||||||
:param df: DataFrame to insert.
|
:param df: DataFrame to insert.
|
||||||
:param table: Name of the table.
|
:param table: Name of the table.
|
||||||
|
:return: The auto-incremented ID of the last inserted row.
|
||||||
"""
|
"""
|
||||||
with SQLite(self.db_file) as con:
|
with SQLite(self.db_file) as con:
|
||||||
|
# Insert the DataFrame into the specified table
|
||||||
df.to_sql(name=table, con=con, index=False, if_exists='append')
|
df.to_sql(name=table, con=con, index=False, if_exists='append')
|
||||||
|
|
||||||
def insert_row(self, table: str, columns: Tuple[str, ...], values: Tuple[Any, ...]) -> None:
|
# Fetch the last inserted row ID
|
||||||
|
cursor = con.execute('SELECT last_insert_rowid()')
|
||||||
|
last_id = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
return last_id
|
||||||
|
|
||||||
|
def insert_row(self, table: str, columns: Tuple[str, ...], values: Tuple[Any, ...]) -> int:
|
||||||
"""
|
"""
|
||||||
Inserts a row into a specified table.
|
Inserts a row into a specified table and returns the auto-incremented ID.
|
||||||
|
|
||||||
:param table: Name of the table.
|
:param table: Name of the table.
|
||||||
:param columns: Tuple of column names.
|
:param columns: Tuple of column names.
|
||||||
:param values: Tuple of values to insert.
|
:param values: Tuple of values to insert.
|
||||||
|
:return: The auto-incremented ID of the inserted row.
|
||||||
"""
|
"""
|
||||||
with SQLite(self.db_file) as conn:
|
with SQLite(self.db_file) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
sql = make_insert(table=table, columns=columns)
|
sql = make_insert(table=table, columns=columns)
|
||||||
cursor.execute(sql, values)
|
cursor.execute(sql, values)
|
||||||
|
|
||||||
|
# Return the auto-incremented ID
|
||||||
|
return cursor.lastrowid
|
||||||
|
|
||||||
def table_exists(self, table_name: str) -> bool:
|
def table_exists(self, table_name: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if a table exists in the database.
|
Checks if a table exists in the database.
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,26 @@
|
||||||
import json
|
import json
|
||||||
from DataCache_v2 import DataCache
|
from DataCache_v2 import DataCache
|
||||||
|
|
||||||
|
|
||||||
class Strategy:
|
class Strategy:
|
||||||
def __init__(self, **args):
|
def __init__(self, **args):
|
||||||
"""
|
"""
|
||||||
:param args: An object containing key_value pairs representing strategy attributes.
|
:param args: An object containing key_value pairs representing strategy attributes.
|
||||||
Strategy format is defined in strategies.js
|
Strategy format is defined in strategies.js
|
||||||
"""
|
"""
|
||||||
|
self.active = None
|
||||||
|
self.type = None
|
||||||
|
self.trade_amount = None
|
||||||
|
self.max_position = None
|
||||||
|
self.side = None
|
||||||
|
self.trd_in_conds = None
|
||||||
|
self.merged_loss = None
|
||||||
|
self.gross_loss = None
|
||||||
|
self.stop_loss = None
|
||||||
|
self.take_profit = None
|
||||||
|
self.gross_profit = None
|
||||||
|
self.merged_profit = None
|
||||||
|
self.name = None
|
||||||
self.current_value = None
|
self.current_value = None
|
||||||
self.opening_value = None
|
self.opening_value = None
|
||||||
self.gross_pl = None
|
self.gross_pl = None
|
||||||
|
|
@ -161,19 +175,22 @@ class Strategies:
|
||||||
# Reference to the trades object that maintains all trading actions and data.
|
# Reference to the trades object that maintains all trading actions and data.
|
||||||
self.trades = trades
|
self.trades = trades
|
||||||
|
|
||||||
|
self.strat_list = []
|
||||||
|
|
||||||
def get_all_strategy_names(self) -> list | None:
|
def get_all_strategy_names(self) -> list | None:
|
||||||
"""Return a list of all strategies in the database"""
|
"""Return a list of all strategies in the database"""
|
||||||
self.data._get_from_database()
|
# # Load existing Strategies from file.
|
||||||
# Load existing Strategies from file.
|
# loaded_strategies = self.data.get_setting('strategies')
|
||||||
loaded_strategies = config.get_setting('strategies')
|
# if loaded_strategies is None:
|
||||||
if loaded_strategies is None:
|
# # Populate the list and file with defaults defined in this class.
|
||||||
# Populate the list and file with defaults defined in this class.
|
# loaded_strategies = self.get_strategy_defaults()
|
||||||
loaded_strategies = self.get_strategy_defaults()
|
# config.set_setting('strategies', loaded_strategies)
|
||||||
config.set_setting('strategies', loaded_strategies)
|
#
|
||||||
|
# for entry in loaded_strategies:
|
||||||
|
# # Initialise all the strategy objects with data from file.
|
||||||
|
# self.strat_list.append(Strategy(**entry))
|
||||||
|
|
||||||
for entry in loaded_strategies:
|
return None
|
||||||
# Initialise all the strategy objects with data from file.
|
|
||||||
self.strat_list.append(Strategy(**entry)) return None
|
|
||||||
|
|
||||||
def new_strategy(self, data):
|
def new_strategy(self, data):
|
||||||
# Create an instance of the new Strategy.
|
# Create an instance of the new Strategy.
|
||||||
|
|
|
||||||
95
src/Users.py
95
src/Users.py
|
|
@ -4,7 +4,7 @@ import random
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from passlib.hash import bcrypt
|
from passlib.hash import bcrypt
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from DataCache_v2 import DataCache
|
from DataCache_v3 import DataCache
|
||||||
|
|
||||||
|
|
||||||
class BaseUser:
|
class BaseUser:
|
||||||
|
|
@ -28,9 +28,9 @@ class BaseUser:
|
||||||
:param user_name: The name of the user.
|
:param user_name: The name of the user.
|
||||||
:return: The ID of the user as an integer.
|
:return: The ID of the user as an integer.
|
||||||
"""
|
"""
|
||||||
return self.data.fetch_cached_item(
|
return self.data.fetch_item(
|
||||||
item_name='id',
|
item_name='id',
|
||||||
table_name='users',
|
cache_name='users',
|
||||||
filter_vals=('user_name', user_name)
|
filter_vals=('user_name', user_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -40,10 +40,10 @@ class BaseUser:
|
||||||
|
|
||||||
:param user_name: The name of the user to remove from the cache.
|
:param user_name: The name of the user to remove from the cache.
|
||||||
"""
|
"""
|
||||||
# Use DataCache to remove the user from the cache only
|
# Remove the user from the cache only
|
||||||
self.data.remove_row(
|
self.data.remove_row(
|
||||||
table='users',
|
cache_name='users',
|
||||||
filter_vals=('user_name', user_name)
|
filter_vals=('user_name', user_name), remove_from_db=False
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete_user(self, user_name: str) -> None:
|
def delete_user(self, user_name: str) -> None:
|
||||||
|
|
@ -54,7 +54,7 @@ class BaseUser:
|
||||||
"""
|
"""
|
||||||
self.data.remove_row(
|
self.data.remove_row(
|
||||||
filter_vals=('user_name', user_name),
|
filter_vals=('user_name', user_name),
|
||||||
table='users'
|
cache_name='users'
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_user_data(self, user_name: str) -> pd.DataFrame | None:
|
def get_user_data(self, user_name: str) -> pd.DataFrame | None:
|
||||||
|
|
@ -67,8 +67,8 @@ class BaseUser:
|
||||||
:raises ValueError: If the user is not found in both the cache and the database.
|
:raises ValueError: If the user is not found in both the cache and the database.
|
||||||
"""
|
"""
|
||||||
# Attempt to fetch the user data from the cache or database via DataCache
|
# Attempt to fetch the user data from the cache or database via DataCache
|
||||||
user = self.data.fetch_cached_rows(
|
user = self.data.get_or_fetch_rows(
|
||||||
table='users',
|
cache_name='users',
|
||||||
filter_vals=('user_name', user_name)
|
filter_vals=('user_name', user_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -86,8 +86,8 @@ class BaseUser:
|
||||||
:param new_data: The new data to be set.
|
:param new_data: The new data to be set.
|
||||||
"""
|
"""
|
||||||
# Use DataCache to modify the user's data
|
# Use DataCache to modify the user's data
|
||||||
self.data.modify_cached_row(
|
self.data.modify_item(
|
||||||
table='users',
|
cache_name='users',
|
||||||
filter_vals=('user_name', username),
|
filter_vals=('user_name', username),
|
||||||
field_name=field_name,
|
field_name=field_name,
|
||||||
new_data=new_data
|
new_data=new_data
|
||||||
|
|
@ -112,8 +112,8 @@ class UserAccountManagement(BaseUser):
|
||||||
self.max_guests = max_guests # Maximum number of guests
|
self.max_guests = max_guests # Maximum number of guests
|
||||||
|
|
||||||
# Initialize data for guest suffixes and cached users
|
# Initialize data for guest suffixes and cached users
|
||||||
self.data.set_cache(data=[], key='guest_suffixes', do_not_overwrite=True)
|
self.data.set_cache_item(data=[], key='guest_suffixes', do_not_overwrite=True)
|
||||||
self.data.set_cache(data={}, key='cached_users', do_not_overwrite=True)
|
self.data.set_cache_item(data={}, key='cached_users', do_not_overwrite=True)
|
||||||
|
|
||||||
def is_logged_in(self, user_name: str) -> bool:
|
def is_logged_in(self, user_name: str) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
@ -138,9 +138,9 @@ class UserAccountManagement(BaseUser):
|
||||||
# If the user is logged in, check if they are a guest.
|
# If the user is logged in, check if they are a guest.
|
||||||
if is_guest(user):
|
if is_guest(user):
|
||||||
# Update the guest suffix cache if the user is a guest.
|
# Update the guest suffix cache if the user is a guest.
|
||||||
guest_suffixes = self.data.get_cache('guest_suffixes')
|
guest_suffixes = self.data.get_cache_item(key='guest_suffixes') or []
|
||||||
guest_suffixes.append(user_name[1])
|
guest_suffixes.append(user_name.split('_')[1])
|
||||||
self.data.set_cache(data=guest_suffixes, key='guest_suffixes')
|
self.data.set_cache_item(data=guest_suffixes, key='guest_suffixes')
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# If the user is not logged in, remove their data from the cache.
|
# If the user is not logged in, remove their data from the cache.
|
||||||
|
|
@ -159,7 +159,7 @@ class UserAccountManagement(BaseUser):
|
||||||
:return: True if the password is correct, False otherwise.
|
:return: True if the password is correct, False otherwise.
|
||||||
"""
|
"""
|
||||||
# Retrieve the hashed password using DataCache
|
# Retrieve the hashed password using DataCache
|
||||||
user_data = self.data.fetch_cached_rows(table='users', filter_vals=('user_name', username))
|
user_data = self.data.get_or_fetch_rows(cache_name='users', filter_vals=('user_name', username))
|
||||||
|
|
||||||
if user_data is None or user_data.empty:
|
if user_data is None or user_data.empty:
|
||||||
return False
|
return False
|
||||||
|
|
@ -215,23 +215,24 @@ class UserAccountManagement(BaseUser):
|
||||||
|
|
||||||
:param enforcement: 'soft' or 'hard' - Determines how strictly users are logged out.
|
:param enforcement: 'soft' or 'hard' - Determines how strictly users are logged out.
|
||||||
"""
|
"""
|
||||||
if enforcement == 'soft':
|
# if enforcement == 'soft':
|
||||||
self._soft_log_out_all_users()
|
# self._soft_log_out_all_users()
|
||||||
elif enforcement == 'hard':
|
# elif enforcement == 'hard':
|
||||||
# Clear all user-related entries from the cache
|
# # Clear all user-related entries from the cache
|
||||||
for index, row in self.data.cache.iterrows():
|
# for index, row in self.data.cache.iterrows():
|
||||||
if 'user_name' in row:
|
# if 'user_name' in row:
|
||||||
self._remove_user_from_memory(row['user_name'])
|
# self._remove_user_from_memory(row['user_name'])
|
||||||
|
#
|
||||||
df = self.data.fetch_cached_rows(table='users', filter_vals=('status', 'logged_in'))
|
# df = self.data.get_or_fetch_rows(cache_name='users', filter_vals=('status', 'logged_in'))
|
||||||
if df is not None:
|
# if df is not None:
|
||||||
df = df[df.user_name != 'guest']
|
# df = df[df.user_name != 'guest']
|
||||||
|
#
|
||||||
# Update the status of all logged-in users to 'logged_out'
|
# # Update the status of all logged-in users to 'logged_out'
|
||||||
for user_name in df.user_name.values:
|
# for user_name in df.user_name.values:
|
||||||
self.modify_user_data(username=user_name, field_name='status', new_data='logged_out')
|
# self.modify_user_data(username=user_name, field_name='status', new_data='logged_out')
|
||||||
else:
|
# else:
|
||||||
raise ValueError("Invalid enforcement type. Use 'soft' or 'hard'.")
|
# raise ValueError("Invalid enforcement type. Use 'soft' or 'hard'.")
|
||||||
|
pass
|
||||||
|
|
||||||
def _soft_log_out_all_users(self) -> None:
|
def _soft_log_out_all_users(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -250,7 +251,7 @@ class UserAccountManagement(BaseUser):
|
||||||
:return: True if the attribute is already taken, False otherwise.
|
:return: True if the attribute is already taken, False otherwise.
|
||||||
"""
|
"""
|
||||||
# Use DataCache to check if the attribute is taken
|
# Use DataCache to check if the attribute is taken
|
||||||
return self.data.is_attr_taken(table='users', attr=attr, val=val)
|
return self.data.is_attr_taken(cache_name='users', attr=attr, val=val)
|
||||||
|
|
||||||
def create_unique_guest_name(self) -> str | None:
|
def create_unique_guest_name(self) -> str | None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -258,12 +259,16 @@ class UserAccountManagement(BaseUser):
|
||||||
|
|
||||||
:return: A unique guest username or None if the guest limit is reached.
|
:return: A unique guest username or None if the guest limit is reached.
|
||||||
"""
|
"""
|
||||||
guest_suffixes = self.data.get_cache('guest_suffixes')
|
guest_suffixes = self.data.get_cache_item(key='guest_suffixes') or []
|
||||||
if len(guest_suffixes) > self.max_guests:
|
if len(guest_suffixes) >= self.max_guests:
|
||||||
return None
|
return None
|
||||||
suffix = random.choice(range(0, (self.max_guests * 9)))
|
|
||||||
|
suffix = random.choice(range(0, self.max_guests * 9))
|
||||||
while suffix in guest_suffixes:
|
while suffix in guest_suffixes:
|
||||||
suffix = random.choice(range(0, (self.max_guests * 9)))
|
suffix = random.choice(range(0, self.max_guests * 9))
|
||||||
|
|
||||||
|
guest_suffixes.append(suffix)
|
||||||
|
self.data.set_cache_item(key='guest_suffixes', data=guest_suffixes)
|
||||||
return f'guest_{suffix}'
|
return f'guest_{suffix}'
|
||||||
|
|
||||||
def create_guest(self) -> str | None:
|
def create_guest(self) -> str | None:
|
||||||
|
|
@ -292,7 +297,7 @@ class UserAccountManagement(BaseUser):
|
||||||
raise ValueError("Attributes must be a tuple of single key-value pair dictionaries.")
|
raise ValueError("Attributes must be a tuple of single key-value pair dictionaries.")
|
||||||
|
|
||||||
# Retrieve the default user template from the database using DataCache
|
# Retrieve the default user template from the database using DataCache
|
||||||
default_user = self.data.fetch_cached_rows(table='users', filter_vals=('user_name', 'guest'))
|
default_user = self.data.get_or_fetch_rows(cache_name='users', filter_vals=('user_name', 'guest'))
|
||||||
|
|
||||||
if default_user is None or default_user.empty:
|
if default_user is None or default_user.empty:
|
||||||
raise ValueError("Default user template not found in the database.")
|
raise ValueError("Default user template not found in the database.")
|
||||||
|
|
@ -306,7 +311,7 @@ class UserAccountManagement(BaseUser):
|
||||||
default_user = default_user.drop(columns='id')
|
default_user = default_user.drop(columns='id')
|
||||||
|
|
||||||
# Insert the modified user data into the database, skipping cache insertion
|
# Insert the modified user data into the database, skipping cache insertion
|
||||||
self.data.insert_data(df=default_user, table="users", skip_cache=True)
|
self.data.insert_df(df=default_user, cache_name="users", skip_cache=True)
|
||||||
|
|
||||||
def create_new_user(self, username: str, email: str, password: str) -> bool:
|
def create_new_user(self, username: str, email: str, password: str) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
@ -456,7 +461,7 @@ class UserIndicatorManagement(UserExchangeManagement):
|
||||||
user_id = self.get_id(user_name)
|
user_id = self.get_id(user_name)
|
||||||
|
|
||||||
# Fetch the indicators from the database using DataCache
|
# Fetch the indicators from the database using DataCache
|
||||||
df = self.data.fetch_cached_rows(table='indicators', filter_vals=('creator', user_id))
|
df = self.data.get_or_fetch_rows(cache_name='indicators', filter_vals=('creator', user_id))
|
||||||
|
|
||||||
# If indicators are found, process the JSON fields
|
# If indicators are found, process the JSON fields
|
||||||
if df is not None and not df.empty:
|
if df is not None and not df.empty:
|
||||||
|
|
@ -481,8 +486,8 @@ class UserIndicatorManagement(UserExchangeManagement):
|
||||||
indicator['kind'], src_string, prop_string)
|
indicator['kind'], src_string, prop_string)
|
||||||
columns = ('creator', 'name', 'visible', 'kind', 'source', 'properties')
|
columns = ('creator', 'name', 'visible', 'kind', 'source', 'properties')
|
||||||
|
|
||||||
# Insert the row into the database using DataCache
|
# Insert the row into the database and cache using DataCache
|
||||||
self.data.insert_row(table='indicators', columns=columns, values=values)
|
self.data.insert_row(cache_name='indicators', columns=columns, values=values)
|
||||||
|
|
||||||
def remove_indicator(self, indicator_name: str, user_name: str) -> None:
|
def remove_indicator(self, indicator_name: str, user_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -495,7 +500,7 @@ class UserIndicatorManagement(UserExchangeManagement):
|
||||||
self.data.remove_row(
|
self.data.remove_row(
|
||||||
filter_vals=('name', indicator_name),
|
filter_vals=('name', indicator_name),
|
||||||
additional_filter=('creator', user_id),
|
additional_filter=('creator', user_id),
|
||||||
table='indicators'
|
cache_name='indicators'
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_chart_view(self, user_name: str, prop: str | None = None):
|
def get_chart_view(self, user_name: str, prop: str | None = None):
|
||||||
|
|
|
||||||
12
src/trade.py
12
src/trade.py
|
|
@ -1,6 +1,6 @@
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
|
from Users import Users
|
||||||
import requests
|
import requests
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
@ -267,7 +267,7 @@ class Trade:
|
||||||
|
|
||||||
|
|
||||||
class Trades:
|
class Trades:
|
||||||
def __init__(self, users):
|
def __init__(self, users: Users):
|
||||||
"""
|
"""
|
||||||
This class receives, executes, tracks and stores all active_trades.
|
This class receives, executes, tracks and stores all active_trades.
|
||||||
:param users: <Users> A class that maintains users each user may have trades.
|
:param users: <Users> A class that maintains users each user may have trades.
|
||||||
|
|
@ -291,10 +291,10 @@ class Trades:
|
||||||
self.stats = {'num_trades': 0, 'total_position': 0, 'total_position_value': 0}
|
self.stats = {'num_trades': 0, 'total_position': 0, 'total_position_value': 0}
|
||||||
|
|
||||||
# Load all trades.
|
# Load all trades.
|
||||||
loaded_trades = users.get_all_active_user_trades()
|
# loaded_trades = users.get_all_active_user_trades()
|
||||||
if loaded_trades is not None:
|
# if loaded_trades is not None:
|
||||||
# Create the active_trades loaded from file.
|
# # Create the active_trades loaded from file.
|
||||||
self.load_trades(loaded_trades)
|
# self.load_trades(loaded_trades)
|
||||||
|
|
||||||
def connect_exchanges(self, exchanges):
|
def connect_exchanges(self, exchanges):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
import time
|
||||||
import pytz
|
import pytz
|
||||||
from DataCache_v2 import DataCache, timeframe_to_timedelta, estimate_record_count
|
from DataCache_v3 import DataCache, timeframe_to_timedelta, estimate_record_count, InMemoryCache, DataCacheBase, \
|
||||||
|
SnapshotDataCache
|
||||||
from ExchangeInterface import ExchangeInterface
|
from ExchangeInterface import ExchangeInterface
|
||||||
import unittest
|
import unittest
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
@ -191,7 +193,7 @@ class DataGenerator:
|
||||||
return dt_obj
|
return dt_obj
|
||||||
|
|
||||||
|
|
||||||
class TestDataCacheV2(unittest.TestCase):
|
class TestDataCache(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# Set up database and exchanges
|
# Set up database and exchanges
|
||||||
self.exchanges = ExchangeInterface()
|
self.exchanges = ExchangeInterface()
|
||||||
|
|
@ -229,11 +231,16 @@ class TestDataCacheV2(unittest.TestCase):
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
data TEXT NOT NULL
|
data TEXT NOT NULL
|
||||||
)"""
|
)"""
|
||||||
sql_create_table_5 = f"""
|
sql_create_table_5 = """
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
users_data TEXT PRIMARY KEY,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
data TEXT NOT NULL
|
user_name TEXT,
|
||||||
)"""
|
age INTEGER,
|
||||||
|
users_data TEXT,
|
||||||
|
data TEXT,
|
||||||
|
password TEXT -- Moved to a new line and added a comma after 'data'
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
with SQLite(db_file=self.db_file) as con:
|
with SQLite(db_file=self.db_file) as con:
|
||||||
con.execute(sql_create_table_1)
|
con.execute(sql_create_table_1)
|
||||||
|
|
@ -251,44 +258,188 @@ class TestDataCacheV2(unittest.TestCase):
|
||||||
if os.path.exists(self.db_file):
|
if os.path.exists(self.db_file):
|
||||||
os.remove(self.db_file)
|
os.remove(self.db_file)
|
||||||
|
|
||||||
def test_set_cache(self):
|
def test_InMemoryCache(self):
|
||||||
print('\nTesting set_cache() method without no-overwrite flag:')
|
# Step 1: Create a cache with a limit of 2 items and 'evict' policy
|
||||||
self.data.set_cache(data='data', key=self.key)
|
print("Creating a cache with a limit of 2 items and 'evict' policy.")
|
||||||
|
cached_users = InMemoryCache(limit=2, eviction_policy='evict')
|
||||||
|
|
||||||
# Access the cache data using the DataFrame structure
|
# Step 2: Set some items in the cache.
|
||||||
cached_value = self.data.get_cache(key=self.key)
|
print("Setting 'user_bob' in the cache with an expiration of 10 seconds.")
|
||||||
self.assertEqual(cached_value, 'data')
|
cached_users.set_item("user_bob", "{password:'BobPass'}", expire_delta=dt.timedelta(seconds=10))
|
||||||
print(' - Set data without no-overwrite flag passed.')
|
|
||||||
|
|
||||||
print('Testing set_cache() once again with new data without no-overwrite flag:')
|
print("Setting 'user_alice' in the cache with an expiration of 20 seconds.")
|
||||||
self.data.set_cache(data='more_data', key=self.key)
|
cached_users.set_item("user_alice", "{password:'AlicePass'}", expire_delta=dt.timedelta(seconds=20))
|
||||||
|
|
||||||
# Access the updated cache data
|
# Step 3: Retrieve 'user_bob' from the cache
|
||||||
cached_value = self.data.get_cache(key=self.key)
|
print("Retrieving 'user_bob' from the cache.")
|
||||||
self.assertEqual(cached_value, 'more_data')
|
retrieved_item = cached_users.get_item('user_bob')
|
||||||
print(' - Set data with new data without no-overwrite flag passed.')
|
print(f"Retrieved: {retrieved_item}")
|
||||||
|
assert retrieved_item == "{password:'BobPass'}", "user_bob should have been retrieved successfully."
|
||||||
|
|
||||||
print('Testing set_cache() method once again with more data with no-overwrite flag set:')
|
# Step 4: Add another item, causing the oldest item to be evicted
|
||||||
self.data.set_cache(data='even_more_data', key=self.key, do_not_overwrite=True)
|
print("Adding 'user_billy' to the cache, which should evict 'user_bob' due to the limit.")
|
||||||
|
cached_users.set_item("user_billy", "{password:'BillyPass'}")
|
||||||
|
|
||||||
# Since do_not_overwrite is True, the cached data should not change
|
# Step 5: Attempt to retrieve the evicted item 'user_bob'
|
||||||
cached_value = self.data.get_cache(key=self.key)
|
print("Attempting to retrieve the evicted item 'user_bob'.")
|
||||||
self.assertEqual(cached_value, 'more_data')
|
evicted_item = cached_users.get_item('user_bob')
|
||||||
print(' - Set data with no-overwrite flag passed.')
|
print(f"Evicted Item: {evicted_item}")
|
||||||
|
assert evicted_item is None, "user_bob should have been evicted from the cache."
|
||||||
|
|
||||||
def test_cache_exists(self):
|
# Step 6: Retrieve the current items in the cache
|
||||||
print('Testing cache_exists() method:')
|
print("Retrieving all current items in the cache after eviction.")
|
||||||
|
all_items = cached_users.get_all_items()
|
||||||
|
print("Current items in cache:\n", all_items)
|
||||||
|
assert "user_alice" in all_items['key'].values, "user_alice should still be in the cache."
|
||||||
|
assert "user_billy" in all_items['key'].values, "user_billy should still be in the cache."
|
||||||
|
|
||||||
# Check that the cache does not contain the key before setting it
|
# Step 7: Simulate waiting for 'user_alice' to expire (assuming 20 seconds pass)
|
||||||
self.assertFalse(self.data.cache_exists(key=self.key))
|
print("Simulating time passing to expire 'user_alice' (20 seconds).")
|
||||||
print(' - Check for non-existent data passed.')
|
time.sleep(20) # This is to simulate the passage of time; in real tests, you may mock datetime.
|
||||||
|
|
||||||
# Set the cache with a DataFrame containing the key-value pair
|
# Step 8: Clean expired items from the cache
|
||||||
self.data.set_cache(data='data', key=self.key)
|
print("Cleaning expired items from the cache.")
|
||||||
|
cached_users.clean_expired_items()
|
||||||
|
|
||||||
# Check that the cache now contains the key
|
# Step 9: Retrieve the current items in the cache after cleaning expired items
|
||||||
self.assertTrue(self.data.cache_exists(key=self.key))
|
print("Retrieving all current items in the cache after cleaning expired items.")
|
||||||
print(' - Check for existent data passed.')
|
all_items_after_cleaning = cached_users.get_all_items()
|
||||||
|
print("Current items in cache after cleaning:\n", all_items_after_cleaning)
|
||||||
|
assert "user_alice" not in all_items_after_cleaning[
|
||||||
|
'key'].values, "user_alice should have been expired and removed from the cache."
|
||||||
|
assert "user_billy" in all_items_after_cleaning['key'].values, "user_billy should still be in the cache."
|
||||||
|
|
||||||
|
# Step 10: Check if 'user_billy' still exists as it should not expire
|
||||||
|
print("Checking if 'user_billy' still exists in the cache (it should not have expired).")
|
||||||
|
user_billy_item = cached_users.get_item('user_billy')
|
||||||
|
print(f"'user_billy' still exists: {user_billy_item}")
|
||||||
|
assert user_billy_item == "{password:'BillyPass'}", "user_billy should still exist in the cache."
|
||||||
|
|
||||||
|
def test_DataCacheBase(self):
|
||||||
|
# Step 1: Create a DataCacheBase instance
|
||||||
|
print("Creating a DataCacheBase instance.")
|
||||||
|
cache_manager = DataCacheBase()
|
||||||
|
|
||||||
|
# Step 2: Set some items in 'my_cache'. The cache is created automatically with limit 2 and 'evict' policy.
|
||||||
|
print("Setting 'key1' in 'my_cache' with an expiration of 10 seconds.")
|
||||||
|
cache_manager.set_cache_item('key1', 'data1', expire_delta=dt.timedelta(seconds=10), cache_name='my_cache',
|
||||||
|
limit=2, eviction_policy='evict')
|
||||||
|
|
||||||
|
print("Setting 'key2' in 'my_cache' with an expiration of 20 seconds.")
|
||||||
|
cache_manager.set_cache_item('key2', 'data2', expire_delta=dt.timedelta(seconds=20), cache_name='my_cache')
|
||||||
|
|
||||||
|
# Step 3: Set some items in 'second_cache'. The cache is created automatically with limit 3 and 'deny' policy.
|
||||||
|
print("Setting 'keyA' in 'second_cache' with an expiration of 15 seconds.")
|
||||||
|
cache_manager.set_cache_item('keyA', 'dataA', expire_delta=dt.timedelta(seconds=15), cache_name='second_cache',
|
||||||
|
limit=3, eviction_policy='deny')
|
||||||
|
|
||||||
|
print("Setting 'keyB' in 'second_cache' with an expiration of 30 seconds.")
|
||||||
|
cache_manager.set_cache_item('keyB', 'dataB', expire_delta=dt.timedelta(seconds=30), cache_name='second_cache')
|
||||||
|
|
||||||
|
print("Setting 'keyC' in 'second_cache' with no expiration.")
|
||||||
|
cache_manager.set_cache_item('keyC', 'dataC', cache_name='second_cache')
|
||||||
|
|
||||||
|
# Step 4: Add another item to 'my_cache', causing the oldest item to be evicted.
|
||||||
|
print("Adding 'key3' to 'my_cache', which should evict 'key1' due to the limit.")
|
||||||
|
cache_manager.set_cache_item('key3', 'data3', cache_name='my_cache')
|
||||||
|
|
||||||
|
# Step 5: Attempt to retrieve the evicted item 'key1' from 'my_cache'.
|
||||||
|
print("Attempting to retrieve the evicted item 'key1' from 'my_cache'.")
|
||||||
|
evicted_item = cache_manager.get_cache_item('key1', cache_name='my_cache')
|
||||||
|
print(f"Evicted Item from 'my_cache': {evicted_item}")
|
||||||
|
assert evicted_item is None, "'key1' should have been evicted from 'my_cache'."
|
||||||
|
|
||||||
|
# Step 6: Retrieve all current items in both caches before cleaning.
|
||||||
|
print("Retrieving all current items in 'my_cache' before cleaning.")
|
||||||
|
all_items_my_cache = cache_manager.get_all_cache_items('my_cache')
|
||||||
|
print("Current items in 'my_cache':\n", all_items_my_cache)
|
||||||
|
|
||||||
|
print("Retrieving all current items in 'second_cache' before cleaning.")
|
||||||
|
all_items_second_cache = cache_manager.get_all_cache_items('second_cache')
|
||||||
|
print("Current items in 'second_cache':\n", all_items_second_cache)
|
||||||
|
|
||||||
|
# Step 7: Simulate time passing to expire 'key2' in 'my_cache' and 'keyA' in 'second_cache'.
|
||||||
|
print("Simulating time passing to expire 'key2' in 'my_cache' (20 seconds)"
|
||||||
|
" and 'keyA' in 'second_cache' (15 seconds).")
|
||||||
|
time.sleep(20) # Simulate the passage of time; in real tests, you may mock datetime.
|
||||||
|
|
||||||
|
# Step 8: Clean expired items in all caches
|
||||||
|
print("Cleaning expired items in all caches.")
|
||||||
|
cache_manager.clean_expired_items()
|
||||||
|
|
||||||
|
# Step 9: Verify the cleaning of expired items in 'my_cache'.
|
||||||
|
print("Retrieving all current items in 'my_cache' after cleaning expired items.")
|
||||||
|
all_items_after_cleaning_my_cache = cache_manager.get_all_cache_items('my_cache')
|
||||||
|
print("Items in 'my_cache' after cleaning:\n", all_items_after_cleaning_my_cache)
|
||||||
|
assert 'key2' not in all_items_after_cleaning_my_cache[
|
||||||
|
'key'].values, "'key2' should have been expired and removed from 'my_cache'."
|
||||||
|
assert 'key3' in all_items_after_cleaning_my_cache['key'].values, "'key3' should still be in 'my_cache'."
|
||||||
|
|
||||||
|
# Step 10: Verify the cleaning of expired items in 'second_cache'.
|
||||||
|
print("Retrieving all current items in 'second_cache' after cleaning expired items.")
|
||||||
|
all_items_after_cleaning_second_cache = cache_manager.get_all_cache_items('second_cache')
|
||||||
|
print("Items in 'second_cache' after cleaning:\n", all_items_after_cleaning_second_cache)
|
||||||
|
assert 'keyA' not in all_items_after_cleaning_second_cache[
|
||||||
|
'key'].values, "'keyA' should have been expired and removed from 'second_cache'."
|
||||||
|
assert 'keyB' in all_items_after_cleaning_second_cache[
|
||||||
|
'key'].values, "'keyB' should still be in 'second_cache'."
|
||||||
|
assert 'keyC' in all_items_after_cleaning_second_cache[
|
||||||
|
'key'].values, "'keyC' should still be in 'second_cache' since it has no expiration."
|
||||||
|
|
||||||
|
def test_SnapshotDataCache(self):
|
||||||
|
# Step 1: Create a SnapshotDataCache instance
|
||||||
|
print("Creating a SnapshotDataCache instance.")
|
||||||
|
snapshot_cache_manager = SnapshotDataCache()
|
||||||
|
|
||||||
|
# Step 2: Create an in-memory cache with a limit of 2 items and 'evict' policy
|
||||||
|
print("Creating an in-memory cache named 'my_cache' with a limit of 2 items and 'evict' policy.")
|
||||||
|
snapshot_cache_manager.create_cache('my_cache', cache_type=InMemoryCache, limit=2, eviction_policy='evict')
|
||||||
|
|
||||||
|
# Step 3: Set some items in the cache
|
||||||
|
print("Setting 'key1' in 'my_cache' with an expiration of 10 seconds.")
|
||||||
|
snapshot_cache_manager.set_cache_item(key='key1', data='data1', expire_delta=dt.timedelta(seconds=10),
|
||||||
|
cache_name='my_cache')
|
||||||
|
|
||||||
|
print("Setting 'key2' in 'my_cache' with an expiration of 20 seconds.")
|
||||||
|
snapshot_cache_manager.set_cache_item(key='key2', data='data2', expire_delta=dt.timedelta(seconds=20),
|
||||||
|
cache_name='my_cache')
|
||||||
|
|
||||||
|
# Step 4: Take a snapshot of the current state of 'my_cache'
|
||||||
|
print("Taking a snapshot of the current state of 'my_cache'.")
|
||||||
|
snapshot_cache_manager.snapshot_cache('my_cache')
|
||||||
|
|
||||||
|
# Step 5: Add another item, causing the oldest item to be evicted
|
||||||
|
print("Adding 'key3' to 'my_cache', which should evict 'key1' due to the limit.")
|
||||||
|
snapshot_cache_manager.set_cache_item(key='key3', data='data3', cache_name='my_cache')
|
||||||
|
|
||||||
|
# Step 6: Retrieve the most recent snapshot of 'my_cache'
|
||||||
|
print("Retrieving the most recent snapshot of 'my_cache'.")
|
||||||
|
snapshot = snapshot_cache_manager.get_snapshot('my_cache')
|
||||||
|
print(f"Snapshot Data:\n{snapshot}")
|
||||||
|
|
||||||
|
# Assert that the snapshot contains 'key1' and 'key2', but not 'key3'
|
||||||
|
assert 'key1' in snapshot['key'].values, "'key1' should be in the snapshot."
|
||||||
|
assert 'key2' in snapshot['key'].values, "'key2' should be in the snapshot."
|
||||||
|
assert 'key3' not in snapshot[
|
||||||
|
'key'].values, "'key3' should not be in the snapshot as it was added after the snapshot."
|
||||||
|
|
||||||
|
# Step 7: List all available snapshots with their timestamps
|
||||||
|
print("Listing all available snapshots with their timestamps.")
|
||||||
|
snapshots_list = snapshot_cache_manager.list_snapshots()
|
||||||
|
print(f"Snapshots List: {snapshots_list}")
|
||||||
|
|
||||||
|
# Assert that the snapshot list contains 'my_cache'
|
||||||
|
assert 'my_cache' in snapshots_list, "'my_cache' should be in the snapshots list."
|
||||||
|
assert isinstance(snapshots_list['my_cache'], str), "The snapshot for 'my_cache' should have a timestamp."
|
||||||
|
|
||||||
|
# Additional validation: Ensure 'key3' is present in the live cache but not in the snapshot
|
||||||
|
print("Ensuring 'key3' is present in the live 'my_cache'.")
|
||||||
|
live_cache_items = snapshot_cache_manager.get_all_cache_items('my_cache')
|
||||||
|
print(f"Live 'my_cache' items after adding 'key3':\n{live_cache_items}")
|
||||||
|
assert 'key3' in live_cache_items['key'].values, "'key3' should be in the live cache."
|
||||||
|
|
||||||
|
# Ensure the live cache does not contain 'key1'
|
||||||
|
assert 'key1' not in live_cache_items['key'].values, "'key1' should have been evicted from the live cache."
|
||||||
|
|
||||||
def test_update_candle_cache(self):
|
def test_update_candle_cache(self):
|
||||||
print('Testing update_candle_cache() method:')
|
print('Testing update_candle_cache() method:')
|
||||||
|
|
@ -296,18 +447,18 @@ class TestDataCacheV2(unittest.TestCase):
|
||||||
# Initialize the DataGenerator with the 5-minute timeframe
|
# Initialize the DataGenerator with the 5-minute timeframe
|
||||||
data_gen = DataGenerator('5m')
|
data_gen = DataGenerator('5m')
|
||||||
|
|
||||||
# Create initial DataFrame and insert into cache
|
# Create initial DataFrame and insert it into the cache
|
||||||
df_initial = data_gen.create_table(num_rec=3, start=dt.datetime(2024, 8, 9, 0, 0, 0, tzinfo=dt.timezone.utc))
|
df_initial = data_gen.create_table(num_rec=3, start=dt.datetime(2024, 8, 9, 0, 0, 0, tzinfo=dt.timezone.utc))
|
||||||
print(f'Inserting this table into cache:\n{df_initial}\n')
|
print(f'Inserting this table into cache:\n{df_initial}\n')
|
||||||
self.data.set_cache(data=df_initial, key=self.key)
|
self.data.set_cache_item(key=self.key, data=df_initial, cache_name='candles')
|
||||||
|
|
||||||
# Create new DataFrame to be added to cache
|
# Create new DataFrame to be added to the cache
|
||||||
df_new = data_gen.create_table(num_rec=3, start=dt.datetime(2024, 8, 9, 0, 15, 0, tzinfo=dt.timezone.utc))
|
df_new = data_gen.create_table(num_rec=3, start=dt.datetime(2024, 8, 9, 0, 15, 0, tzinfo=dt.timezone.utc))
|
||||||
print(f'Updating cache with this table:\n{df_new}\n')
|
print(f'Updating cache with this table:\n{df_new}\n')
|
||||||
self.data._update_candle_cache(more_records=df_new, key=self.key)
|
self.data._update_candle_cache(more_records=df_new, key=self.key)
|
||||||
|
|
||||||
# Retrieve the resulting DataFrame from cache
|
# Retrieve the resulting DataFrame from the cache
|
||||||
result = self.data.get_cache(key=self.key)
|
result = self.data.get_cache_item(key=self.key, cache_name='candles')
|
||||||
print(f'The resulting table in cache is:\n{result}\n')
|
print(f'The resulting table in cache is:\n{result}\n')
|
||||||
|
|
||||||
# Create the expected DataFrame
|
# Create the expected DataFrame
|
||||||
|
|
@ -316,8 +467,7 @@ class TestDataCacheV2(unittest.TestCase):
|
||||||
|
|
||||||
# Assert that the open_time values in the result match those in the expected DataFrame, in order
|
# Assert that the open_time values in the result match those in the expected DataFrame, in order
|
||||||
assert result['open_time'].tolist() == expected['open_time'].tolist(), \
|
assert result['open_time'].tolist() == expected['open_time'].tolist(), \
|
||||||
f"open_time values in result are {result['open_time'].tolist()}" \
|
f"open_time values in result are {result['open_time'].tolist()} expected {expected['open_time'].tolist()}"
|
||||||
f" but expected {expected['open_time'].tolist()}"
|
|
||||||
|
|
||||||
print(f'The result open_time values match:\n{result["open_time"].tolist()}\n')
|
print(f'The result open_time values match:\n{result["open_time"].tolist()}\n')
|
||||||
print(' - Update cache with new records passed.')
|
print(' - Update cache with new records passed.')
|
||||||
|
|
@ -325,32 +475,25 @@ class TestDataCacheV2(unittest.TestCase):
|
||||||
def test_update_cached_dict(self):
|
def test_update_cached_dict(self):
|
||||||
print('Testing update_cached_dict() method:')
|
print('Testing update_cached_dict() method:')
|
||||||
|
|
||||||
# Set an empty dictionary in the cache for the specified key
|
# Step 1: Set an empty dictionary in the cache for the specified key
|
||||||
self.data.set_cache(data={}, key=self.key)
|
print(f'Setting an empty dictionary in the cache with key: {self.key}')
|
||||||
|
self.data.set_cache_item(data={}, key=self.key)
|
||||||
|
|
||||||
# Update the cached dictionary with a new key-value pair
|
# Step 2: Update the cached dictionary with a new key-value pair
|
||||||
self.data.update_cached_dict(cache_key=self.key, dict_key='sub_key', data='value')
|
print(f'Updating the cached dictionary with key: {self.key}, adding sub_key="sub_key" with value="value".')
|
||||||
|
self.data.update_cached_dict(cache_name='default_cache', cache_key=self.key, dict_key='sub_key', data='value')
|
||||||
|
|
||||||
# Retrieve the updated cache
|
# Step 3: Retrieve the updated cache
|
||||||
cache = self.data.get_cache(key=self.key)
|
print(f'Retrieving the updated dictionary from the cache with key: {self.key}')
|
||||||
|
cache = self.data.get_cache_item(key=self.key)
|
||||||
|
|
||||||
# Verify that the 'sub_key' in the cached dictionary has the correct value
|
# Step 4: Verify that the 'sub_key' in the cached dictionary has the correct value
|
||||||
|
print(f'Verifying that "sub_key" in the cached dictionary has the value "value".')
|
||||||
|
self.assertIsInstance(cache, dict, "The cache should be a dictionary.")
|
||||||
|
self.assertIn('sub_key', cache, "The 'sub_key' should be present in the cached dictionary.")
|
||||||
self.assertEqual(cache['sub_key'], 'value')
|
self.assertEqual(cache['sub_key'], 'value')
|
||||||
print(' - Update dictionary in cache passed.')
|
print(' - Update dictionary in cache passed.')
|
||||||
|
|
||||||
def test_get_cache(self):
|
|
||||||
print('Testing get_cache() method:')
|
|
||||||
|
|
||||||
# Set some data into the cache
|
|
||||||
self.data.set_cache(data='data', key=self.key)
|
|
||||||
|
|
||||||
# Retrieve the cached data using the get_cache method
|
|
||||||
result = self.data.get_cache(key=self.key)
|
|
||||||
|
|
||||||
# Verify that the result matches the data we set
|
|
||||||
self.assertEqual(result, 'data')
|
|
||||||
print(' - Retrieve data passed.')
|
|
||||||
|
|
||||||
def _test_get_records_since(self, set_cache=True, set_db=True, query_offset=None, num_rec=None, ex_details=None,
|
def _test_get_records_since(self, set_cache=True, set_db=True, query_offset=None, num_rec=None, ex_details=None,
|
||||||
simulate_scenarios=None):
|
simulate_scenarios=None):
|
||||||
"""
|
"""
|
||||||
|
|
@ -371,94 +514,70 @@ class TestDataCacheV2(unittest.TestCase):
|
||||||
|
|
||||||
print('Testing get_records_since() method:')
|
print('Testing get_records_since() method:')
|
||||||
|
|
||||||
# Use provided ex_details or fallback to the class attribute.
|
|
||||||
ex_details = ex_details or self.ex_details
|
ex_details = ex_details or self.ex_details
|
||||||
# Generate a data/database key using exchange details.
|
|
||||||
key = f'{ex_details[0]}_{ex_details[1]}_{ex_details[2]}'
|
key = f'{ex_details[0]}_{ex_details[1]}_{ex_details[2]}'
|
||||||
|
|
||||||
# Set default number of records if not provided.
|
|
||||||
num_rec = num_rec or 12
|
num_rec = num_rec or 12
|
||||||
table_timeframe = ex_details[1] # Extract timeframe from exchange details.
|
table_timeframe = ex_details[1]
|
||||||
|
|
||||||
# Initialize DataGenerator with the given timeframe.
|
|
||||||
data_gen = DataGenerator(table_timeframe)
|
data_gen = DataGenerator(table_timeframe)
|
||||||
|
|
||||||
if simulate_scenarios == 'not_enough_data':
|
if simulate_scenarios == 'not_enough_data':
|
||||||
# Set query_offset to a time earlier than the start of the table data.
|
|
||||||
query_offset = (num_rec + 5) * data_gen.timeframe_amount
|
query_offset = (num_rec + 5) * data_gen.timeframe_amount
|
||||||
else:
|
else:
|
||||||
# Default to querying for 1 record length less than the table duration.
|
|
||||||
query_offset = query_offset or (num_rec - 1) * data_gen.timeframe_amount
|
query_offset = query_offset or (num_rec - 1) * data_gen.timeframe_amount
|
||||||
|
|
||||||
if simulate_scenarios == 'incomplete_data':
|
if simulate_scenarios == 'incomplete_data':
|
||||||
# Set start time to generate fewer records than required.
|
|
||||||
start_time_for_data = data_gen.x_time_ago(num_rec * data_gen.timeframe_amount)
|
start_time_for_data = data_gen.x_time_ago(num_rec * data_gen.timeframe_amount)
|
||||||
num_rec = 5 # Set a smaller number of records to simulate incomplete data.
|
num_rec = 5
|
||||||
else:
|
else:
|
||||||
# No specific start time for data generation.
|
|
||||||
start_time_for_data = None
|
start_time_for_data = None
|
||||||
|
|
||||||
# Create the initial data table.
|
|
||||||
df_initial = data_gen.create_table(num_rec, start=start_time_for_data)
|
df_initial = data_gen.create_table(num_rec, start=start_time_for_data)
|
||||||
|
|
||||||
if simulate_scenarios == 'missing_section':
|
if simulate_scenarios == 'missing_section':
|
||||||
# Simulate missing section in the data by dropping records.
|
|
||||||
df_initial = data_gen.generate_missing_section(df_initial, drop_start=2, drop_end=5)
|
df_initial = data_gen.generate_missing_section(df_initial, drop_start=2, drop_end=5)
|
||||||
|
|
||||||
# Convert 'open_time' to datetime for better readability.
|
|
||||||
temp_df = df_initial.copy()
|
temp_df = df_initial.copy()
|
||||||
temp_df['open_time'] = pd.to_datetime(temp_df['open_time'], unit='ms')
|
temp_df['open_time'] = pd.to_datetime(temp_df['open_time'], unit='ms')
|
||||||
print(f'Table Created:\n{temp_df}')
|
print(f'Table Created:\n{temp_df}')
|
||||||
|
|
||||||
if set_cache:
|
if set_cache:
|
||||||
# Insert the generated table into the cache.
|
print('Ensuring the cache exists and then inserting table into the cache.')
|
||||||
print('Inserting table into the cache.')
|
self.data.set_cache_item(data=df_initial, key=key, cache_name='candles')
|
||||||
self.data.set_cache(data=df_initial, key=key)
|
|
||||||
|
|
||||||
if set_db:
|
if set_db:
|
||||||
# Insert the generated table into the database.
|
|
||||||
print('Inserting table into the database.')
|
print('Inserting table into the database.')
|
||||||
with SQLite(self.db_file) as con:
|
with SQLite(self.db_file) as con:
|
||||||
df_initial.to_sql(key, con, if_exists='replace', index=False)
|
df_initial.to_sql(key, con, if_exists='replace', index=False)
|
||||||
|
|
||||||
# Calculate the start time for querying the records.
|
|
||||||
start_datetime = data_gen.x_time_ago(query_offset)
|
start_datetime = data_gen.x_time_ago(query_offset)
|
||||||
|
|
||||||
# Ensure start_datetime is timezone-aware (UTC).
|
|
||||||
if start_datetime.tzinfo is None:
|
if start_datetime.tzinfo is None:
|
||||||
start_datetime = start_datetime.replace(tzinfo=dt.timezone.utc)
|
start_datetime = start_datetime.replace(tzinfo=dt.timezone.utc)
|
||||||
|
|
||||||
# Defaults to current time if not provided to get_records_since()
|
|
||||||
query_end_time = dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc)
|
query_end_time = dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc)
|
||||||
print(f'Requesting records from {start_datetime} to {query_end_time}')
|
print(f'Requesting records from {start_datetime} to {query_end_time}')
|
||||||
|
|
||||||
# Query the records since the calculated start time.
|
|
||||||
result = self.data.get_records_since(start_datetime=start_datetime, ex_details=ex_details)
|
result = self.data.get_records_since(start_datetime=start_datetime, ex_details=ex_details)
|
||||||
|
|
||||||
# Filter the initial data table to match the query time.
|
|
||||||
expected = df_initial[df_initial['open_time'] >= data_gen.unix_time_millis(start_datetime)].reset_index(
|
expected = df_initial[df_initial['open_time'] >= data_gen.unix_time_millis(start_datetime)].reset_index(
|
||||||
drop=True)
|
drop=True)
|
||||||
temp_df = expected.copy()
|
temp_df = expected.copy()
|
||||||
temp_df['open_time'] = pd.to_datetime(temp_df['open_time'], unit='ms')
|
temp_df['open_time'] = pd.to_datetime(temp_df['open_time'], unit='ms')
|
||||||
print(f'Expected table:\n{temp_df}')
|
print(f'Expected table:\n{temp_df}')
|
||||||
|
|
||||||
# Print the result from the query for comparison.
|
|
||||||
temp_df = result.copy()
|
temp_df = result.copy()
|
||||||
temp_df['open_time'] = pd.to_datetime(temp_df['open_time'], unit='ms')
|
temp_df['open_time'] = pd.to_datetime(temp_df['open_time'], unit='ms')
|
||||||
print(f'Resulting table:\n{temp_df}')
|
print(f'Resulting table:\n{temp_df}')
|
||||||
|
|
||||||
if simulate_scenarios in ['not_enough_data', 'incomplete_data', 'missing_section']:
|
if simulate_scenarios in ['not_enough_data', 'incomplete_data', 'missing_section']:
|
||||||
# Check that the result has more rows than the expected incomplete data.
|
|
||||||
assert result.shape[0] > expected.shape[
|
assert result.shape[0] > expected.shape[
|
||||||
0], "Result has fewer or equal rows compared to the incomplete data."
|
0], "Result has fewer or equal rows compared to the incomplete data."
|
||||||
print("\nThe returned DataFrame has filled in the missing data!")
|
print("\nThe returned DataFrame has filled in the missing data!")
|
||||||
else:
|
else:
|
||||||
# Ensure the result and expected dataframes match in shape and content.
|
|
||||||
assert result.shape == expected.shape, f"Shape mismatch: {result.shape} vs {expected.shape}"
|
assert result.shape == expected.shape, f"Shape mismatch: {result.shape} vs {expected.shape}"
|
||||||
pd.testing.assert_series_equal(result['open_time'], expected['open_time'], check_dtype=False)
|
pd.testing.assert_series_equal(result['open_time'], expected['open_time'], check_dtype=False)
|
||||||
print("\nThe DataFrames have the same shape and the 'open_time' columns match.")
|
print("\nThe DataFrames have the same shape and the 'open_time' columns match.")
|
||||||
|
|
||||||
# Verify that the oldest timestamp in the result is within the allowed time difference.
|
|
||||||
oldest_timestamp = pd.to_datetime(result['open_time'].min(), unit='ms').tz_localize('UTC')
|
oldest_timestamp = pd.to_datetime(result['open_time'].min(), unit='ms').tz_localize('UTC')
|
||||||
time_diff = oldest_timestamp - start_datetime
|
time_diff = oldest_timestamp - start_datetime
|
||||||
max_allowed_time_diff = dt.timedelta(**{data_gen.timeframe_unit: data_gen.timeframe_amount})
|
max_allowed_time_diff = dt.timedelta(**{data_gen.timeframe_unit: data_gen.timeframe_amount})
|
||||||
|
|
@ -469,7 +588,6 @@ class TestDataCacheV2(unittest.TestCase):
|
||||||
|
|
||||||
print(f'The first timestamp is {time_diff} from {start_datetime}')
|
print(f'The first timestamp is {time_diff} from {start_datetime}')
|
||||||
|
|
||||||
# Verify that the newest timestamp in the result is within the allowed time difference.
|
|
||||||
newest_timestamp = pd.to_datetime(result['open_time'].max(), unit='ms').tz_localize('UTC')
|
newest_timestamp = pd.to_datetime(result['open_time'].max(), unit='ms').tz_localize('UTC')
|
||||||
time_diff_end = abs(query_end_time - newest_timestamp)
|
time_diff_end = abs(query_end_time - newest_timestamp)
|
||||||
|
|
||||||
|
|
@ -505,6 +623,9 @@ class TestDataCacheV2(unittest.TestCase):
|
||||||
|
|
||||||
def test_other_timeframes(self):
|
def test_other_timeframes(self):
|
||||||
print('\nTest get_records_since with a different timeframe')
|
print('\nTest get_records_since with a different timeframe')
|
||||||
|
if 'candles' not in self.data.caches:
|
||||||
|
self.data.create_cache(cache_name='candles')
|
||||||
|
|
||||||
ex_details = ['BTC/USD', '15m', 'binance', 'test_guy']
|
ex_details = ['BTC/USD', '15m', 'binance', 'test_guy']
|
||||||
start_datetime = dt.datetime.now(dt.timezone.utc) - dt.timedelta(hours=2)
|
start_datetime = dt.datetime.now(dt.timezone.utc) - dt.timedelta(hours=2)
|
||||||
# Query the records since the calculated start time.
|
# Query the records since the calculated start time.
|
||||||
|
|
@ -573,37 +694,53 @@ class TestDataCacheV2(unittest.TestCase):
|
||||||
def test_remove_row(self):
|
def test_remove_row(self):
|
||||||
print('Testing remove_row() method:')
|
print('Testing remove_row() method:')
|
||||||
|
|
||||||
# Insert data into the cache with the expected columns
|
# Create a DataFrame to insert as the data
|
||||||
df = pd.DataFrame({
|
user_data = pd.DataFrame({
|
||||||
'key': [self.key],
|
'user_name': ['test_user'],
|
||||||
'data': ['test_data']
|
'password': ['test_password']
|
||||||
})
|
})
|
||||||
self.data.set_cache(data='test_data', key=self.key)
|
|
||||||
|
# Insert data into the cache
|
||||||
|
self.data.set_cache_item(
|
||||||
|
cache_name='users',
|
||||||
|
key='user1',
|
||||||
|
data=user_data
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure the data is in the cache
|
# Ensure the data is in the cache
|
||||||
self.assertTrue(self.data.cache_exists(self.key), "Data was not correctly inserted into the cache.")
|
cache_item = self.data.get_cache_item('user1', 'users')
|
||||||
|
self.assertIsNotNone(cache_item, "Data was not correctly inserted into the cache.")
|
||||||
|
|
||||||
|
# The cache_item is a DataFrame, so we access the 'user_name' column directly
|
||||||
|
self.assertEqual(cache_item['user_name'].iloc[0], 'test_user', "Inserted data is incorrect.")
|
||||||
|
|
||||||
# Remove the row from the cache only (soft delete)
|
# Remove the row from the cache only (soft delete)
|
||||||
self.data.remove_row(table='test_table_2', filter_vals=('key', self.key), remove_from_db=False)
|
self.data.remove_row(cache_name='users', filter_vals=('user_name', 'test_user'), remove_from_db=False)
|
||||||
|
|
||||||
# Verify the row has been removed from the cache
|
# Verify the row has been removed from the cache
|
||||||
self.assertFalse(self.data.cache_exists(self.key), "Row was not correctly removed from the cache.")
|
cache_item = self.data.get_cache_item('user1', 'users')
|
||||||
|
self.assertIsNone(cache_item, "Row was not correctly removed from the cache.")
|
||||||
|
|
||||||
# Reinsert the data for hard delete test
|
# Reinsert the data for hard delete test
|
||||||
self.data.set_cache(data='test_data', key=self.key)
|
self.data.set_cache_item(
|
||||||
|
cache_name='users',
|
||||||
|
key='user1',
|
||||||
|
data=user_data
|
||||||
|
)
|
||||||
|
|
||||||
# Mock database delete by adding the row to the database
|
# Mock database delete by adding the row to the database
|
||||||
self.data.db.insert_row(table='test_table_2', columns=('key', 'data'), values=(self.key, 'test_data'))
|
self.data.db.insert_row(table='users', columns=('user_name', 'password'), values=('test_user', 'test_password'))
|
||||||
|
|
||||||
# Remove the row from both cache and database (hard delete)
|
# Remove the row from both cache and database (hard delete)
|
||||||
self.data.remove_row(table='test_table_2', filter_vals=('key', self.key), remove_from_db=True)
|
self.data.remove_row(cache_name='users', filter_vals=('user_name', 'test_user'), remove_from_db=True)
|
||||||
|
|
||||||
# Verify the row has been removed from the cache
|
# Verify the row has been removed from the cache
|
||||||
self.assertFalse(self.data.cache_exists(self.key), "Row was not correctly removed from the cache.")
|
cache_item = self.data.get_cache_item('user1', 'users')
|
||||||
|
self.assertIsNone(cache_item, "Row was not correctly removed from the cache.")
|
||||||
|
|
||||||
# Verify the row has been removed from the database
|
# Verify the row has been removed from the database
|
||||||
with SQLite(self.db_file) as con:
|
with SQLite(self.db_file) as con:
|
||||||
result = pd.read_sql(f'SELECT * FROM test_table_2 WHERE key="{self.key}"', con)
|
result = pd.read_sql(f'SELECT * FROM users WHERE user_name="test_user"', con)
|
||||||
self.assertTrue(result.empty, "Row was not correctly removed from the database.")
|
self.assertTrue(result.empty, "Row was not correctly removed from the database.")
|
||||||
|
|
||||||
print(' - Remove row from cache and database passed.')
|
print(' - Remove row from cache and database passed.')
|
||||||
|
|
@ -662,80 +799,164 @@ class TestDataCacheV2(unittest.TestCase):
|
||||||
|
|
||||||
print(' - All estimate_record_count() tests passed.')
|
print(' - All estimate_record_count() tests passed.')
|
||||||
|
|
||||||
def test_fetch_cached_rows(self):
|
def test_get_or_fetch_rows(self):
|
||||||
print('Testing fetch_cached_rows() method:')
|
|
||||||
|
|
||||||
# Set up mock data in the cache
|
# Create a mock table in the cache with multiple entries
|
||||||
df = pd.DataFrame({
|
df1 = pd.DataFrame({
|
||||||
'table': ['test_table_2'],
|
'user_name': ['billy'],
|
||||||
'key': ['test_key'],
|
'password': ['1234'],
|
||||||
'data': ['test_data']
|
'exchanges': [['ex1', 'ex2', 'ex3']]
|
||||||
})
|
})
|
||||||
self.data.cache = pd.concat([self.data.cache, df])
|
|
||||||
|
|
||||||
# Test fetching from cache
|
df2 = pd.DataFrame({
|
||||||
result = self.data.fetch_cached_rows('test_table_2', ('key', 'test_key'))
|
'user_name': ['john'],
|
||||||
|
'password': ['5678'],
|
||||||
|
'exchanges': [['ex4', 'ex5', 'ex6']]
|
||||||
|
})
|
||||||
|
|
||||||
|
df3 = pd.DataFrame({
|
||||||
|
'user_name': ['alice'],
|
||||||
|
'password': ['91011'],
|
||||||
|
'exchanges': [['ex7', 'ex8', 'ex9']]
|
||||||
|
})
|
||||||
|
|
||||||
|
# Insert these DataFrames into the 'users' cache
|
||||||
|
self.data.create_cache('users', cache_type=InMemoryCache)
|
||||||
|
self.data.set_cache_item(key='user_billy', data=df1, cache_name='users')
|
||||||
|
self.data.set_cache_item(key='user_john', data=df2, cache_name='users')
|
||||||
|
self.data.set_cache_item(key='user_alice', data=df3, cache_name='users')
|
||||||
|
|
||||||
|
print('Testing get_or_fetch_rows() method:')
|
||||||
|
|
||||||
|
# Test fetching an existing user from the cache
|
||||||
|
result = self.data.get_or_fetch_rows('users', ('user_name', 'billy'))
|
||||||
self.assertIsInstance(result, pd.DataFrame, "Failed to fetch DataFrame from cache")
|
self.assertIsInstance(result, pd.DataFrame, "Failed to fetch DataFrame from cache")
|
||||||
self.assertFalse(result.empty, "The fetched DataFrame is empty")
|
self.assertFalse(result.empty, "The fetched DataFrame is empty")
|
||||||
self.assertEqual(result.iloc[0]['data'], 'test_data', "Incorrect data fetched from cache")
|
self.assertEqual(result.iloc[0]['password'], '1234', "Incorrect data fetched from cache")
|
||||||
|
|
||||||
# Test fetching from database (assuming the method calls it)
|
# Test fetching another user from the cache
|
||||||
# Here we would typically mock the database call
|
result = self.data.get_or_fetch_rows('users', ('user_name', 'john'))
|
||||||
# But since we're not doing I/O, we will skip that part
|
self.assertIsInstance(result, pd.DataFrame, "Failed to fetch DataFrame from cache")
|
||||||
print(' - Fetch from cache and database simulated.')
|
self.assertFalse(result.empty, "The fetched DataFrame is empty")
|
||||||
|
self.assertEqual(result.iloc[0]['password'], '5678', "Incorrect data fetched from cache")
|
||||||
|
|
||||||
|
# Test fetching a user that does not exist in the cache
|
||||||
|
result = self.data.get_or_fetch_rows('users', ('user_name', 'non_existent_user'))
|
||||||
|
|
||||||
|
# Check if result is None (indicating that no data was found)
|
||||||
|
self.assertIsNone(result, "Expected result to be None for a non-existent user")
|
||||||
|
|
||||||
|
print(' - Fetching rows from cache passed.')
|
||||||
|
|
||||||
def test_is_attr_taken(self):
|
def test_is_attr_taken(self):
|
||||||
print('Testing is_attr_taken() method:')
|
# Create a cache named 'users'
|
||||||
|
self.data.create_cache('users', cache_type=InMemoryCache)
|
||||||
|
|
||||||
# Set up mock data in the cache
|
# Create mock data for three users
|
||||||
df = pd.DataFrame({
|
user_data_1 = pd.DataFrame({
|
||||||
'table': ['users'],
|
'user_name': ['billy'],
|
||||||
'user_name': ['test_user'],
|
'password': ['1234'],
|
||||||
'data': ['test_data']
|
'exchanges': [['ex1', 'ex2', 'ex3']]
|
||||||
|
})
|
||||||
|
user_data_2 = pd.DataFrame({
|
||||||
|
'user_name': ['john'],
|
||||||
|
'password': ['5678'],
|
||||||
|
'exchanges': [['ex1', 'ex2', 'ex4']]
|
||||||
|
})
|
||||||
|
user_data_3 = pd.DataFrame({
|
||||||
|
'user_name': ['alice'],
|
||||||
|
'password': ['abcd'],
|
||||||
|
'exchanges': [['ex5', 'ex6', 'ex7']]
|
||||||
})
|
})
|
||||||
self.data.cache = pd.concat([self.data.cache, df])
|
|
||||||
|
|
||||||
# Test for existing attribute
|
# Insert mock data into the cache
|
||||||
result = self.data.is_attr_taken('users', 'user_name', 'test_user')
|
self.data.set_cache_item('user1', user_data_1, cache_name='users')
|
||||||
self.assertTrue(result, "Failed to detect existing attribute")
|
self.data.set_cache_item('user2', user_data_2, cache_name='users')
|
||||||
|
self.data.set_cache_item('user3', user_data_3, cache_name='users')
|
||||||
|
|
||||||
# Test for non-existing attribute
|
# Test when attribute value is taken
|
||||||
result = self.data.is_attr_taken('users', 'user_name', 'non_existing_user')
|
result_taken = self.data.is_attr_taken(cache_name='users', attr='user_name', val='billy')
|
||||||
self.assertFalse(result, "Incorrectly detected non-existing attribute")
|
self.assertTrue(result_taken, "Expected 'billy' to be taken, but it was not.")
|
||||||
|
|
||||||
print(' - All is_attr_taken() tests passed.')
|
# Test when attribute value is not taken
|
||||||
|
result_not_taken = self.data.is_attr_taken(cache_name='users', attr='user_name', val='charlie')
|
||||||
|
self.assertFalse(result_not_taken, "Expected 'charlie' not to be taken, but it was.")
|
||||||
|
|
||||||
def test_insert_data(self):
|
def test_insert_df(self):
|
||||||
print('Testing insert_data() method:')
|
print('Testing insert_df() method:')
|
||||||
|
|
||||||
# Create a DataFrame to insert
|
# Create a DataFrame to insert
|
||||||
df = pd.DataFrame({
|
df = pd.DataFrame({
|
||||||
'key': ['new_key'],
|
'user_name': ['Alice'],
|
||||||
'data': ['new_data']
|
'age': [30],
|
||||||
|
'users_data': ['user_data_1'],
|
||||||
|
'data': ['additional_data'],
|
||||||
|
'password': ['1234']
|
||||||
})
|
})
|
||||||
|
|
||||||
# Insert data into the database and cache
|
# Insert data into the database and cache
|
||||||
self.data.insert_data(df=df, table='test_table_2')
|
self.data.insert_df(df=df, cache_name='users')
|
||||||
|
|
||||||
# Verify that the data was added to the cache
|
# Assume the database will return an auto-incremented ID starting at 1
|
||||||
cached_value = self.data.get_cache('new_key')
|
auto_incremented_id = 1
|
||||||
self.assertEqual(cached_value, 'new_data', "Failed to insert data into cache")
|
|
||||||
|
|
||||||
# Normally, we would also verify that the data was inserted into the database
|
# Verify that the data was added to the cache using the auto-incremented ID as the key
|
||||||
# This would typically be done with a mock database or by checking the database state directly
|
cached_df = self.data.get_cache_item(key=str(auto_incremented_id), cache_name='users')
|
||||||
print(' - Data insertion into cache and database simulated.')
|
|
||||||
|
# Check that the DataFrame in the cache matches the original DataFrame
|
||||||
|
pd.testing.assert_frame_equal(cached_df, df, check_dtype=False)
|
||||||
|
|
||||||
|
# Now, let's verify the data was inserted into the database
|
||||||
|
with SQLite(self.data.db.db_file) as conn:
|
||||||
|
# Query the users table for the inserted data
|
||||||
|
query_result = pd.read_sql_query(f"SELECT * FROM users WHERE id = {auto_incremented_id}", conn)
|
||||||
|
|
||||||
|
# Verify the database content matches the inserted DataFrame
|
||||||
|
expected_db_df = df.copy()
|
||||||
|
expected_db_df['id'] = auto_incremented_id # Add the auto-incremented ID to the expected DataFrame
|
||||||
|
# Align column order
|
||||||
|
expected_db_df = expected_db_df[['id', 'user_name', 'age', 'users_data', 'data', 'password']]
|
||||||
|
|
||||||
|
# Check that the database DataFrame matches the expected DataFrame
|
||||||
|
pd.testing.assert_frame_equal(query_result, expected_db_df, check_dtype=False)
|
||||||
|
|
||||||
|
print(' - Data insertion into cache and database verified successfully.')
|
||||||
|
|
||||||
def test_insert_row(self):
|
def test_insert_row(self):
|
||||||
print('Testing insert_row() method:')
|
print("Testing insert_row() method:")
|
||||||
|
|
||||||
self.data.insert_row(table='test_table_2', columns=('key', 'data'), values=('test_key', 'test_data'))
|
# Define the cache name, columns, and values to insert
|
||||||
|
cache_name = 'users'
|
||||||
|
columns = ('user_name', 'age')
|
||||||
|
values = ('Alice', 30)
|
||||||
|
|
||||||
# Verify the row was inserted
|
# Create the cache first
|
||||||
with SQLite(self.db_file) as con:
|
self.data.create_cache(cache_name, cache_type=InMemoryCache)
|
||||||
result = pd.read_sql('SELECT * FROM test_table_2 WHERE key="test_key"', con)
|
|
||||||
self.assertFalse(result.empty, "Row was not inserted into the database.")
|
|
||||||
|
|
||||||
print(' - Insert row passed.')
|
# Insert a row into the cache and database without skipping the cache
|
||||||
|
self.data.insert_row(cache_name=cache_name, columns=columns, values=values, skip_cache=False)
|
||||||
|
|
||||||
|
# Retrieve the inserted item from the cache
|
||||||
|
result = self.data.get_cache_item(key='1', cache_name=cache_name)
|
||||||
|
|
||||||
|
# Assert that the data in the cache matches what was inserted
|
||||||
|
self.assertIsNotNone(result, "No data found in the cache for the inserted ID.")
|
||||||
|
self.assertEqual(result.iloc[0]['user_name'], 'Alice', "The name in the cache doesn't match the inserted value.")
|
||||||
|
self.assertEqual(result.iloc[0]['age'], 30, "The age in the cache does not match the inserted value.")
|
||||||
|
|
||||||
|
# Now test with skipping the cache
|
||||||
|
print("Testing insert_row() with skip_cache=True")
|
||||||
|
|
||||||
|
# Insert another row into the database, this time skipping the cache
|
||||||
|
self.data.insert_row(cache_name=cache_name, columns=columns, values=('Bob', 40), skip_cache=True)
|
||||||
|
|
||||||
|
# Attempt to retrieve the newly inserted row from the cache
|
||||||
|
result_after_skip = self.data.get_cache_item(key='2', cache_name=cache_name)
|
||||||
|
|
||||||
|
# Assert that no data is found in the cache for the new row
|
||||||
|
self.assertIsNone(result_after_skip, "Data should not have been cached when skip_cache=True.")
|
||||||
|
|
||||||
|
print(" - Insert row with and without caching passed all checks.")
|
||||||
|
|
||||||
def test_fill_data_holes(self):
|
def test_fill_data_holes(self):
|
||||||
print('Testing _fill_data_holes() method:')
|
print('Testing _fill_data_holes() method:')
|
||||||
Loading…
Reference in New Issue