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 # 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 # 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. # 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 click
genanki genanki
pandas pandas
pyyaml pyyaml
bullet

View File

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

View File

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

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 re
import click
from ankimaker.commands import cli from ankimaker.commands import cli
from ankimaker.tasks import basic_pandas_to_anki 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 .load_config import load_config_file
from .configuration import AnkimakerConfig as Config from .configuration import AnkimakerConfig as Config
from .filters import FilterConfig

View File

@ -1,4 +1,9 @@
import yaml import yaml
from typing import List
from .filters import FilterConfig
_empty_list = ()
class AnkimakerConfig(yaml.YAMLObject): class AnkimakerConfig(yaml.YAMLObject):
@ -7,17 +12,41 @@ class AnkimakerConfig(yaml.YAMLObject):
question_column = None question_column = None
answer_column = None answer_column = None
separators = ',' separators = ','
filters: List[List[FilterConfig]] = list()
def __init__(self, header=None, answer_column=None, question_column=None): def __init__(
AnkimakerConfig.answer_column = answer_column self, separators=',', header=None, answer_column=None, question_column=None,
AnkimakerConfig.question_column = question_column filters=tuple(), *args, **karhs
AnkimakerConfig.header = header ):
AnkimakerConfig.AnkimakerConfig = AnkimakerConfig self.answer_column = answer_column
self.question_column = question_column
self.header = header
self.separators = separators
self.filters = _conditionally_create_new_filters(filters)
@staticmethod @staticmethod
def loader(configuration_content): 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.header = content.header
AnkimakerConfig.question_column = content.question_column AnkimakerConfig.question_column = content.question_column
AnkimakerConfig.answer_column = content.answer_column AnkimakerConfig.answer_column = content.answer_column
AnkimakerConfig.separators = content.separators 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 import yaml
from pathlib import Path
from .configuration import AnkimakerConfig from .configuration import AnkimakerConfig
@ -10,7 +11,7 @@ def load_config_file(file_path: str):
:param file_path: Path to yaml file with configuration :param file_path: Path to yaml file with configuration
:return: Dict config :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.exists()
assert file_path.is_file() assert file_path.is_file()
with open(file_path, 'r') as file: with open(file_path, 'r') as file:

View File

@ -1,5 +1,5 @@
from . import ( from . import (
deck, 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 .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 genanki
import pandas as pd import pandas as pd
from typing import List
from functools import reduce
from ankimaker.config import Config
from ankimaker import generator, config from ankimaker import generator, config
from ankimaker.config import Config, FilterConfig
def create_model(): def load_csv(path: str) -> pd.DataFrame:
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):
df = pd.read_csv(path, header=Config.header, sep=Config.separators) df = pd.read_csv(path, header=Config.header, sep=Config.separators)
df_columns_are_unnamed = all(map(lambda x: str(x).isnumeric(), df.columns)) df_columns_are_unnamed = all(map(lambda x: str(x).isnumeric(), df.columns))
if df_columns_are_unnamed: if df_columns_are_unnamed:
Config.answer_column = int(Config.answer_column) Config.answer_column = int(Config.answer_column)
Config.question_column = int(Config.question_column) Config.question_column = int(Config.question_column)
df = apply_filters(df)
return df return df
def add_df_to_deck(df: pd.DataFrame, deck: genanki.Deck): def add_df_to_deck(df: pd.DataFrame, deck: genanki.Deck) -> genanki.Deck:
model = create_model() model = generator.create_model()
for entry in df.to_dict('records'): for entry in df.to_dict('records'):
question = entry[Config.question_column] question = entry[Config.question_column]
answer = entry[Config.answer_column] answer = entry[Config.answer_column]
content_fields = (question, answer) content_fields = (question, answer)
note = create_note(model, fields=content_fields) note = generator.create_note(model, fields=content_fields)
deck.add_note(note) deck.add_note(note)
return deck return deck
def handle_config(config_file_path): def handle_config(config_file_path: str):
if config_file_path is None: if config_file_path is None:
Config.header = None Config.header = None
Config.question_column = 0 Config.question_column = 0
@ -62,6 +38,60 @@ def handle_config(config_file_path):
config.load_config_file(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): def basic_pandas_to_anki(csv_path, output_path, name, config_file_path):
handle_config(config_file_path) handle_config(config_file_path)
df = load_csv(csv_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