Ejemplo n.º 1
0
def test_if_both_id_and_markers_are_specified_ignores_markers(mocker):
    """
  If both 'card id' and 'markers' args are specified, 'markers' are ignored.
  """

    card_obj1 = knards.Card(markers='python specific test')
    card_obj2 = knards.Card(markers='python')
    card_obj3 = knards.Card(markers='python specific test')

    runner = CliRunner()
    with runner.isolated_filesystem():
        # create the DB
        runner.invoke(knards.main, ['bootstrap-db'])
        db_path = os.getcwd() + '/' + config.DB
        mocker.patch('knards.config.get_DB_name', return_value=db_path)

        # create cards
        api.create_card(card_obj1)
        api.create_card(card_obj2)
        api.create_card(card_obj3)

        # check that cards were successfully stored
        assert len(api.get_card_set()) == 3

        # invoke the subcommand
        result = runner.invoke(knards.main,
                               ['delete', '--id', 1, '--m', 'python'])
        assert result.exit_code == 0
        assert len(api.get_card_set()) == 2
Ejemplo n.º 2
0
def test_a_single_card_is_properly_deleted_if_card_id_is_specified(mocker):
    """
  If 'card id' arg is specified, delete the card that has that id from the DB.
  """

    card_obj1 = knards.Card()
    card_obj2 = knards.Card()

    runner = CliRunner()
    with runner.isolated_filesystem():
        # create the DB
        runner.invoke(knards.main, ['bootstrap-db'])
        db_path = os.getcwd() + '/' + config.DB
        mocker.patch('knards.config.get_DB_name', return_value=db_path)

        # create cards
        api.create_card(card_obj1)
        api.create_card(card_obj2)

        # check that cards were successfully stored
        assert len(api.get_card_set()) == 2

        # invoke the subcommand
        result = runner.invoke(knards.main, ['delete', '--id', 1])
        assert result.exit_code == 0

        # one card should be deleted, the one that's left must have an id = 2
        assert len(api.get_card_set()) == 1
        assert api.get_card_set()[0].id == 2
Ejemplo n.º 3
0
def status(include_markers, exclude_markers):
    """
    TODO
    """

    if include_markers:
        include_markers = [
            a for a in \
            re.split(r'(\s|,)', include_markers.strip(''))
            if a != ' ' and a != ','
        ]
    if exclude_markers:
        exclude_markers = [
            a for a in \
            re.split(r'(\s|,)', exclude_markers.strip(''))
            if a != ' ' and a != ','
        ]

    total_card_set = api.get_card_set(include_markers=include_markers,
                                      exclude_markers=exclude_markers)
    revised_today_set = api.get_card_set(today=True,
                                         include_markers=include_markers,
                                         exclude_markers=exclude_markers)
    more_revisable = api.get_card_set(revisable_only=True,
                                      include_markers=include_markers,
                                      exclude_markers=exclude_markers)

    click.secho('There\'re {} cards in the DB file in total.\n\
You\'ve revised {} cards today.\n\
There\'re {} more cards ready for revision today.'.format(
        len(total_card_set), len(revised_today_set), len(more_revisable)),
                fg='yellow',
                bold=True)
Ejemplo n.º 4
0
def test_if_markers_passed_in_all_cards_with_those_markers_are_removed(
  mocker,
  init_db
):
  """
  If markers list is passed to the delete_card() method, all cards that contain
  ALL of the specified markers are being removed from the DB.
  """
  card_obj1 = knards.Card(markers='python specific')
  card_obj2 = knards.Card(markers='python nonspecific')
  card_obj3 = knards.Card(markers='javascript specific test')
  card_obj4 = knards.Card(markers='python special')
  card_obj5 = knards.Card(markers='python specific test')
  card_obj6 = knards.Card(markers='specifically test')
  api.create_card(card_obj1, init_db)
  api.create_card(card_obj2, init_db)
  api.create_card(card_obj3, init_db)
  api.create_card(card_obj4, init_db)
  api.create_card(card_obj5, init_db)
  api.create_card(card_obj6, init_db)

  assert len(api.get_card_set(db_path=init_db)) == 6

  mocker.patch(
    'knards.api.get_card_set',
    return_value=api.get_card_set(db_path=init_db)
  )
  assert api.delete_card(markers=['specific', 'test'], db_path=init_db) is True
  mocker.stopall()

  assert len(api.get_card_set(db_path=init_db)) == 4
