def get_card_by_id(card_id, db_path=config.get_DB_name()): """ Takes in: 1. An integer that represents target card's id. 2. A path to the DB file (optional, defaults to config.DB) Returns an object of type knards.Card representing the card data stored in the DB. """ try: int(card_id) except ValueError: raise ValueError('id must be a proper number.') with util.db_connect(db_path) as connection: cursor = connection.cursor() cursor.execute(""" SELECT * FROM cards WHERE id = {} """.format(card_id)) card = cursor.fetchone() if not card: raise exceptions.CardNotFound( 'Card #{} was not found in the DB.'.format(card_id)) card_obj = knards.Card(*card) return card_obj
def get_series_set(series_name, db_path=config.get_DB_name()): """Returns all cards that belong to the specified series. Args: series_name (str): The series name db_path (str): The path to the DB (optional, defaults to what's defined in config module) Raises: TODO Returns: A set of cards all of which belong to the series """ if not isinstance(series_name, str): raise TypeError('\'series_name\' argument must be a list.') with util.db_connect(db_path) as connection: cursor = connection.cursor() cursor.execute(""" SELECT * FROM cards WHERE series = '{}' """.format(series_name)) card_set = cursor.fetchall() if not card_set: raise exceptions.EmptyDB( 'No cards adhere to the specified constraints.') sorted_card_set = {} for card in card_set: sorted_card_set[knards.Card(*card).pos_in_series] = knards.Card(*card) return sorted_card_set
def test_db_connect_with_permission_denied(): """ If a filename is passed to db_connect() as the DB file to connect to and the script has no permissions to access it -> error is printed out (tested in CLI tests) and None is returned. """ # we can never (almost) access /test.db connection = util.db_connect('/test.db') assert connection is None
def create_card(card_obj, db_path=config.get_DB_name()): """ Takes in: 1. An object of type knards.Card 2. A path to the DB file (optional, defaults to config.DB) Returns an id of the card in the DB created based on the passed in object. """ if type(card_obj) is not knards.Card: raise ValueError('Input card object must be of type knards.Card') # find a free id # this allows to reuse ids that were used and then freed up by deleting the # object free_id = 1 with util.db_connect(db_path) as connection: cursor = connection.cursor() cursor.execute(""" SELECT (id) FROM cards """) for id in cursor.fetchall(): if free_id != id[0]: break free_id += 1 card_obj = card_obj._replace(id=free_id) with util.db_connect(db_path) as connection: cursor = connection.cursor() cursor.execute( """ INSERT INTO cards VALUES ({}) """.format(','.join(list('?' * len(card_obj)))), (card_obj)) created_with_id = cursor.lastrowid return created_with_id
def test_db_connect_with_uninitialized_db(): """ If a filename is passed to db_connect() as the DB file to connect to and the file doesn't exist (but the path is accessible) - the default behavior of sqlite3 is to create the file from scratch (we don't want this) - error is printed out (tested in CLI tests) and None is returned. Also, method must clean up after itself -> the db_path file mustn't exist after the method returns. """ connection = util.db_connect('test.db') assert connection is None assert not os.path.exists('test.db')
def update_card(card_obj, update_now=True, db_path=config.get_DB_name()): """ Takes in: 1. An object of type knards.Card 2. A path to the DB file (optional, defaults to config.DB) Returns the id of the card that is updated. """ if type(card_obj) is not knards.Card: raise ValueError('Input card object must be of type knards.Card') with util.db_connect(db_path) as connection: cursor = connection.cursor() if update_now: cursor.execute( """ UPDATE cards SET question = ?, answer = ?, markers = ?, series = ?, pos_in_series = ?, date_updated = ?, score = ? WHERE id = ? """, (card_obj.question, card_obj.answer, card_obj.markers, card_obj.series, card_obj.pos_in_series, datetime.now(), card_obj.score, card_obj.id)) else: cursor.execute( """ UPDATE cards SET question = ?, answer = ?, markers = ?, series = ?, pos_in_series = ?, score = ? WHERE id = ? """, (card_obj.question, card_obj.answer, card_obj.markers, card_obj.series, card_obj.pos_in_series, card_obj.score, card_obj.id)) return card_obj.id
def merge(db_file): """ TODO """ # check if merge file exists and is a proper DB file try: with util.db_connect(db_file) as connection: cursor = connection.cursor() cursor.execute(""" SELECT * FROM cards """) card_set = cursor.fetchall() card_set_as_objects = [] for card in card_set: card_set_as_objects.append(Card(*card)) full_card_set = api.get_card_set() dates = [] for card in full_card_set: dates.append(card.date_created) except exceptions.DBFileNotFound as e: print(e.args[0]) sys.exit(1) except sqlite3.DatabaseError: print('{} is not a proper DB file to merge.'.format(db_file)) sys.exit(1) # backup both DB files try: copyfile( config.get_DB_name(), config.get_backup_path() + 'main_{}'.format(datetime.now().strftime('%Y_%m_%d_%H_%M_%S_%f'))) except IOError as e: print("Unable to copy file. %s" % e) sys.exit(1) except: print("Unexpected error:", sys.exc_info()) sys.exit(1) try: copyfile( db_file, config.get_backup_path() + 'merge_{}'.format(datetime.now().strftime('%Y_%m_%d_%H_%M_%S_%f'))) except IOError as e: print("Unable to copy file. %s" % e) sys.exit(1) except: print("Unexpected error:", sys.exc_info()) sys.exit(1) # merge merged = 0 skipped = 0 for card in card_set_as_objects: if card.date_created in dates: skipped += 1 continue merged += 1 api.create_card(card) if skipped == 0: click.secho( 'DB file ({}) was successfully merged into the default DB file.'. format(db_file), fg='green', bold=True) os.remove(db_file) else: click.secho( '{} cards merged; {} cards skipped; You might be trying to merge a DB \ file that was already merged.\nMerged DB file wasn\'t removed in case \ something went wrong, please check manually and then remove the file.'. format(merged, skipped), fg='red', bold=True)
def get_card_set(revisable_only=False, show_question=True, show_answer=True, include_markers=[], exclude_markers=[], today=False, db_path=config.get_DB_name()): """Outputs a set of objects of type knards.Card constrained by the passed in options Args: revisable_only (bool): Returns only card objects that are ready to be revised show_questions (bool): Include/exclude question text into results show_answer (bool): Include/exclude answer text into results include_markers (str[]): A list of markers all of which each card that is to be revised must have exclude_markers (str[]): A list of markers none of which each card that is to be revised must have today (bool): Returns only card objects that were already revised today db_path (str): The path to the DB (optional, defaults to what's defined in config module) Raises: TODO Returns: TODO """ if not isinstance(revisable_only, bool): raise TypeError('revisable_only must be a boolean.') if not isinstance(today, bool): raise TypeError('today must be a boolean.') if not isinstance(show_question, bool): raise TypeError('show_question must be a boolean.') if not isinstance(show_answer, bool): raise TypeError('show_answer must be a boolean.') if not isinstance(include_markers, abc.Sequence): raise TypeError('include_markers must be a list.') if not isinstance(exclude_markers, abc.Sequence): raise TypeError('exclude_markers must be a list.') with util.db_connect(db_path) as connection: cursor = connection.cursor() cursor.execute(""" SELECT * FROM cards """) card_set = cursor.fetchall() if not card_set: raise exceptions.EmptyDB( 'No cards adhere to the specified constraints.') card_set_as_objects = [] for card in card_set: card_set_as_objects.append(knards.Card(*list(card))) card_set_revisable = [] for card in card_set_as_objects: # if revisable_only is True -> only return cards that has their score # equal to or more than the difference between today and the date of the # last card update (revision) and cards that weren't yet revised if revisable_only: if card.date_updated is None: card_set_revisable.append(card) elif card.score <= (datetime.now() - card.date_updated).days: card_set_revisable.append(card) else: card_set_revisable = card_set_as_objects break card_set_included_markers = [] for card in card_set_revisable: # if include_markers is not '' -> filter cards that must have all of the # specified markers (as whole words) if include_markers: compare_list = card.markers.split() for marker in include_markers: if not marker in compare_list: break else: card_set_included_markers.append(card) else: card_set_included_markers = card_set_revisable break card_set_excluded_markers = [] for card in card_set_included_markers: # if exclude_markers is not '' -> filter cards that must have none of the # specified markers (as whole words) if exclude_markers: compare_list = card.markers.split() for marker in exclude_markers: if marker in compare_list: break else: card_set_excluded_markers.append(card) else: card_set_excluded_markers = card_set_included_markers break card_set_today = [] for card in card_set_excluded_markers: # return cards that have date_updated equal to today's date (were revised # today) if today: if card.date_updated and card.date_updated.date() == datetime.now( ).date(): card_set_today.append(card) else: card_set_today = card_set_excluded_markers break if not show_question: card_set_without_questions = [] for card in card_set_today: card_set_without_questions.append(card._replace(question='')) else: card_set_without_questions = card_set_today if not show_answer: card_set_without_answers = [] for card in card_set_without_questions: card_set_without_answers.append(card._replace(answer='')) else: card_set_without_answers = card_set_without_questions return card_set_without_answers
def delete_card(card_id=None, markers=None, series=None, db_path=config.get_DB_name()): """Deletes a card/cards from the DB Args: card_id (int): The id of the card that is to be deleted markers (str[]): A list of markers all of which each card that is to be deleted must have series (str): The name of the series that is to be deleted db_path (str): The path to the DB (optional, defaults to what's defined in config module) Raises: TODO Returns: card_id: id of the card that was deleted deleted_ids: List of all deleted cards' ids """ assert card_id or markers or series assert isinstance(db_path, str) and len(db_path) > 0 if card_id: if not isinstance(get_card_by_id(card_id), knards.Card): raise exceptions.CardNotFound( 'Card #{} does not exist in the DB.'.format(card_id)) with util.db_connect(db_path) as connection: cursor = connection.cursor() cursor.execute(""" DELETE FROM cards WHERE id = {} """.format(card_id)) return card_id elif markers: if not isinstance(markers, list): raise TypeError('\'markers\' argument must be a list.') card_set = get_card_set(include_markers=markers) assert len(card_set) != len(get_card_set()) with util.db_connect(db_path) as connection: cursor = connection.cursor() for card in card_set: cursor.execute(""" DELETE FROM cards WHERE id = {} """.format(card.id)) return [card.id for card in card_set] elif series: if not isinstance(series, str): raise TypeError('\'series\' argument must be a string.') with util.db_connect(db_path) as connection: cursor = connection.cursor() cursor.execute(""" SELECT * FROM cards WHERE series = '{}' """.format(series)) card_set = cursor.fetchall() card_set_as_objects = [] for card in card_set: card_set_as_objects.append(knards.Card(*list(card))) cursor.execute(""" DELETE FROM cards WHERE series = '{}' """.format(series)) return [card.id for card in card_set_as_objects]
def get_last_card(markers=None, db_path=config.DB): """ Takes in: 1. A list of strings that represent the set of markers that all the affected card objects must contain. 2. A path to the DB file (optional, defaults to config.DB) If 'markers' is not passed in, returns a knards.Card object that's a copy of the most recently created card. If 'markers' is passed in, returns a knards.Card object that's a copy of the most recently created card that contains ALL of the specified markers. Returns None upon if the DB is empty or if no cards contain all of the specified markers (if 'markers' were passed in) """ if markers: if type(markers) is not list: print(msg.MARKERS_MUST_BE_LIST) return None else: for elem in markers: if type(elem) is not str: print(msg.MARKERS_MUST_BE_LIST) return None connection = util.db_connect(db_path) if not connection: return None cursor = connection.cursor() with connection: if not markers: cursor.execute(""" SELECT * FROM cards WHERE id = (SELECT MAX(id) FROM cards WHERE \ date_created = (SELECT MAX(date_created) FROM cards)) """) card = cursor.fetchone() if card: card_obj = knards.Card(*list(card)) else: card_set = get_card_set() cards_to_pick_from = [] # sift in only cards that contain all of the specified markers for card in card_set: has_markers = card.markers.split() for marker in markers: if marker not in has_markers: break else: cards_to_pick_from.append(card) if not cards_to_pick_from: print(msg.CARDS_BY_MARKERS_NOT_FOUND.format( ', '.join(markers))) return None # find out the most recent date of addition of a card with specified markers max_date = datetime(1970, 1, 1) for card in cards_to_pick_from: if card.date_created > max_date: max_date = card.date_created # find out the max card id among the sifted in cards card_with_max_id = knards.Card(id=0) for card in cards_to_pick_from: if card.date_created == max_date and card.id > card_with_max_id.id: card_with_max_id = card card_obj = card_with_max_id card = 'This is a shitty algorithm, rewrite it!' assert card return card_obj