def find_cell(self, val=""): ''' Gets data from cell with value of val. Returns none if cell cannot be found. :param val: Str :return: Cell | None ''' # Throw error if val is not a Str assert type(val) == str, "'val' parameter passed '" + str( val) + "' should be Str but isn't" # sheet.find() throws an error if cell cannot be found, so this is handled by returning 'none' try: # Try to get cell dbg.log("Attempting to find cell with value '" + val + "'") cell = self.sheet.find(val) # Log success if cell if found dbg.success("Found cell with value '" + val + "'") return cell except gs_exceptions.CellNotFound: dbg.warn("Could not find cell with value '" + val + "'") return None
def get_cell(self, x=0, y=0): ''' Gets data from cell at (x, y). Coords start at (0, 0). :param x: Int :param y: Int :return: Cell ''' dbg.log("Attempting to get data at cell (" + str(x) + "," + str(y) + ")") # Throw error if x or y are out of range assert x < 1000, "Specified x coord '" + str( x ) + "' is too high. x can be 999 at max because there are only 1000 rows." assert y < 26, "Specified y coord '" + str( y ) + "' is too high. y can be 25 at max because there are only 25 columns." assert x >= 0, "Specified x coord '" + str( x) + "' is too low. x can be 0 at minimum." assert y >= 0, "Specified y coord '" + str( y) + "' is too low. y can be 0 at minimum." # Log success dbg.success("Data successfully gotten") # Return cell value by converting to Google Sheet coords (instead of (0, 0) being min, (1, 1) is) return self.sheet.cell(x + 1, y + 1)
def get_range(self, p1=(0, 0), p2=(0, 0)): ''' Gets all cells in range of the 2 tuples passed in. Returns a list of cells. :param p1: Tuple with values (int x, int y) :param p2: Tuple with values (int x, int y) :return: Cell[] ''' # Throw error if p1 or p2 are out of range assert 0 <= p1[0] < 26, "p1 'x' parameter '" + str( p1[0]) + "' is out of range [0, 25]" assert 0 <= p2[0] < 26, "p2 'x' parameter '" + str( p2[0]) + "' is out of range [0, 25]" assert 0 <= p1[1] < 26, "p1 'y' parameter '" + str( p1[1]) + "' is out of range [0, 1000]" assert 0 <= p2[1] < 26, "p1 'y' parameter '" + str( p2[1]) + "' is out of range [0, 1000]" # Log range getting status (for lack of a better term) dbg.log("Attempting to get all cells in range (" + str(p1[0]) + ',' + str(p1[1]) + ") and (" + str(p2[0]) + ',' + str(p2[1]) + ')') # Get cells in range of points p1 and p2 using GSheet coords (converting from cartesian coords) data = self.sheet.range(p1[0] + 1, p1[1] + 1, p2[0] + 1, p2[1] + 1) # Log success dbg.success("Successfully got all cells in range (" + str(p1[0]) + ',' + str(p1[1]) + ") and (" + str(p2[0]) + ',' + str(p2[1]) + ')') return data
def __init__(self, doc_ID='', wsheet_ID=0, creds={}): ''' Initialize new Database class with credentials, a document ID and a worksheet ID. :param doc_ID: Str: Document ID of Google Sheets document to be linked with Database class :param wsheet_ID: Int: Worksheet ID of Google Sheets data to link against :param creds: ServiceAccountCredentials: Credentials used to gain Google Sheets permissions ''' try: # Attempt to log into Google Sheets and make new gspread instance dbg.log("Attempting Google Sheets authorization...") self.creds = creds self.gs = gspread.authorize(self.creds) # Log Google Sheet authorization success if authorisation is successful dbg.success("Authorization successful") except: # Throw error if 'creds' passed are invalid raise ValueError( "Invalid creds object passed into 'Database' constructor: " + str(creds)) # Set document ID and worksheet ID of Database to what was passed in through constructor args self.set_doc(doc_ID, wsheet_ID)
def http_homepage(): ''' Sends html from ‘./Website/index.html' when client connects ''' # Attempt to send homepage to client dbg.log("Attempting to send './Website/index.html' to clients") # Test if ./Website/index.html exists. try: open('./Website/index.html', 'r') # Throw error and exit if ./Website/index.html does not exist except FileNotFoundError: dbg.err( "Failed to send homepage to client because './Website/index.html' does not exist." ) input( "Server cannot run without homepage... Press any key to exit.") os._exit(-1) # Send html from ./Website/index.html to client html = send_from_directory('./Website/', 'index.html') dbg.success( "'./Website/index.html' successfully retrieved. Sending to clients..." ) return html
def check_version(): try: if os.path.isfile(globals.INSTALL_DIRECTORY + globals.CLIENT_VERSION_FILE): debug.info("Found client version file in " + globals.INSTALL_DIRECTORY) with open(globals.INSTALL_DIRECTORY + globals.CLIENT_VERSION_FILE) as current_version_file: globals.CLIENT_VERSION = current_version_file.read().replace( "\n", "") current_version_file.close() debug.info("Contents of client version file: " + globals.CLIENT_VERSION) filehandler.download( globals.CLIENT_DOWNLOAD_URL + globals.CLIENT_VERSION_FILE, globals.CLIENT_VERSION_FILE) debug.info("Saved client version file to ./" + globals.CLIENT_VERSION_FILE) with open(globals.CLIENT_VERSION_FILE, "r") as online_version_file: globals.ONLINE_CLIENT_VERSION = online_version_file.read().replace( "\n", "") debug.info("Current client version: " + globals.CLIENT_VERSION) debug.info("Latest client version: " + globals.ONLINE_CLIENT_VERSION) if globals.CLIENT_VERSION == globals.ONLINE_CLIENT_VERSION: debug.success("Your client is up to date!") os.remove(globals.CLIENT_VERSION_FILE) debug.terminate() if os.path.isfile(globals.INSTALL_DIRECTORY + globals.CLIENT_VERSION_FILE): debug.info("Deleting old version data..") os.remove(globals.INSTALL_DIRECTORY + globals.CLIENT_VERSION_FILE) debug.success("Deleted old version data!") debug.info("Installing new version data..") shutil.move(globals.CLIENT_VERSION_FILE, globals.INSTALL_DIRECTORY + globals.CLIENT_VERSION_FILE) return os.path.isfile(globals.INSTALL_DIRECTORY + globals.CLIENT_VERSION_FILE) except (OSError, URLError) as e: debug.error( "There was an error while checking the version of your client:") print(e) return False
def emit_pageData(): ''' Emits 'pageData' event and sends 'poller.radiothonInfo' RadiothonInfo data structure to clients :return: None ''' # Log attempt to emit 'pageData' event dbg.log("Attempting to emit 'pageData' event") # If radiothonInfo exists, send 'pageData' event containing radiothonInfo to client if poller.radiothonInfo: socket.emit('pageData', poller.radiothonInfo) dbg.success("'pageData' event successfully emitted") # Else throw a warning else: dbg.warn( "Was not able to emit 'pageData' SocketIO event. Didn't have enough time to process 'poller.radiothonInfo'" )
def update_console(): debug.info("Saving current updater files..") try: os.rename( globals.APPLICATION_PATH + globals.MAIN_EXECUTABLE[globals.OPERATING_SYSTEM], globals.APPLICATION_PATH + globals.BACKUP_EXECUTABLE[globals.OPERATING_SYSTEM]) debug.success("Successfully saved updater files!") debug.info("Downloading new updater files.") debug.info("This may take a minute.") if filehandler.download( globals.CONSOLE_DOWNLOAD_URL + globals.OPERATING_SYSTEM + "/" + globals.MAIN_EXECUTABLE[globals.OPERATING_SYSTEM], globals.APPLICATION_PATH + globals.MAIN_EXECUTABLE[globals.OPERATING_SYSTEM]): debug.success("Downloaded updater files.") else: debug.error("Failed to download new updater files.") debug.error("Ending the update process.") debug.terminate() if globals.OPERATING_SYSTEM == "Linux" or globals.OPERATING_SYSTEM == "Darwin": debug.info("Applying file permissions..") os.system("sudo chmod +x " + globals.APPLICATION_PATH + "main") debug.success("Update completed!") debug.terminate() except URLError as e: debug.error("Failed to download new updater files.") print(e) debug.info("Loading saved files.") os.rename(globals.APPLICATION_PATH + globals.BACKUP_EXECUTABLE, globals.APPLICATION_PATH + globals.MAIN_EXECUTABLE) debug.success("Loaded saved files!") debug.success("Ending update process.") debug.terminate() except OSError as e: debug.error("The updater encountered an error:") print(e) debug.error("Terminating update.") debug.terminate()
def set_doc(self, doc_ID='', wsheet_ID=0): ''' Link with Google Sheets document ID 'doc_ID' and worksheet ID 'wsheet_ID' :param doc_ID: Str :param wsheet_ID: ID :return: None ''' try: # Attempt to link 'sheet' against GSheets document with ID 'doc_ID' and worksheet ID 'wsheet_ID' dbg.log("Attempting to link with Google Sheets document...") self.sheet = self.gs.open_by_key(doc_ID).get_worksheet(wsheet_ID) # Print success message if able to succesfully link with GSheets document dbg.success("Successfully linked with Google Sheets document") except gs_exceptions.APIError as e: # Throw error and exit if read permissions were not set if e.response.json()['error']['code'] == 403: dbg.err( "Didn't have permissions to read Google Sheets document. " + "Please make document public or create a shareable link to it." ) input("Press any key to exit...") os._exit(-1) # Throw error and exit if document does not exist elif e.response.json()['error']['code'] == 404: dbg.err( "Tried to read Google Sheets document, but the document does not exist." ) input("Press any key to exit...") os._exit(-1) # Throw error and exit if any other bad response is received else: dbg.err( "Tried to read Google Sheets document, but got the following error response instead:" ) dbg.err(e.response.text) input("Press any key to exit...") os._exit(-1)
def http_other(path): ''' Sends html from ‘./Website/{path} when client connects ''' # Attempt to send homepage to client dbg.log("Attempting to send './Website/" + path + "' to clients") # Test if ./Website/<path> exists. Throw warning if it doesn't. # Wont exit because something like 'favicon.ico' might not be integral to the application try: open('./Website/' + path, 'r') except FileNotFoundError: dbg.warn("Failed to send file './Website/" + path + "' to clients . File does not exist.") # Send html to client html = send_from_directory('./Website/', path) dbg.success("'./Website/" + path + "' successfully retrieved. Sending to client...") return html
def get_row(self, y=0): ''' Gets every value at row y. ‘y’ coord starts from 0. :param y: Int :return: List ''' # Log row getting status (for lack of a better term) dbg.log("Attempting to get data from row " + str(y) + "...") # Throw error if y is out of bounds assert y < 1000, "'y' parameter '" + str( y ) + "' is too high. Must be at most 999 because there are 1000 rows in a sheet." assert y >= 0, "'y' parameter '" + str( y) + "' is too low. Must be 0 at minimum." # Return data from column by converting from cartesian to GSheet coords data = self.sheet.row_values(y + 1) dbg.success("Successfully got data from row " + str(y)) return data
def get_col(self, x=0): ''' Gets every value at column x. ‘x’ coord starts from 0. :param x: Int :return: List ''' # Log column getting status (for lack of a better term) dbg.log("Attempting to get data from column " + str(x) + "...") # Throw error if y is out of bounds assert x < 26, "'y' parameter '" + str( x ) + "' is too high. Must be at most 25 because there are 26 columns in a sheet." assert x >= 0, "'y' parameter '" + str( x) + "' is too low. Must be 0 at minimum." # Return data from column by converting from cartesian to GSheet coords data = self.sheet.col_values(x + 1) dbg.success("Successfully got data from column " + str(x)) return data
def install_files(): try: new_client_file = "clientfiles(" + globals.ONLINE_CLIENT_VERSION + ").zip" client_zip = zipfile.ZipFile( globals.INSTALL_DIRECTORY + new_client_file, "r") client_zip.extractall(globals.INSTALL_DIRECTORY) client_zip.close() for i in range(0, len(globals.FOLDER_NAME)): debug.info("Verifying the installation of " + globals.FOLDER_NAME[i] + "...") if os.path.isdir(globals.INSTALL_DIRECTORY + globals.FOLDER_NAME[i]): debug.success("Installation found: " + globals.FOLDER_NAME[i] + ".") else: debug.info(globals.FOLDER_NAME[i] + " could not be found!") return False return True except (OSError, IOError) as e: debug.error("Failed to install client files.") print(e) return False
def update_radiothonInfo(): ''' This method updates the global ‘radiothonInfo’ variable by getting the latest data from GSheets document and parsing it and the config into a ‘RadiothonInfo’ data structure. This method uses the ‘gsparser.py’ module to process all the data. :return: None ''' # Bring global variables into local scope global radiothonInfo global database global config # Attempt to refresh GAPI credentials. Needed otherwise Google will reject requests after certain period of time. dbg.log("Attempting to refresh GAPI credentials") database.gs.login() dbg.success("Successfully refreshed GAPI credentials") # Update config update_config_status() # Log radiothonInfo update attempt dbg.log("Attempting to update server status (poller.radiothonInfo)") # Get all rows from Google sheets document dbg.log("Attempting to get all the data from GSheets document.") rows = database.get_all_vals() dbg.success("Successfully got all data from GSheets document.") # Throw error and exit if there is no data in GSheets document at all if len(rows) == 0: dbg.err("Failed to update server status because there is no data or header in Google Sheets document") input("Cannot continue. Press any key to exit.") os._exit(-1) # Get header from GSheets doc to compare pledges against. header = rows[0] # PledgeDict accumulator. Will contain a list of 'Pledge' data structures pledges = [] # Get all rows with content excluding header (which is why i starts at 1) # from GSheets doc and turn them into PledgeDict's for i in range(1, len(rows)): pledges.append(gsparser.to_Pledge(rows[i], header)) # Update radiothonInfo to latest state radiothonInfo = gsparser.to_RadiothonInfo(pledges, config) # Log that radiothonInfo was successfully updated dbg.success("Server status update successful (poller.radiothonInfo updated)")
def main(): # Clear the terminal depending on OS. if globals.OPERATING_SYSTEM == "Darwin" or globals.OPERATING_SYSTEM == "Linux": os.system("clear") if globals.OPERATING_SYSTEM == "Windows": os.system("cls") debug.info("Welcome to the Banana Kingdom server updater V" + globals.CONSOLE_VERSION + "!") try: debug.info("Checking Internet connection..") if validator.check_internet(): debug.success("Connected to the Internet!") else: debug.error("Failed Internet connection test.") debug.error("Terminating client launch.") debug.terminate() # If the updater has an update, it'll attempt to update itself. if validator.check_console_updates(): update_executable = { "Windows": "update.exe", "Linux": "update", "Darwin": "update" } input("Press enter to install the new updates.") executable_path = os.path.join( globals.APPLICATION_PATH, update_executable[globals.OPERATING_SYSTEM]) os.system(executable_path) sys.exit() # Delete backup of original executable, if the update was successful. if os.path.exists(globals.BACKUP_EXECUTABLE[globals.OPERATING_SYSTEM]): os.remove(globals.BACKUP_EXECUTABLE[globals.OPERATING_SYSTEM]) debug.info("Would you like to check for updates?") install_confirm = input("Enter 'y' for yes or 'n' for no.") if not install_confirm == "y": sys.exit() if validator.check_directory(): debug.success("Found your Minecraft installation in " + globals.INSTALL_DIRECTORY) else: debug.error("Failed to find Minecraft installation folder.") debug.error("Ending the update process.") debug.terminate() if not validator.check_version(): debug.error("Failed to check your client version.") debug.error("Ending the update process.") debug.terminate() if filehandler.download_client(): debug.success("Successfully downloaded client files!") else: debug.error("Failed to download client files.") debug.error("Ending the update process.") debug.terminate() filehandler.delete_folders() # Verify that the files were decompressed correctly. if filehandler.install_files(): debug.success("Update Complete! Welcome to Banana Kingdom V" + globals.ONLINE_CLIENT_VERSION) else: debug.error("Failed to install required client files.") except OSError as e: debug.error("There was an error while running this program:") print(e) debug.terminate()
"-----------------------------------------------------------------------------------------------------------------------------------------------------------\n" ) # Pause and give user time to read copyright message sleep(1) # Load config and GAPI credentials poller.update_config_status() # Attempt to load GAPI credentials from 'creds.pickle' try: dbg.log("Attempting to load GAPI credentials file 'creds.pickle'") creds = pickle.load(open('creds.pickle', 'rb')) # Log success dbg.success("Successfully loaded GAPI credentials file 'creds.pickle'") # Throw error and exit if GAPI credentials file cannot be found except FileNotFoundError: dbg.err( "Failed to load 'creds.pickle' because it could not be found. 'creds.pickle' is required for server to run." ) input("Cannot continue. Press any key to exit.") exit(-1) # Throw error that reads off if any other exceptions are raised except Exception as e: dbg.err("Failed to load 'creds.pickle'. Got the following error instead:") dbg.err(str(e)) input("Cannot continue. Press any key to exit.") exit(-1)
def update_config_status(): ''' Updates variable ‘config’ by loading server_config.json file as Dict :return: None ''' def handle_corrupt_key(dict={}, key_name="", types_allowed=None): ''' Throws error and quits if a key with name set to value of 'key_name' does not exist or if the value associated with the key is not set to type 'types_allowed' Used primarily to test for corrupted server_config.json. :param dict: ServerConfigDict :param key_name: Str :param types_allowed: Type[] :return: None ''' # Attempt to get value from key try: # Throw error and exit if value associated with key is not of any types specified in 'types_allowed' if type(dict[key_name]) not in types_allowed: # Print error message "Failed to read server_config.json. The '<key_name>' key must be set to one of the following data types: ['typename',...]" dbg.err( "Failed to read server_config.json. The '" + key_name + "' key needs must be set to one of the following data types: " + str([i.__name__ for i in types_allowed])) # Prompt for input then exit input("Cannot continue. Press any key to exit..") os._exit(-1) # Throw error and exit if key cannot be found except KeyError: dbg.err("Failed to read server_config.json. '" + key_name + "' key does not exist in server_config.json.") input("Cannot continue. Press any key to exit..") os._exit(-1) global config # Attempt to load server config and throw error if any keys either don't exist or have an invalid type try: # Log attempt dbg.log("Attempting to update server config status") # Attempt to load JSON from server_config.json as type Dict config = json.load(open('../server_config.json', 'r')) # Throw error if any keys in server_config.json are either missing or invalidly typed handle_corrupt_key(config, "radiothon", [dict]) handle_corrupt_key(config["radiothon"], "goal", [dict]) handle_corrupt_key(config["radiothon"], "name", [str]) handle_corrupt_key(config["radiothon"], "start_date", [str]) handle_corrupt_key(config["radiothon"], "end_date", [str]) handle_corrupt_key(config["radiothon"]["goal"], "hourly", [float, int]) handle_corrupt_key(config["radiothon"]["goal"], "daily", [float, int]) handle_corrupt_key(config["radiothon"]["goal"], "weekly", [float, int]) handle_corrupt_key(config["radiothon"]["goal"], "total", [float, int]) handle_corrupt_key(config, "gsheets", [dict]) handle_corrupt_key(config["gsheets"], "doc_ID", [str]) handle_corrupt_key(config["gsheets"], "wsheet_ID", [int]) handle_corrupt_key(config["gsheets"], "poll_interval", [float, int]) # Throw error if file does not exist except FileNotFoundError: dbg.err( "Failed to read server config because server_config.json does not exist. server_config.json must be in project's root directory.") input("Cannot continue. Press any key to exit") os._exit(-1) # Throw error if JSON from server_config.json could not be parsed except json.JSONDecodeError as e: dbg.err("Failed to read server_config.json. JSON Decoder threw the following error: \n'" + str(e) + "'") input("Cannot continue. Press any key to exit.") os._exit(-1) # Log config loading success dbg.success("Server config status successfully updated")
def main(): ''' Entry point for host.py module :return: void ''' # Bring global variables into local scope global http global socket # These constant variables specify port and address for server to run on # '0.0.0.0' tells OS to make server visible to other computers on network ADDRESS = '0.0.0.0' PORT = 80 # Thread that the server runs on (daemon thread) server_thread = threading.Thread(target=socket.run, args=[http, ADDRESS, PORT]) server_thread.setDaemon(True) # Sends html from ‘./Website/index.html’ @http.route('/') def http_homepage(): ''' Sends html from ‘./Website/index.html' when client connects ''' # Attempt to send homepage to client dbg.log("Attempting to send './Website/index.html' to clients") # Test if ./Website/index.html exists. try: open('./Website/index.html', 'r') # Throw error and exit if ./Website/index.html does not exist except FileNotFoundError: dbg.err( "Failed to send homepage to client because './Website/index.html' does not exist." ) input( "Server cannot run without homepage... Press any key to exit.") os._exit(-1) # Send html from ./Website/index.html to client html = send_from_directory('./Website/', 'index.html') dbg.success( "'./Website/index.html' successfully retrieved. Sending to clients..." ) return html # Sends html from ‘./Website/{path}’ @http.route('/<path:path>') def http_other(path): ''' Sends html from ‘./Website/{path} when client connects ''' # Attempt to send homepage to client dbg.log("Attempting to send './Website/" + path + "' to clients") # Test if ./Website/<path> exists. Throw warning if it doesn't. # Wont exit because something like 'favicon.ico' might not be integral to the application try: open('./Website/' + path, 'r') except FileNotFoundError: dbg.warn("Failed to send file './Website/" + path + "' to clients . File does not exist.") # Send html to client html = send_from_directory('./Website/', path) dbg.success("'./Website/" + path + "' successfully retrieved. Sending to client...") return html # Emit pageData event when clients connect @socket.on('connect') def socket_connect(): ''' Emit a ‘pageData’ event and send poller.radiothonInfo to clients when a client connects through SocketIO ''' emit_pageData() # Attempt to start server on port 80 try: # Start server dbg.log("Attempting to start SocketIO/HTTP server on address '" + ADDRESS + ":" + str(PORT) + "'...") server_thread.start() # Log success dbg.success("Successfully started server") # If server fails to start, throw error except Exception as e: dbg.err("Failed to start SocketIO/HTTP server on address '" + ADDRESS + ":" + str(PORT) + "'. Flask threw the following error: \n" + str(e)) print("Cannot continue. Press any key to exit.") os._exit(-1) # Periodically send 'pageData' event to client at interval specified in config while True: # Send pageData event and 'radiothonInfo' data structure to client emit_pageData() # If poller.config has been set if poller.config != {}: # Sleep for however long was specified in config sleep(poller.config['gsheets']['poll_interval'] * 60) # Else sleep for a second and try to emit pageData event again else: sleep(1)