Ejemplo n.º 5
0
def test_returns_list_of_proper_type_objects(init_db):
  """
  get_card_set() must always return either a list of objects of type
  knards.Card or []
  """
  assert api.get_card_set(db_path=init_db) == []

  card_obj = knards.Card()
  api.create_card(card_obj, init_db)

  assert isinstance(api.get_card_set(db_path=init_db), list)
  assert isinstance(api.get_card_set(db_path=init_db)[0], knards.Card)
Ejemplo n.º 6
0
def test_if_markers_passed_return_cards_with_respect_to_constraints(init_db):
  """
  If get_card_set() is passed include_markers=[...], return only cards that
  have ALL of the specified markers.
  If get_card_set() is passed exclude_markers=[...], return only cards that
  have NONE of the specified markers.
  """
  card_obj1 = knards.Card(markers='python specific')
  card_obj2 = knards.Card(markers='javascript specific')
  card_obj3 = knards.Card(markers='python nonspecific')
  card_obj4 = knards.Card(markers='javascript test')
  card_obj5 = knards.Card(markers='javascript test special')
  card_obj6 = knards.Card(markers='javascript python')
  api.create_card(card_obj1, init_db)
  api.create_card(card_obj2, init_db)
  api.create_card(card_obj3, init_db)
  api.create_card(card_obj4, init_db)
  api.create_card(card_obj5, init_db)
  api.create_card(card_obj6, init_db)

  assert len(api.get_card_set(
    include_markers=['test'],
    db_path=init_db
  )) == 2
  assert len(api.get_card_set(
    include_markers=['specific'],
    db_path=init_db
  )) == 2
  assert len(api.get_card_set(
    include_markers=['spec'],
    db_path=init_db
  )) == 0
  assert len(api.get_card_set(
    include_markers=['javascript'],
    db_path=init_db
  )) == 4
  assert len(api.get_card_set(
    include_markers=['special', 'javascript'],
    db_path=init_db
  )) == 1
  assert len(api.get_card_set(
    exclude_markers=['javascript'],
    db_path=init_db
  )) == 2
  assert len(api.get_card_set(
    exclude_markers=['python', 'test'],
    db_path=init_db
  )) == 1
  assert len(api.get_card_set(
    exclude_markers=['specific'],
    db_path=init_db
  )) == 4
  assert len(api.get_card_set(
    exclude_markers=['specific', 'test'],
    db_path=init_db
  )) == 2
Ejemplo n.º 7
0
def test_if_card_id_and_markers_are_passed_in_markers_are_ignored(
  mocker,
  init_db
):
  """
  If both card_id and markers are passed to the delete_card() method, markers
  are ignored and only the card with the card_id id is removed from the DB.
  """
  card_obj1 = knards.Card(markers='python specific')
  card_obj2 = knards.Card(markers='python nonspecific')
  card_obj3 = knards.Card(markers='javascript specific')
  api.create_card(card_obj1, init_db)
  api.create_card(card_obj2, init_db)
  api.create_card(card_obj3, init_db)

  assert len(api.get_card_set(db_path=init_db)) == 3

  mocker.patch(
    'knards.api.get_card_by_id',
    return_value=api.get_card_by_id(1, db_path=init_db)
  )
  mocker.patch(
    'knards.api.get_card_set',
    return_value=api.get_card_set(db_path=init_db)
  )
  assert api.delete_card(
    card_id=4,
    markers=['specific'],
    db_path=init_db
  ) is True
  mocker.stopall()

  assert len(api.get_card_set(db_path=init_db)) == 3

  mocker.patch(
    'knards.api.get_card_by_id',
    return_value=api.get_card_by_id(1, db_path=init_db)
  )
  mocker.patch(
    'knards.api.get_card_set',
    return_value=api.get_card_set(db_path=init_db)
  )
  assert api.delete_card(
    card_id=1,
    markers=['specific'],
    db_path=init_db
  ) is True
  mocker.stopall()

  assert len(api.get_card_set(db_path=init_db)) == 2
