Compare commits

...

3 Commits

Author SHA1 Message Date
gabriel becker
79e7714395 Add filter creation in configuration creation command. 2022-12-09 18:34:19 +11:00
gabriel becker
eaa82edc81 Create filters in configuration and implement it. 2022-12-09 12:56:02 +11:00
gabriel becker
01eb182c84 Create configuration command for user input configuration generation. 2022-11-30 17:36:01 +11:00
22 changed files with 389 additions and 55 deletions

5
.gitignore vendored
View File

@ -157,4 +157,7 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/
# Project Specific
scripts/

View File

@ -1,4 +1,5 @@
click
genanki
pandas
pyyaml
pyyaml
bullet

View File

@ -8,7 +8,7 @@ def readme():
setup(
name='ankimaker',
version='0.0.4',
version='0.0.5',
description='Makes anki with files',
url="https://git.lgoon.xyz/gabriel/ankimaker",
license="BSD-3-Clause",
@ -27,6 +27,7 @@ setup(
"genanki",
"pandas",
"pyyaml",
"bullet"
],
long_description_content_type='text/markdown',
)

View File

@ -1,8 +1,3 @@
import click
@click.group("cli")
def cli():
pass
from ..commands.from_csv import generate_anki
from .base_click import cli
from .from_csv import generate_anki
from .make_config import make_csv_config

View File

@ -0,0 +1,6 @@
import click
@click.group("cli")
def cli():
pass

View File

@ -1,5 +1,6 @@
import click
import re
import click
from ankimaker.commands import cli
from ankimaker.tasks import basic_pandas_to_anki

View File

@ -0,0 +1,31 @@
import click
from ankimaker.commands import cli
from ankimaker import tasks, utils
_YAML = ['yml', 'yaml']
_CSV = ['csv']
@cli.command('make-csv-config')
@click.option('-i', '--input', 'input_file', type=click.Path(exists=True))
@click.option('-o', '--output', 'output_path', type=click.Path(exists=False))
def make_csv_config(
input_file, output_path
):
"""
Menu for creating a config file.
:param input_file: filepath of the csv used as sample to create configuration
:param output_path: filepath where configuration will be saved
"""
filetype = utils.files.get_fyle_type(input_file)
if filetype in _CSV:
tasks.config_tasks.create_config(input_file, output_path)
elif filetype in _YAML:
tasks.config_tasks.enhance_config(input_file)
else:
click.echo('Wrong File Type. Should be CSV or YAML.')

View File

@ -1,2 +1,3 @@
from .load_config import load_config_file
from .configuration import AnkimakerConfig as Config
from .filters import FilterConfig

View File

@ -1,4 +1,9 @@
import yaml
from typing import List
from .filters import FilterConfig
_empty_list = ()
class AnkimakerConfig(yaml.YAMLObject):
@ -7,17 +12,41 @@ class AnkimakerConfig(yaml.YAMLObject):
question_column = None
answer_column = None
separators = ','
filters: List[List[FilterConfig]] = list()
def __init__(self, header=None, answer_column=None, question_column=None):
AnkimakerConfig.answer_column = answer_column
AnkimakerConfig.question_column = question_column
AnkimakerConfig.header = header
AnkimakerConfig.AnkimakerConfig = AnkimakerConfig
def __init__(
self, separators=',', header=None, answer_column=None, question_column=None,
filters=tuple(), *args, **karhs
):
self.answer_column = answer_column
self.question_column = question_column
self.header = header
self.separators = separators
self.filters = _conditionally_create_new_filters(filters)
@staticmethod
def loader(configuration_content):
content = configuration_content['AnkimakerConfig']
if isinstance(configuration_content, dict):
content = configuration_content['AnkimakerConfig']
else:
content = configuration_content
AnkimakerConfig.header = content.header
AnkimakerConfig.question_column = content.question_column
AnkimakerConfig.answer_column = content.answer_column
AnkimakerConfig.separators = content.separators
AnkimakerConfig.filters = _conditionally_create_new_filters(content.filters)
def _conditionally_create_new_filters(filters):
conf_has_filters = len(filters) > 0
if conf_has_filters:
should_cast_filter = not isinstance(filters[0][0], FilterConfig)
if should_cast_filter:
new_filters = [
[FilterConfig(**x) for x in or_filter]
for or_filter in filters
]
else:
new_filters = filters
return new_filters
return list()

View File

@ -0,0 +1,19 @@
import yaml
from typing import List, Union
class FilterConfig(yaml.YAMLObject):
yaml_tag = '!fitlerconfig'
column: Union[str, int]
values: Union[List[Union[int, str]], Union[int, str]]
def __init__(self, column: str, values: Union[List[Union[int, str]], Union[int, str]]):
self.column = column
self.values = values
def __str__(self):
return f'<F({self.column}:{self.values})>'
def __repr__(self):
return self.__str__()

View File

