diff --git a/README.md b/README.md index c5a565d..926bbfc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ jekyll-utils ======= +**TODO:** + - Add this to PyPa (tutorial here: [Package a Python Project and make it available on PyPa](https://queirozf.com/entries/package-a-python-project-and-make-it-available-via-pip-install-simple-example)) + - Add tests for public functions + A small Python 3 command-line application (based upon [click](http://click.pocoo.org/6/)) to speed up some common tasks for those who blog using Jekyll. This will be **especially** useful for people who post many short blog entries, as opposed to those who prefer to blog less frequently, with longer posts. @@ -9,18 +13,20 @@ This will be **especially** useful for people who post many short blog entries, - Prerequisites: `virtualenv` - - On Ubuntu, install it via `$ sudo apt-get install python3-virtualenv` + - On Ubuntu, install it via `$ sudo apt-get install python3-virtualenv` -- Installing jekyll-utils using pip +- Installing jekyll-utils from Github using pip - - `$ git clone https://github.com/queirozfcom/jekyll-utils` - - `$ cd jekyll-utils` - - `$ virtualenv -p python3 jekyll-venv` - - `$ source jekyll-venv/bin/activate` - - `$ pip install .` + - `$ git clone https://github.com/queirozfcom/jekyll-utils` + - `$ cd jekyll-utils` + - `$ virtualenv -p python3 jekyll-venv` + - `$ source jekyll-venv/bin/activate` + - `$ pip install .` This will install all commands to your virtualenv. Type `jk-` and then hit `` to see all available commands. + ![jekyll-utils-commands](https://i.imgur.com/f1sF6Iq.gif) + - Initial configs. These are needed to start using the tools: - `$ jk-config-set-editor ` diff --git a/jekyllutils/configs.py b/jekyllutils/configs.py index f9d7f22..7d62f66 100644 --- a/jekyllutils/configs.py +++ b/jekyllutils/configs.py @@ -1,24 +1,23 @@ import click -from jekyllutils.helpers import files, configs -from jekyllutils.helpers.messages import wrap_success +from jekyllutils.helpers import configs +from jekyllutils import files +from jekyllutils.helpers.colours import with_success_prefix @click.command() @click.argument('path') -def set_path_to_posts_dir(path): +def set_posts_path(path): absolute_path = files.resolve_path(path) - configs.set_path_to_posts_dir(absolute_path) - click.echo(wrap_success( - """Config key "path-to-jekyll-posts" was set to "{0}" """.format(path))) + configs.set_posts_path_dir(absolute_path) + click.echo(with_success_prefix(f"""Config key "posts-path" was set to "{path}" """)) @click.command() @click.argument('name') def set_editor(name): configs.set_editor_name(name) - click.echo( - wrap_success("""Config key "editor" was set to "{0}" """.format(name))) + click.echo(with_success_prefix(f"""Config key "editor" was set to "{name}" """)) @click.command() @@ -29,4 +28,4 @@ def dump_configs(): @click.command() def clear_configs(): configs.clear_configs() - click.echo(wrap_success("Configs cleared")) + click.echo(with_success_prefix("Configs cleared")) diff --git a/jekyllutils/helpers/files.py b/jekyllutils/files.py similarity index 64% rename from jekyllutils/helpers/files.py rename to jekyllutils/files.py index 72771c8..c9e233f 100644 --- a/jekyllutils/helpers/files.py +++ b/jekyllutils/files.py @@ -1,6 +1,9 @@ +import os +import re from pathlib import Path -import os +from jekyllutils.helpers.colours import wrap_yellow, wrap_blue, wrap_green +from jekyllutils.helpers.text import match_all, filter_match_all def resolve_path(path, strip_trailing_slash=True): @@ -28,7 +31,7 @@ def list_files(absolute_directory, keywords): matches = [] for root, dirnames, filenames in os.walk(absolute_directory): - for filename in _filter_match_all(filenames, keywords): + for filename in filter_match_all(filenames, keywords): matches.append(os.path.join(root, filename)) # remove the paths, return only file names @@ -56,7 +59,7 @@ def list_filenames_by_tag(absolute_directory, tags): with open(absolute_path_to_file, "r") as f: for line in f: if line.strip().startswith("tags:"): - if _match_all(line, tags): + if match_all(line, tags): matches.append(filename) break @@ -69,7 +72,7 @@ def list_unpublished_filenames(absolute_directory, include_wip): (i.e. files where published: false in front-matter) :param absolute_directory: - :param include_wip: if true, also return files that contain "wip alert" + :param include_wip: if true, also return files that contain "wip alert" and "TODO" :return: a list of the filenames """ matches = [] @@ -79,47 +82,25 @@ def list_unpublished_filenames(absolute_directory, include_wip): absolute_path_to_file = os.path.join(root, filename) with open(absolute_path_to_file, "r") as f: - for line in f: - if line.strip().startswith("published:"): - if _match_all(line, ("false",)): - matches.append("[UNP]" + filename) - break - if include_wip and ("wip alert" in line.lower()): - matches.append("[WIP]" + filename) - break - return matches - - -def _match_all(s, keywords): - """ - True if all strings in keywords are contained in s, False otherwise. - Case-insensitive. - - :param s: string - :param keywords: a tuple containing keywords that should all be included - :return: True if all strings in keywords are contained in s, False otherwise - """ - - for kw in keywords: - if kw.lower().strip() not in s.lower().strip(): - return False - - return True - - -def _filter_match_all(elements, keywords): - """ - Returns the elements for which all keywords are contained. + for line in f: - :param elements: a list of strings to filter - :param keywords: a tuple containing keywords that should all be included - :return: matching matching elements - """ - matching = [] + line_clean = line.strip().lower() - for elem in elements: - if all(keyword in elem for keyword in keywords): - matching.append(elem) + if line_clean.startswith("published:") and match_all(line_clean, ("false",)): + prefix = wrap_yellow("[UNP] ") + matches.append(f"{prefix} {filename}") + break + elif include_wip and ("wip alert" in line_clean): + prefix = wrap_blue("[WIP] ") + matches.append(f"{prefix} {filename}") + break + elif include_wip and (re.search("todo: ", line_clean) or + re.search("todo\n", line_clean) or + re.search("^todo ", line_clean) or + re.search(" todo ", line_clean)): + prefix = wrap_green("[TODO]") + matches.append(f"{prefix} {filename}") + break - return matching + return list(set(matches)) diff --git a/jekyllutils/generators.py b/jekyllutils/generators.py index 2af44a3..93e4d6a 100644 --- a/jekyllutils/generators.py +++ b/jekyllutils/generators.py @@ -4,9 +4,9 @@ from subprocess import call import click -from jekyllutils.helpers.configs import get_path_to_posts_dir, get_editor_name +from jekyllutils.helpers.configs import get_posts_path, get_editor_name from jekyllutils.helpers.editors import get_executable_from_name -from jekyllutils.helpers.messages import wrap_success +from jekyllutils.helpers.colours import with_success_prefix from slugify import slugify @@ -33,7 +33,7 @@ def new_post(title, tag, image): file_name = date.strftime("%Y-%m-%d") + "-" + slug + ".markdown" - path_to_file = get_path_to_posts_dir().rstrip("/") + "/" + file_name + path_to_file = get_posts_path().rstrip("/") + "/" + file_name with open(path_to_file, "w") as f: f.write(textwrap @@ -46,7 +46,7 @@ def new_post(title, tag, image): call([editor_executable, path_to_file]) time.sleep(2) # just to give the os time for the editor to load - click.echo(wrap_success("Post created at: {0}".format(path_to_file))) + click.echo(with_success_prefix("Post created at: {0}".format(path_to_file))) @click.command() @@ -60,7 +60,7 @@ def new_post_paper_summary(title, tag): contents = _get_contents_paper_summary() - title_full = "Paper Summary: "+title + title_full = "Paper Summary: " + title slug = slugify(title_full) date = datetime.now() @@ -69,7 +69,7 @@ def new_post_paper_summary(title, tag): file_name = date.strftime("%Y-%m-%d") + "-" + slug + ".markdown" - path_to_file = get_path_to_posts_dir().rstrip("/") + "/" + file_name + path_to_file = get_posts_path().rstrip("/") + "/" + file_name with open(path_to_file, "w") as f: f.write(textwrap @@ -82,7 +82,8 @@ def new_post_paper_summary(title, tag): call([editor_executable, path_to_file]) time.sleep(2) # just to give the os time for the editor to load - click.echo(wrap_success("Post (Paper Summary) created at: {0}".format(path_to_file))) + click.echo(with_success_prefix("Post (Paper Summary) created at: {0}".format(path_to_file))) + @click.command() @click.option('--tag', '-t', multiple=True, help="Multiple values allowed") @@ -95,7 +96,7 @@ def new_post_crypto_asset_overview(title, tag): contents = _get_contents_crypto_asset_overview() - title_full = "Crypto Asset Overview: "+title + title_full = "Crypto Asset Overview: " + title slug = slugify(title_full) date = datetime.now() @@ -104,7 +105,7 @@ def new_post_crypto_asset_overview(title, tag): file_name = date.strftime("%Y-%m-%d") + "-" + slug + ".markdown" - path_to_file = get_path_to_posts_dir().rstrip("/") + "/" + file_name + path_to_file = get_posts_path().rstrip("/") + "/" + file_name with open(path_to_file, "w") as f: f.write(textwrap @@ -117,7 +118,8 @@ def new_post_crypto_asset_overview(title, tag): call([editor_executable, path_to_file]) time.sleep(2) # just to give the os time for the editor to load - click.echo(wrap_success("Post (Paper Summary) created at: {0}".format(path_to_file))) + click.echo(with_success_prefix("Post (Paper Summary) created at: {0}".format(path_to_file))) + def _get_contents_no_img(): return """ @@ -158,6 +160,7 @@ def _get_contents_img(): """ + def _get_contents_paper_summary(): return """ --- diff --git a/jekyllutils/helpers/colours.py b/jekyllutils/helpers/colours.py new file mode 100644 index 0000000..88b5030 --- /dev/null +++ b/jekyllutils/helpers/colours.py @@ -0,0 +1,18 @@ +def with_error_prefix(msg): + return f"\033[31mERROR: \033[00m{msg}" + + +def with_success_prefix(msg): + return f"\033[32mSUCCESS: \033[00m{msg}" + + +def wrap_yellow(msg): + return f"\033[33m{msg}\033[00m" + + +def wrap_green(msg): + return f"\033[32m{msg}\033[00m" + + +def wrap_blue(msg): + return f"\033[34m{msg}\033[00m" diff --git a/jekyllutils/helpers/configs.py b/jekyllutils/helpers/configs.py index d6072a0..f66731f 100644 --- a/jekyllutils/helpers/configs.py +++ b/jekyllutils/helpers/configs.py @@ -4,11 +4,11 @@ import os import toml from appdirs import user_config_dir -from jekyllutils.helpers.messages import wrap_error +from jekyllutils.helpers.colours import with_error_prefix, wrap_yellow, with_success_prefix -def get_path_to_posts_dir(): - return _get_config("path-to-jekyll-posts") +def get_posts_path(): + return _get_config("posts-path") def get_editor_name(): @@ -37,35 +37,46 @@ def set_editor_name(name): _set_config("editor", name) -def set_path_to_posts_dir(path): - _set_config("path-to-jekyll-posts", path) +def set_posts_path_dir(path): + _set_config("posts-path", path) # private def _set_config(name, value): - with open(_get_config_file(), "r+") as f: + _create_config_file_if_needed() + + with open(_get_path_to_config_file(), "a+") as f: + + # move the pointer to the start because we want to read the file first + f.seek(0) config = toml.loads(f.read()) config[name] = value new_conf = toml.dumps(config) + + # then delete everything and write the new config f.truncate(0) f.seek(0) f.write(new_conf) + def _get_config(name): try: - with open(_get_config_file()) as f: + with open(_get_path_to_config_file()) as f: config = toml.loads(f.read()) return config[name] except KeyError as e: - print(wrap_error( - """Please set a value for key "{0}" in the config.""".format(name))) + + sample_command = f"jk-config-set-{name} foo-bar" + + print(with_error_prefix( + f"""Please set a value for key "{name}" in the config.\nFor example: {wrap_yellow(sample_command)}""")) sys.exit(1) def _create_config_file_if_needed(): - filename = _get_config_file() + filename = _get_path_to_config_file() if os.path.isfile(filename): return @@ -78,13 +89,14 @@ def _create_config_file_if_needed(): raise with open(filename, "w") as f: + print(with_success_prefix(f"Created new config file at: {filename}")) f.write("") -def _get_config_file(): +def _get_path_to_config_file(): return user_config_dir("jekyll-utils", "queirozfcom") + "/config.toml" def _raise_error_if_no_config(): - if not os.path.isfile(_get_config_file()): + if not os.path.isfile(_get_path_to_config_file()): raise FileNotFoundError("Config file not found") diff --git a/jekyllutils/helpers/messages.py b/jekyllutils/helpers/messages.py deleted file mode 100644 index 68b598e..0000000 --- a/jekyllutils/helpers/messages.py +++ /dev/null @@ -1,6 +0,0 @@ -def wrap_error(msg): - return "\033[31mERROR: \033[00m{0}".format(msg) - - -def wrap_success(msg): - return "\033[32mSUCCESS: \033[00m{0}".format(msg) diff --git a/jekyllutils/helpers/sorting.py b/jekyllutils/helpers/sorting.py index b453e4f..c0219de 100644 --- a/jekyllutils/helpers/sorting.py +++ b/jekyllutils/helpers/sorting.py @@ -1,11 +1,11 @@ import re -def sort_ignoring_brackets(list_of_filenames, reverse=None): +def sort_filenames_ignoring_leading_text(list_of_filenames, reverse=None): """ need a custom function because we don't want strings starting with [...] before everything else - :param list_of_filenames: + :param list_of_prefixed_filenames: list of strings of the form ".... yyyy-mm-dd-post-title-slug.markdown" :param reverse: :return: """ @@ -13,4 +13,6 @@ def sort_ignoring_brackets(list_of_filenames, reverse=None): if reverse is None: reverse = False - return sorted(list_of_filenames, key=lambda a: re.sub("(?i)^\[\w+\]\s*", "", a), reverse=reverse) + leading_pattern_to_remove = r"""^[^]]+\]\s*""" + + return sorted(list_of_filenames, key=lambda a: re.sub(leading_pattern_to_remove, "", a), reverse=reverse) diff --git a/jekyllutils/helpers/text.py b/jekyllutils/helpers/text.py new file mode 100644 index 0000000..e063371 --- /dev/null +++ b/jekyllutils/helpers/text.py @@ -0,0 +1,32 @@ +def match_all(s, keywords): + """ + True if all strings in keywords are contained in s, False otherwise. + Case-insensitive. + + :param s: string + :param keywords: a tuple containing keywords that should all be included + :return: True if all strings in keywords are contained in s, False otherwise + """ + + for kw in keywords: + if kw.lower().strip() not in s.lower().strip(): + return False + + return True + + +def filter_match_all(elements, keywords): + """ + Returns the elements for which all keywords are contained. + + :param elements: a list of strings to filter + :param keywords: a tuple containing keywords that should all be included + :return: matching matching elements + """ + matching = [] + + for elem in elements: + if all(keyword in elem for keyword in keywords): + matching.append(elem) + + return matching diff --git a/jekyllutils/managers.py b/jekyllutils/managers.py index b812aeb..b614656 100644 --- a/jekyllutils/managers.py +++ b/jekyllutils/managers.py @@ -2,10 +2,10 @@ from subprocess import call import click -from jekyllutils.helpers.configs import get_path_to_posts_dir, get_editor_name +from jekyllutils.helpers.configs import get_posts_path, get_editor_name from jekyllutils.helpers.editors import get_executable_from_name -from jekyllutils.helpers.files import list_files, list_filenames_by_tag, list_unpublished_filenames, resolve_path -from jekyllutils.helpers.sorting import sort_ignoring_brackets +from jekyllutils.files import list_files, list_filenames_by_tag, list_unpublished_filenames, resolve_path +from jekyllutils.helpers.sorting import sort_filenames_ignoring_leading_text @click.command() @@ -14,7 +14,7 @@ def edit_post(keywords): if len(keywords) == 0: raise click.UsageError('Please supply at least one keyword as argument') - path_to_posts_directory = resolve_path(get_path_to_posts_dir()) + path_to_posts_directory = resolve_path(get_posts_path()) post_files = list_files(path_to_posts_directory, keywords) if len(post_files) == 0: @@ -45,7 +45,7 @@ def list_by_tag(tags, reverse): if len(tags) == 0: raise click.UsageError('Please supply at least one tag as argument') - path_to_posts_directory = resolve_path(get_path_to_posts_dir()) + path_to_posts_directory = resolve_path(get_posts_path()) filenames = list_filenames_by_tag(path_to_posts_directory, tags) if len(filenames) == 0: @@ -61,9 +61,9 @@ def list_by_tag(tags, reverse): @click.option('--reverse/--no-reverse', default=True, help="Default is to list files in reverse chronological order, just like git log") @click.option('--include-wip-alerts/--no-include-wip-alerts', default=True, - help="Whether to consider WIP posts as unpublished even though they be published") + help="Whether to consider WIP posts and posts with \"TODOS\" as unpublished even though they are published") def list_unpublished(reverse, include_wip_alerts): - path_to_posts_directory = resolve_path(get_path_to_posts_dir()) + path_to_posts_directory = resolve_path(get_posts_path()) filenames = list_unpublished_filenames(path_to_posts_directory, include_wip_alerts) if len(filenames) == 0: @@ -71,6 +71,8 @@ def list_unpublished(reverse, include_wip_alerts): else: if reverse: - click.echo_via_pager('\n'.join(sort_ignoring_brackets(filenames, reverse=True))) + output = click.echo_via_pager('\n'.join(sort_filenames_ignoring_leading_text(filenames, reverse=True))) else: - click.echo_via_pager('\n'.join(sort_ignoring_brackets(filenames))) + output = click.echo_via_pager('\n'.join(sort_filenames_ignoring_leading_text(filenames))) + + return output diff --git a/setup.py b/setup.py index e79c664..7087a8f 100644 --- a/setup.py +++ b/setup.py @@ -2,15 +2,15 @@ setup( name="jekyllutils", - version='0.4', + version='0.5', py_modules=['generators'], packages=find_packages(), include_package_data=True, install_requires=[ - 'Click', + 'Click==8.0.2', 'python-slugify', - 'appdirs', - 'toml' + 'appdirs==1.4.4', + 'toml==0.10.2' ], entry_points=''' [console_scripts] @@ -19,7 +19,7 @@ jk-new-crypto-overview=jekyllutils.generators:new_post_crypto_asset_overview jk-edit=jekyllutils.managers:edit_post jk-config-set-editor=jekyllutils.configs:set_editor - jk-config-set-posts-path=jekyllutils.configs:set_path_to_posts_dir + jk-config-set-posts-path=jekyllutils.configs:set_posts_path jk-config-dump-configs=jekyllutils.configs:dump_configs jk-config-clear-configs=jekyllutils.configs:clear_configs jk-list-by-tag=jekyllutils.managers:list_by_tag