Ejemplo n.º 8
0
def test_input_markers_must_be_lists(init_db):
  """
  We don't expect include_markers/exclude_markers to be anything other than
  lists.
  """
  card_obj = knards.Card()
  api.create_card(card_obj, init_db)

  assert api.get_card_set(include_markers='test', db_path=init_db) == []
  assert api.get_card_set(exclude_markers=111, db_path=init_db) == []
  assert api.get_card_set(
    include_markers=True,
    exclude_markers=111,
    db_path=init_db
  ) == []
Ejemplo n.º 9
0
def test_all_cards_containing_all_markers_are_deleted_if_markers_is_specified(
        mocker):
    """
  If 'markers' arg is specified, delete ALL the cards that have ALL the markers
  that are specified.
  """

    card_obj1 = knards.Card(markers='python specific test')
    card_obj2 = knards.Card(markers='python')
    card_obj3 = knards.Card(markers='python specific test')
    card_obj4 = knards.Card(markers='python specifico test')

    runner = CliRunner()
    with runner.isolated_filesystem():
        # create the DB
        runner.invoke(knards.main, ['bootstrap-db'])
        db_path = os.getcwd() + '/' + config.DB
        mocker.patch('knards.config.get_DB_name', return_value=db_path)

        # create cards
        api.create_card(card_obj1)
        api.create_card(card_obj2)
        api.create_card(card_obj3)
        api.create_card(card_obj4)

        # check that cards were successfully stored
        assert len(api.get_card_set()) == 4

        # invoke the subcommand
        result = runner.invoke(knards.main,
                               ['delete', '--m', 'python,specific test'])
        assert result.exit_code == 0

        # two cards should be deleted, two that's left have ids 2 and 4
        assert len(api.get_card_set()) == 2
        assert api.get_card_set()[0].id == 2
        assert api.get_card_set()[1].id == 4

        # in development we don't allow to delete all cards at once using markers
        result = runner.invoke(knards.main, ['delete', '--m', 'python'])
        assert result.exit_code == 1
        assert len(api.get_card_set()) == 2
Ejemplo n.º 10
0
def test_dates_are_cast_to_convenient_format(init_db):
  """
  sqlite3 keeps dates in UNIX timestamp format, upon each get_card_set() we,
  among other things, cast all dates to 'YYYY-mm-dd' format.
  """
  card_obj = knards.Card(date_created='2019-08-31', date_updated='2019-08-31')
  api.create_card(card_obj, init_db)

  return_value = api.get_card_set(db_path=init_db)
  assert return_value[0].date_created == card_obj.date_created
  assert return_value[0].date_updated == card_obj.date_updated
Ejemplo n.º 11
0
def test_if_today_is_true_return_only_cards_that_were_reviewed_today(init_db):
  """
  If today=True is passed to get_card_set(), return cards that have
  .date_updated equal to today's date.
  """
  card_obj1 = knards.Card()
  card_obj2 = knards.Card(
    date_updated=datetime.today().strftime('%Y-%m-%d'),
    score=1
  )
  card_obj3 = knards.Card(
    date_created=(datetime.today() - timedelta(10)).strftime('%Y-%m-%d'),
    score=2
  )

  api.create_card(card_obj1, init_db)
  api.create_card(card_obj2, init_db)
  api.create_card(card_obj3, init_db)

  assert len(api.get_card_set(today=True, db_path=init_db)) == 1
  assert len(api.get_card_set(today=False, db_path=init_db)) == 3
Ejemplo n.º 12
0
def test_revisable_only_today_show_question_and_show_answer_must_be_boolean(
  init_db
):
  """
  We don't expect revisable_only/today/show question/show answer to be anything
  other than a boolean.
  """
  card_obj = knards.Card()
  api.create_card(card_obj, init_db)

  # a bunch of different combinations of input args (at least one of them is
  # not of an expected type)
  assert api.get_card_set(revisable_only=1, db_path=init_db) == []
  assert api.get_card_set(revisable_only=True, today=1, db_path=init_db) == []
  assert api.get_card_set(show_question=1, db_path=init_db) == []
  assert api.get_card_set(show_question=1, show_answer=1, db_path=init_db) == []
  assert api.get_card_set(
    revisable_only='not boolean',
    show_question=True,
    today=True
  ) == []
  assert api.get_card_set(
    revisable_only='not boolean',
    show_question=True,
    today=card_obj
  ) == []