@ -1,5 +1,6 @@
from pathlib import Path
import os
import yaml
from pathlib import Path
from .configuration import AnkimakerConfig
@ -10,7 +11,7 @@ def load_config_file(file_path: str):
:param file_path: Path to yaml file with configuration
:return: Dict config
"""
file_path = Path(file_path)
file_path = Path(file_path if '~' not in file_path else os.path.expanduser(file_path))
assert file_path.exists()
assert file_path.is_file()
with open(file_path, 'r') as file:

View File

@ -1,5 +1,5 @@
from . import (
deck,
# models,
# card
)
from .card import create_note
from .model import create_model

View File

@ -0,0 +1,9 @@
import genanki
def create_note(model, fields):
note = genanki.Note(
model=model,
fields=fields
)
return note

View File

@ -0,0 +1,20 @@
import genanki
def create_model():
my_model = genanki.Model(
1607392319,
'Simple Model',
fields=[
{'name': 'Question'},
{'name': 'Answer'},
],
templates=[
{
'name': 'Card 1',
'qfmt': '<div style="text-align: center;">{{Question}}</div>',
'afmt': '{{FrontSide}}<hr id="answer"><div style="text-align: center;">{{Answer}}</div>',
},
]
)
return my_model

View File

@ -0,0 +1,17 @@
import genanki as anki
simple_flashcard = anki.Model(
16073923194617823,
name='simple_flashcard',
fields=[
{'name': 'word'},
{'name': 'meaning'}
],
templates=[
{
'name': 'geneticname',
'qfmt': '{{word}}',
'afmt': '{{FrontSide}}<hr id="answer">{{meaning}}'
}
]
)

View File

@ -1 +1,2 @@
from .basic_csv_to_anki import basic_pandas_to_anki
from .config_tasks import create_config, enhance_config

View File

@ -1,59 +1,35 @@
import genanki
import pandas as pd
from typing import List
from functools import reduce
from ankimaker.config import Config
from ankimaker import generator, config
from ankimaker.config import Config, FilterConfig
def create_model():
my_model = genanki.Model(
1607392319,
'Simple Model',
fields=[
{'name': 'Question'},
{'name': 'Answer'},
],
templates=[
{
'name': 'Card 1',
'qfmt': '<div style="text-align: center;">{{Question}}</div>',
'afmt': '{{FrontSide}}<hr id="answer"><div style="text-align: center;">{{Answer}}</div>',
},
]
)
return my_model
def create_note(model, fields):
note = genanki.Note(
model=model,
fields=fields
)
return note
def load_csv(path):
def load_csv(path: str) -> pd.DataFrame:
df = pd.read_csv(path, header=Config.header, sep=Config.separators)
df_columns_are_unnamed = all(map(lambda x: str(x).isnumeric(), df.columns))
if df_columns_are_unnamed:
Config.answer_column = int(Config.answer_column)
Config.question_column = int(Config.question_column)
df = apply_filters(df)
return df
def add_df_to_deck(df: pd.DataFrame, deck: genanki.Deck):
model = create_model()
def add_df_to_deck(df: pd.DataFrame, deck: genanki.Deck) -> genanki.Deck:
model = generator.create_model()
for entry in df.to_dict('records'):
question = entry[Config.question_column]
answer = entry[Config.answer_column]
content_fields = (question, answer)
note = create_note(model, fields=content_fields)
note = generator.create_note(model, fields=content_fields)
deck.add_note(note)
return deck
def handle_config(config_file_path):
def handle_config(config_file_path: str):
if config_file_path is None:
Config.header = None
Config.question_column = 0
@ -62,6 +38,60 @@ def handle_config(config_file_path):
config.load_config_file(config_file_path)
def apply_filters(df: pd.DataFrame) -> pd.DataFrame:
"""
Returns filtered dataframe removing any row that does not correspond to at least one
of the filter groups defined in Configuration.
:param df: Original dataframe.
:return: Filtered Dataframe.
"""
there_are_no_filter_to_apply = len(Config.filters) == 0
if there_are_no_filter_to_apply:
return df
is_in_configured_filter_rules = load_filter_from_config(df)
df_filtered = df[is_in_configured_filter_rules]
return df_filtered
def load_filter_from_config(df: pd.DataFrame) -> pd.Series:
"""
Given a dataframe, returns a series indicating which rows should be kept according to loaded
Config [AnkimakerConfig]. The rows presented in any filter group should be kept.
:param df: Original dataframe.
:return pd.Series: Boolean Series to filter df.
"""
group_filters: List[pd.Series] = list()
for group in Config.filters:
if len(group) > 0:
group_filters.append(
create_group_filter(df, group)
)
config_filter = reduce(lambda a, b: a | b, group_filters)
return config_filter
def create_group_filter(df: pd.DataFrame, group: List[FilterConfig]) -> pd.Series:
"""
Creates a boolean series indicating which rows are in the filters configuration defined
group to be used to filter the dataframe.
:param df: Input dataframe to be filtered.
:param group: Filter defined Group.
:return: Series of boolean indicating rows that are in the group.
"""
rule: FilterConfig
query: List[pd.Series] = list()
for rule in group:
__assert_rule_is_valid(df, rule)
is_in_rule = df[rule.column].apply(lambda x: x in rule.values)
query.append(is_in_rule)
is_in_group = reduce(lambda a, b: a & b, query)
return is_in_group
def __assert_rule_is_valid(df: pd.DataFrame, rule: FilterConfig):
assert rule.column in df.columns
def basic_pandas_to_anki(csv_path, output_path, name, config_file_path):
handle_config(config_file_path)
df = load_csv(csv_path)

View File

@ -0,0 +1,2 @@
from .create_config import create_config
from .enhance_config import enhance_config

View File

@ -0,0 +1,161 @@
import os
import yaml
import click
import pandas as pd
from typing import Type, List
from bullet import Bullet, Input, YesNo
from ankimaker.config import Config, FilterConfig
__CONFIRMATION_QUESTION = """
Is this read option {option} correct?
______________
{preview}
"""
__SUCCESS_MESSAGE = """
All done. You can now use your configuration with this command:
{command}
"""
__COMMAND_SAMPLE = """ankimaker csv \
-i {input} \
-o my-new-deck.apkg \
--conf {output}
"""
__ADD_FILTER_QUESTION = """Do you want do add a filter to the configuration?"""
def create_config(input_file, output_path):
separators = handle_read_option(
input_file, read_option='sep', sep=','
)
header = handle_read_option(
input_file, read_option='header', header=None,
sep=separators, option_type=int
)
question_column = get_column('question')
answer_column = get_column('answer')
filters = process_filters(input_file, header, separators)
new_config = Config(
separators=separators,
header=header,
question_column=question_column,
answer_column=answer_column,
filters=filters
)
save_file(new_config, output_path)
finish_message = __SUCCESS_MESSAGE.format(command=make_sample_command(input_file, output_path))
click.clear()
click.echo(finish_message)
def process_filters(input_file, header, separators):
df = pd.read_csv(input_file, header=header, sep=separators)
filters = add_filters_to_config(df)
return filters
def __inline_yes_or_no_question(question):
answer = YesNo(prompt=question, default='n').launch()
return answer
def add_filters_to_config(df: pd.DataFrame) -> List[List[FilterConfig]]:
config = Config()
should_add_filter = __inline_yes_or_no_question(__ADD_FILTER_QUESTION)
while should_add_filter:
config = add_filter_to_or_create_filter_group(df, config)
should_add_filter = __inline_yes_or_no_question(__ADD_FILTER_QUESTION)
return config.filters
def add_filter_to_or_create_filter_group(df: pd.DataFrame, config: Config) -> Config:
config_has_filters = len(config.filters) > 0
chosen_group = -1
if config_has_filters:
filter_options = [f'({"|".join(map(str, group)):.45s})' for group in config.filters]
filter_options = [f'Group{i+1}{s}' for i, s in enumerate(filter_options)]
cli = Bullet(
prompt="Select group: ",
choices=["Create new", *filter_options],
return_index=True,
)
chosen_group = cli.launch()[1] - 1
new_filter = create_filter_config(df)
if chosen_group < 0:
config.filters.append([new_filter])
else:
config.filters[chosen_group].append(new_filter)
return config
def create_filter_config(df: pd.DataFrame) -> FilterConfig:
options = list(df.columns)
cli = Bullet(
prompt="Select a columns to filter: ",
choices=list(map(str, options)),
return_index=True
)
chosen = cli.launch()[1]
filter_column = options[chosen]
columns_values = df[filter_column].unique()
values = Input(f'Which values fo filter out? values[{columns_values}]: ').launch()
new_filter = FilterConfig(column=filter_column, values=values)
return new_filter
def get_column(name: str) -> str:
answer = click.prompt(f'Which is your {name} column?', type=str, confirmation_prompt=True)
return answer
def handle_read_option(input_file, read_option, option_type: Type = str, **kargs):
preview: str
is_finished = False
while not is_finished:
preview = load_preview(input_file, **kargs)
question = __CONFIRMATION_QUESTION.format(
option=str(read_option),
preview=preview
)
answer: str = click.prompt(question, type=str, default='y')
click.clear()
if answer.lower()[0] == 'y':
return kargs[read_option]
else:
updated_option: str = click.prompt('Insert the correct', type=option_type)
kargs[read_option] = updated_option
click.clear()
def load_preview(input_file, *args, **kargs):
try:
df = pd.read_csv(input_file, *args, **kargs)
preview = df.head()
except pd.errors.ParserError:
preview = 'FAIL'
return preview
def save_file(config: Config, file_path):
if '~' in file_path:
file_path = os.path.expanduser(file_path)
with open(file_path, 'w') as f:
yaml.dump(config, f)
def make_sample_command(input_config, output):
command = __COMMAND_SAMPLE.format(
input=input_config, output=output
)
return command

View File

@ -0,0 +1,2 @@
def enhance_config(filename):
raise NotImplementedError()

View File

@ -0,0 +1 @@
from . import files

View File

@ -0,0 +1,3 @@
def get_fyle_type(filename: str) -> str:
filetype = filename.split('.')[-1] if len(filename.split('.')) > 0 else None
return filetype