def func_launcher(*args, **kwargs) -> Any: nonlocal when_was_called item_type = kwargs.get('item_type', None) feature = kwargs.get('feature', None) message = kwargs.get('message', None) if item_type == 'country_ru' or item_type == 'country_en': result_id = users.find_one(message).language + item_type elif item_type or feature: result_id = item_type or feature else: raise ValueError("Cannot find a key to cache your function") high_time = (when_was_called + timedelta(minutes=CACHE_TIME) < datetime.now() if when_was_called else True) if not cache.get(result_id, None) or not when_was_called or high_time: when_was_called = datetime.now() result = func(*args, **kwargs) cache[result_id] = result return result else: log.info('Returning cached result of %s', func.__name__) time_left = (when_was_called + timedelta(minutes=CACHE_TIME) - datetime.now()) log.debug('Time to to reevaluate result of %s is %s', func.__name__, str(time_left)[:-7]) return cache[result_id]
def clean_cache(self, limit: int) -> None: """ Method that remove several User objects from cache - the least active users :param limit: number of the users that the method should remove from cache :return: None """ log.info('Figuring out the least active users...') # Select users that the least active recently user_ids = tuple(self.users.keys()) query = ('SELECT chat_id ' 'FROM photo_queries_table2 ' f'WHERE chat_id in {user_ids} ' 'GROUP BY chat_id ' 'ORDER BY MAX(time) ' f'LIMIT %s') parameters = limit, try: cursor = db.execute_query(query, parameters) except DatabaseConnectionError: log.error("Can't figure out the least active users...") return if not cursor.rowcount: log.warning("There are no users in the db") return # Make list out of tuple of tuples that is returned by MySQL least_active_users = [chat_id[0] for chat_id in cursor.fetchall()] log.info('Removing %d least active users from cache...', limit) num_deleted_entries = 0 for entry in least_active_users: log.debug('Deleting %s...', entry) deleted_entry = self.users.pop(entry, None) if deleted_entry: num_deleted_entries += 1 log.debug("%d users were removed from cache.", num_deleted_entries)
def _open_ssh_tunnel(self) -> None: """ Method that opens a new ssh tunnel to the server where the database of photogpsbot is located :return: None """ log.debug('Establishing SSH tunnel to the server where the database ' 'is located...') sshtunnel.SSH_TIMEOUT = 5.0 sshtunnel.TUNNEL_TIMEOUT = 5.0 self.tunnel = sshtunnel.SSHTunnelForwarder( ssh_address_or_host=config.SERVER_ADDRESS, ssh_username=config.SSH_USER, ssh_password=config.SSH_PASSWD, ssh_port=22, remote_bind_address=('127.0.0.1', 3306)) self.tunnel.start() self.tunnel_opened = True log.debug('SSH tunnel has been established.')
def get_number_users_by_feature(feature: str, feature_type: str) -> int: """ Get number of users that have same smartphone, camera, lens or that have been to the same country :param feature: string which is name of a particular feature e.g. camera name or country name :param feature_type: string which is name of the column in database :return: string which is a message to user """ log.debug('Check how many users also have this feature: %s...', feature) query = ("SELECT DISTINCT chat_id " "FROM photo_queries_table2 " f"WHERE {feature_type}=%s") parameters = feature, try: cursor = db.execute_query(query, parameters) except DatabaseConnectionError: log.error("Cannot check how many users also have this feature: %s...", feature) raise if not cursor.rowcount: log.debug('There were no users with %s...', feature) return 0 log.debug('There is %d users with %s', cursor.rowcount, feature) return cursor.rowcount - 1
def set_language(self, lang: str) -> None: """ Update language of user in the User object and in the database :param lang: string with language tag like "en-US" :return: None """ log.debug( 'Updating info about user %s language ' 'in memory & database...', self) self.language = lang query = ("UPDATE users " f"SET language=%s " f"WHERE chat_id=%s") parameters = self.language, self.chat_id try: db.add(query, parameters) except DatabaseError: log.error("Can't add new language of %s to the database", self) else: log.debug('Language updated.')
def _get_address(self, latitude: float, longitude: float) \ -> Tuple[Dict[str, str], Dict[str, str]]: """ # Get address as a string by coordinates from photo that user sent to bot :param latitude: latitude from a photo as a float :param longitude: longitude rom a photo as a float :return: address as a string where photo was taken; name of country in English and Russian to keep statistics of the most popular countries among users of the bot """ address = {} country = {} coordinates = f"{latitude}, {longitude}" log.debug('Getting address from coordinates %s...', coordinates) geolocator = Nominatim() lang = self.user.language try: # Get name of the country in English and Russian language location_en = geolocator.reverse(coordinates, language='en') address['en-US'] = location_en.address country['en-US'] = location_en.raw['address']['country'] location_ru = geolocator.reverse(coordinates, language='ru') address['ru-RU'] = location_ru.address country['ru-RU'] = location_ru.raw['address']['country'] address = address[lang] return address, country except Exception as e: log.error(e) log.error('Getting address has failed!') raise
def cache(self, limit: int) -> None: """ Caches last active users from database to a dictionary inside object of this class :param limit: limit of entries to be cached :return: None """ log.debug("Start caching last active users from the DB...") try: last_active_users = self.get_last_active_users(limit) except DatabaseConnectionError: log.error("Cannot cache users!") return for items in last_active_users: # if chat_id of a user is not known to the bot if items[0] not in self.users: # adding a user from the database to the "cache" self.users[items[0]] = User(*items) log.debug("Caching user: %s", self.users[items[0]]) log.info('Users have been cached.')
def compare_and_update(user, message: Message) -> None: """ Updates user's info if needed This method compare a user object from the bot and his info from the Telegram message to check whether a user has changed his bio or not. If yes, the user object that represents him in the bot will be updated accordingly. Now this function is called only when a user asks the bot for showing the most popular cams :param user: user object that represents a Telegram user in this bot :param message: object from Telegram that contains info about user's message and about himself :return: None """ log.info('Checking whether user have changed his info or not...') msg = message.from_user usr_from_message = User(message.chat.id, msg.first_name, msg.username, msg.last_name) if user.chat_id != usr_from_message.chat_id: log.error("Wrong user to compare!") return if user.first_name != usr_from_message.first_name: user.first_name = usr_from_message.first_name elif user.nickname != usr_from_message.nickname: user.nickname = usr_from_message.nickname elif user.last_name != usr_from_message.last_name: user.last_name = usr_from_message.last_name else: log.debug("User's info hasn't changed") return log.info("User has changed his info") log.debug("Updating user's info in the database...") query = (f"UPDATE users " f"SET first_name=%s, " f"nickname=%s, " f"last_name=%s " f"WHERE chat_id=%s") parameters = (user.first_name, user.nickname, user.last_name, user.chat_id) try: db.add(query, parameters) except DatabaseError: log.error("Could not update info about %s in the database", user) else: log.debug("User's info has been updated")
def find_one(self, message: Message) -> User: """ Look up a user by a message which we get together with request from Telegram :param message: object from Telegram that contains info about user's message and about himself :return: user object that represents a Telegram user in this bot """ # look up user in the cache of the bot user = self.users.get(message.chat.id, None) if user: return user # otherwise look up the user in the database log.debug("Looking up the user in the database as it doesn't " "appear in cache") query = (f'SELECT first_name, nickname, last_name, language ' f'FROM users ' f'WHERE chat_id=%s') parameters = message.chat.id, try: cursor = db.execute_query(query, parameters) except DatabaseConnectionError: # Even if the database in unreachable add user to dictionary # with users otherwise the bot will crash requesting this # user's info log.error('Cannot lookup the user with chat_id %d in database', message.chat.id) msg = message.from_user user = self.add_new_one(message.chat.id, msg.first_name, msg.last_name, msg.username, language='en-US', add_to_db=False) return user if not cursor.rowcount: # This user uses our photoGPSbot for the first time as we # can't find him in the database log.info('Adding totally new user to the system...') msg = message.from_user user = self.add_new_one(message.chat.id, msg.first_name, msg.last_name, msg.username, language='en-US') bot.send_message(config.MY_TELEGRAM, text=f'You have a new user! {user}') log.info('You have a new user! Welcome %s', user) # finally if the user wasn't found in the cache of the bot, but was # found in the database else: log.debug('User %d has been found in the database', message.chat.id) user_data = cursor.fetchall()[0] user = self.add_new_one(message.chat.id, *user_data, add_to_db=False) return user
def get_most_popular_items(item_type: str, message: Message) -> str: """ Get the most common cameras/lenses/countries from database and make list of them :param item_type: string with column name to choose between cameras, lenses and countries :param message: telebot object with info about user and his message :return: string which is either list of most common cameras/lenses/countries or message which states that list is empty """ user = users.find_one(message) def tuple_to_ordered_str_list(list_of_gadgets: Tuple[Tuple[str]]) -> str: """ Converts Python list to ordered list as a string Example: 1. Canon 80D 2. iPhone 4S :param list_of_gadgets: tuple of tuples with string where every string is a name of a camera or lens or a country :return: ordered list as a string """ string_roaster = '' index = 1 for item in list_of_gadgets: if not item[0]: continue string_roaster += '{}. {}\n'.format(index, item[0]) index += 1 return string_roaster log.debug('Evaluating most popular things...') # This query returns item types in order where the first one item # has the highest number of occurrences # in a given column query = (f'SELECT {item_type} FROM photo_queries_table2 ' f'GROUP BY {item_type} ' f'ORDER BY count({item_type}) ' 'DESC') try: cursor = db.execute_query(query) except DatabaseConnectionError: log.error("Can't evaluate a list of the most popular items") return messages[user.language]['doesnt work'] # Almost impossible case but still if not cursor.rowcount: log.warning('There is nothing in the main database table') bot.send_message(chat_id=config.MY_TELEGRAM, text='There is nothing in the main database table') return messages[user.language]['no_top'] popular_items = cursor.fetchall() log.info('Finish evaluating the most popular items') return tuple_to_ordered_str_list(popular_items[:30])