Ejemplo n.º 13
0
def test_if_revisable_only_is_true_return_only_cards_ready_for_revision(init_db):
  """
  If revisable_only=True is passed to get_card_set(), return cards that either
  don't have .date_updated set (None) or have their .score less than or equal
  to the difference between today and its .date_updated in days.
  """
  card_obj1 = knards.Card()
  card_obj2 = knards.Card(
    date_updated=(datetime.today() - timedelta(1)).strftime('%Y-%m-%d'),
    score=1
  )
  card_obj3 = knards.Card(
    date_updated=(datetime.today() - timedelta(1)).strftime('%Y-%m-%d'),
    score=2
  )

  api.create_card(card_obj1, init_db)
  api.create_card(card_obj2, init_db)
  api.create_card(card_obj3, init_db)

  assert len(api.get_card_set(revisable_only=False, db_path=init_db)) == 3
  assert len(api.get_card_set(revisable_only=True, db_path=init_db)) == 2
Ejemplo n.º 14
0
def test_if_markers_passed_in_method_returns_true_always(mocker, init_db):
  """
  If no cards contain the specified set of markers, no cards are removed from
  the DB and True is returned.
  """
  card_obj1 = knards.Card(markers='python specific')
  card_obj2 = knards.Card(markers='python nonspecific')
  card_obj3 = knards.Card(markers='javascript specific')
  api.create_card(card_obj1, init_db)
  api.create_card(card_obj2, init_db)
  api.create_card(card_obj3, init_db)

  assert len(api.get_card_set(db_path=init_db)) == 3

  mocker.patch(
    'knards.api.get_card_set',
    return_value=api.get_card_set(db_path=init_db)
  )
  assert api.delete_card(markers=['specific', 'test'], db_path=init_db) is True
  mocker.stopall()

  assert len(api.get_card_set(db_path=init_db)) == 3
Ejemplo n.º 15
0
def test_performance_on_11000_cards(init_db):
    """
  In this test we simulate a creation of 11000 cards (and storing them in the
  DB) and the following fetching for them in the DB, with respect to different
  constraints specified.
  """
    for i in range(10000):
        api.create_card(knards.Card(markers='first second third'), init_db)

    for i in range(1000):
        api.create_card(knards.Card(markers='first second fourth'), init_db)

    set1 = api.get_card_set(include_markers=['second'], db_path=init_db)
    set2 = api.get_card_set(include_markers=['second', 'third'],
                            db_path=init_db)
    set3 = api.get_card_set(include_markers=['second'],
                            exclude_markers=['third'],
                            db_path=init_db)

    assert len(set1) == 11000
    assert len(set2) == 10000
    assert len(set3) == 1000
Ejemplo n.º 16
0
def list(q, a, include_markers, exclude_markers):
    """
    Output a set of cards in the set up editor.
    """
    if include_markers is not None:
        include_markers = include_markers.split(',')
    else:
        include_markers = []
    if exclude_markers is not None:
        exclude_markers = exclude_markers.split(',')
    else:
        exclude_markers = []

    # fetch cards from the DB according to the constraints defined by input args
    card_set = api.get_card_set(
        show_question=q,
        show_answer=a,
        include_markers=include_markers,
        exclude_markers=exclude_markers,
    )

    # generate list buffer
    buf = ''
    for card in card_set:
        if card.date_updated is not None:
            date_updated = \
                card.date_updated.strftime('%d %b %Y')
        else:
            date_updated = 'Never'
        buf += msg.CARD_LIST_TEMPLATE.format(
            card.id,
            card.markers,
            card.pos_in_series,
            card.series,
            card.date_created.strftime('%d %b %Y'),
            date_updated,
            card.score,
        )
        if q:
            buf += '\n{}\n'.format(card.question)
            if a:
                buf += '{}\n'.format(msg.DIVIDER_LINE)
        if a:
            buf += '\n{}\n'.format(card.answer)

    util.open_in_editor(buf)
Ejemplo n.º 17
0
def test_method_returns_a_proper_card_obj_upon_success(mocker, init_db):
    """
  get_last_card() returns an object of type knards.Card that is a copy of the
  last stored card object.
  If markers argument is passed to the method, it returns an object of type
  knards.Card that is a copy of the last stored card that has ALL of the
  specified markers.
  """
    card_obj1 = knards.Card(markers='python specific test')
    card_obj2 = knards.Card(markers='javascript specific')
    card_obj3 = knards.Card(markers='javascript specific test',
                            date_created=(datetime.today() -
                                          timedelta(1)).strftime('%Y-%m-%d'))
    api.create_card(card_obj1, init_db)
    api.create_card(card_obj2, init_db)
    api.create_card(card_obj3, init_db)

    mocker.patch('knards.api.get_card_set',
                 return_value=api.get_card_set(db_path=init_db))
    result = api.get_last_card(db_path=init_db)
    assert result.id == 2

    result = api.get_last_card(markers=['specific', 'test'], db_path=init_db)
    assert result.id == 1
Ejemplo n.º 18
0
def mass_tag_assign(include_markers, exclude_markers, add_marker,
                    remove_marker):
    """
    [WIP] Mass assign of tags.
    """
    if include_markers is not None:
        include_markers = include_markers.split(',')
    else:
        include_markers = []
    if exclude_markers is not None:
        exclude_markers = exclude_markers.split(',')
    else:
        exclude_markers = []

    # fetch cards from the DB according to the constraints defined by input args
    card_set = api.get_card_set(include_markers=include_markers,
                                exclude_markers=exclude_markers)

    if add_marker is not None:
        for card in card_set:
            card = card._replace(markers=card.markers + ' ' + add_marker)
            api.update_card(card, update_now=False)

    if remove_marker is not None:
        for card in card_set:
            markers = card.markers
            i = markers.find(remove_marker)
            ilen = i + len(remove_marker)
            if ' ' + remove_marker + ' ' in markers:
                markers = markers[0:i] + markers[ilen + 1:len(markers)]
            elif remove_marker in markers and ilen == len(markers):
                markers = markers[0:i]
            elif remove_marker in markers and i == 0:
                markers = markers[ilen + 1:len(markers)]
            card = card._replace(markers=markers)
            api.update_card(card, update_now=False)
Ejemplo n.º 19
0
def recommend():
    """
    [WIP] Command to show recommendations.
    """

    # total_cards = len(api.get_card_set(
    #     revisable_only=True
    # ))

    tags_groups_indexes = [i for i, key in enumerate(config.get_tags_list())]
    tags_groups_names = [key for i, key in enumerate(config.get_tags_list())]
    tags_groups_total_amounts = []
    tags_groups_ready_for_revision = []

    for group_name in tags_groups_names:
        tags_groups_total_amounts.append(
            len(api.get_card_set(include_markers=[group_name])))
        tags_groups_ready_for_revision.append(
            len(
                api.get_card_set(revisable_only=True,
                                 include_markers=[group_name])))

    group_indexes_to_revise = []
    group_indexes_to_learn = []

    for index in tags_groups_indexes:
        if ((tags_groups_total_amounts[index] /
             (tags_groups_ready_for_revision[index] + 1)) > 2
                or tags_groups_total_amounts[index] == 0):
            group_indexes_to_learn.append(index)
        else:
            group_indexes_to_revise.append(index)

    if len(group_indexes_to_learn) == 0:
        click.secho('Nothing to learn just yet.\n', fg='red', bold=True)
    else:
        for index in group_indexes_to_learn:
            sorted_by_priority = {}
            for tag in config.get_tags_list()[tags_groups_names[index]]:
                total = len(api.get_card_set(include_markers=[tag]))
                if total not in sorted_by_priority:
                    sorted_by_priority[total] = []
                sorted_by_priority[total].append(tag)

            what = tags_groups_names[index]
            tags = ', '.join(sorted_by_priority[min(
                [val for i, val in enumerate(sorted_by_priority)])])
            click.secho('Learn {}: {}.\n'.format(what, tags),
                        fg='green',
                        bold=True)

    for index in group_indexes_to_revise:
        sorted_by_priority = {}
        for tag in config.get_tags_list()[tags_groups_names[index]]:
            total = api.get_card_set(include_markers=[tag])
            revisable = api.get_card_set(revisable_only=True,
                                         include_markers=[tag])
            priority = 0
            for card in revisable:
                if card.date_updated:
                    priority += (datetime.now() - card.date_updated).days
                else:
                    priority += (datetime.now() - card.date_created).days
            if len(total) != 0:
                priority = round(priority / len(total))
            if priority not in sorted_by_priority:
                sorted_by_priority[priority] = []
            sorted_by_priority[priority].append(tag)

        what = tags_groups_names[index]
        if sorted_by_priority != {}:
            tags = ', '.join(sorted_by_priority[max(
                [val for i, val in enumerate(sorted_by_priority)])])
            click.secho('Revise {}: {}.'.format(what, tags),
                        fg='yellow',
                        bold=True)
        else:
            click.secho('Revise {}: nothing to revise.'.format(what),
                        fg='red',
                        bold=True)
Ejemplo n.º 20
0
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)
Ejemplo n.º 21
0
def test_empty_DB_returns_empty_list(init_db):
  """
  get_card_set() upon an empty DB must not raise no exceptions, but return []
  instead.
  """
  assert api.get_card_set(db_path=init_db) == []
Ejemplo n.º 22
0
def revise(include_markers, exclude_markers):
    """Revise a set of cards"""

    # Exit codes:
    # 0: success
    # 1: unknown error
    # 2: bad input arguments
    # 3: sqlite3 module exception
    # 4: api method got wrong input
    # 5: DB file not found
    # 6: no cards adhere to constraints

    if include_markers:
        include_markers = [
            a for a in \
            re.split(r'(\s|,)', include_markers.strip(''))
            if a != ' ' and a != ','
        ]
    if exclude_markers:
        exclude_markers = [
            a for a in \
            re.split(r'(\s|,)', exclude_markers.strip(''))
            if a != ' ' and a != ','
        ]

    try:
        card_set = api.get_card_set(include_markers=include_markers,
                                    exclude_markers=exclude_markers)
    except TypeError as e:
        click.secho(e.args[0], fg='red', bold=True)
        sys.exit(4)
    except exceptions.DBFileNotFound as e:
        click.secho(e.args[0], fg='red', bold=True)
        sys.exit(5)
    except exceptions.EmptyDB as e:
        click.secho(e.args[0], fg='red', bold=True)
        sys.exit(6)

    # sort the card set
    never_updated = []
    updated = []
    for card in card_set:
        if not card.date_updated:
            never_updated.append(card)
            continue
        updated.append(card)
    updated.sort(key=lambda obj: (obj.score, obj.date_updated))
    never_updated.sort(key=lambda obj: obj.date_created)

    # we want this: first go all cards that were ever revised, then unrevised
    card_set = never_updated + updated

    # proceed to revising cards
    while card_set:
        card_obj = card_set.pop(0)

        # if the card is part of series, pick out all cards of that series
        if card_obj.series:
            try:
                subset = api.get_series_set(card_obj.series)
                subset_length = len(subset)
            except (TypeError, sqlite3.OperationalError,
                    exceptions.DBFileNotFound, exceptions.EmptyDB):
                subset = {1: card_obj}
                subset_length = 1

            filtered_subset = {}
            for series_obj_num in subset:
                if subset[series_obj_num].date_updated is not None:
                    if subset[series_obj_num].score < (
                            datetime.now().date() -
                            subset[series_obj_num].date_updated.date()).days:
                        filtered_subset[series_obj_num] = subset[
                            series_obj_num]
                else:
                    filtered_subset[series_obj_num] = subset[series_obj_num]

            while filtered_subset:
                series_obj = filtered_subset.pop(min(filtered_subset.keys()))

                try:
                    util.ask(series_obj, subset_length)
                except ValueError as e:
                    # from api.update_card
                    pass
                except sqlite3.OperationalError as e:
                    # from api.update_card
                    pass

        # else, just ask the question
        else:
            # check if the card is ready to be revised
            if card_obj.date_updated is not None:
                if card_obj.score > (datetime.now().date() -
                                     card_obj.date_updated.date()).days:
                    continue

            try:
                util.ask(card_obj)
            except ValueError as e:
                # from api.update_card
                pass
            except sqlite3.OperationalError as e:
                # from api.update_card
                pass