Module ezcv.cli

The module containing all cli functionality of ezcv including: - Initializing sites - Generating temporary preview - Getting lists of themes and/or copying themes

Functions

init(): Initializes an ezcv site

preview(): Creates a temporary folder of the site's files and then previews it in browser

theme(): Used to get information about the available themes and/or copy a theme folder

main(): The primary entrypoint for the ezcv cli

Examples

Create a new site

from ezcv.cli import init

theme = "freelancer"
name = "John Doe"

init(theme, name)

Preview a site that is in the cwd

from ezcv.cli import preview

preview()

Copy the aerial theme

from ezcv.cli import theme

theme(copy_theme = True, theme = "aerial")
from ezcv.cli import theme

theme(list_themes = True)
Expand source code
"""The module containing all cli functionality of ezcv including:
    - Initializing sites
    - Generating temporary preview
    - Getting lists of themes and/or copying themes

Functions
---------
init():
    Initializes an ezcv site

preview():
    Creates a temporary folder of the site's files and then previews it in browser

theme():
    Used to get information about the available themes and/or copy a theme folder

main():
    The primary entrypoint for the ezcv cli

Examples
--------
#### Create a new site
```
from ezcv.cli import init

theme = "freelancer"
name = "John Doe"

init(theme, name)
```

#### Preview a site that is in the cwd
```
from ezcv.cli import preview

preview()
```

#### Copy the aerial theme
```
from ezcv.cli import theme

theme(copy_theme = True, theme = "aerial")
```

#### Print a list of available themes
```
from ezcv.cli import theme

theme(list_themes = True)
```
"""

# Standard Lib Dependencies
import os                   # Used for path validation
import shutil               # Used for file/folder copying and removal
import tempfile             # Used to generate temporary folders for previews
from sys import argv, exit  # Used to get length of CLI args and exit cleanly

## internal dependencies
from ezcv.core import generate_site, get_site_config
from ezcv.themes import THEMES_FOLDER, get_remote_themes, locate_theme_directory, setup_remote_theme

# Third party dependencies
from colored import fg     # Used to highlight output with colors
from docopt import docopt  # Used to complete argument parsing for the cli

usage = """Usage:
    ezcv [-h] [-v] [-p]
    ezcv init [<name>] [<theme>]
    ezcv build [-d OUTPUT_DIR] [-p]
    ezcv theme [-l] [-c] [-s SECTION_NAME] [<theme>]


Options:
-h, --help            show this help message and exit
-v, --version         show program's version number and exit
-l, --list            list the possible themes
-c, --copy            copy the provided theme, or defined site theme
-p, --preview         preview the current state of the site
-d OUTPUT_DIR, --dir OUTPUT_DIR The folder name to export the site to
-s SECTION_NAME, --section SECTION_NAME The section name to initialize
"""

def init(theme="dimension", name="John Doe"):
    """Initializes an ezcv site

    Parameters
    ----------
    theme : (str, optional)
        The theme to use in the config, by default "dimension"

    name : (str, optional)
        The name to use in the config, by default "John Doe"
    """
    print(f"Generating site at {os.path.abspath(name)}")

    shutil.copytree(os.path.join(os.path.dirname(__file__), "example_site"), os.path.abspath(name))

    # Generate initial config.yml file
    with open(os.path.join(name, "config.yml"), "w+") as config_file:
        config_file.write(f"# See https://ezcv.readthedocs.io for documentation\nname: {name}\ntheme: {theme}\nresume: false")

    if theme != "dimension":
        # Check if theme is remote theme, and download it if it is
        
        remote_themes = get_remote_themes()
        if remote_themes.get(theme, False):
            original_directory = os.path.abspath(os.getcwd()) # Store CWD
            os.chdir(os.path.abspath(name))                   # Go into new site folder
            setup_remote_theme(theme, remote_themes[theme])   # Download theme 
            os.chdir(original_directory)                      # Navigate back to original cwd

    print(f"Site generated and is available at {os.path.abspath(name)}")


def preview():
    """Creates a temporary folder of the site's files and then previews it in browser"""
    with tempfile.TemporaryDirectory() as temp_dir:
        generate_site(temp_dir, preview=True)
        try:
            input("Press enter when done previewing")
        except EOFError:
            print(f"\nKeyboard interupt detected, ending preview and removing {temp_dir}")
            return
        except KeyboardInterrupt:
            print(f"\nKeyboard interupt detected, ending preview and removing {temp_dir}")
            return

        print(f"Ending preview and removing {temp_dir}")


def theme(list_themes: bool = False, copy_theme:bool = False, theme:str = ""):
    """Used to get information about the available themes and/or copy a theme folder

    Parameters
    ----------
    list_themes : bool, optional
        Whether or not to list the available themes, by default False

    copy_theme : bool, optional
        Whether or not to copy provided theme, by default False

    theme : str, optional
        The theme to copy, by default "" (which will copy the dimension theme)
    """
    if not theme:
        theme = "dimension"

    if copy_theme:
        if os.path.exists(os.path.join(THEMES_FOLDER, theme)): # If the theme exists in the themes folder
            try: # Try to copy the theme to ./<theme>
                shutil.copytree(os.path.join(THEMES_FOLDER, theme), theme)
            except FileExistsError: # If a folder exists at ./<theme> remove and then re-copy
                shutil.rmtree(theme)
                shutil.copytree(os.path.join(THEMES_FOLDER, theme), theme)
            print(f"Copied {os.path.join(THEMES_FOLDER, theme)} to .{os.sep}{theme}")
        else: # Theme could not be found
            print(f"Theme {theme} not found and was unable to be copied")

    if list_themes:
        # Get local themes
        print(f"\nAvailable local themes\n{'='*22}")
        for theme in os.listdir(THEMES_FOLDER):
            if not theme == "remotes.yml":
                print(f"  - {theme}")

        # Get remote themes
        print(f"\nAvailable remote themes\n{'='*23}")
        import yaml
        with open(os.path.join(THEMES_FOLDER, "remotes.yml")) as remote_themes:
            for theme in yaml.safe_load(remote_themes):
                print(f"  - {theme}")
        print() # empty newline after list



def new_section(section_name:str) -> bool:
    """Creates a new section (content folder, and section template in the theme)

    Parameters
    ----------
    section_name : str
        The name you want to give to the section, used to generate folder and template filename

    Notes
    -----
    - Needs to be run from the main folder of a site, or at most one folder deep (i.e. the content or Images folder)

    Returns
    -------
    bool
        True if the creation was successful and False if it failed early
    """    
    if os.path.exists("config.yml"):
        config_path = "config.yml"
    elif os.path.exists("../config.yml") :
        config_path = "../config.yml"
    else:
        print(f"{fg(1)}You are not in a project root folder, please run from folder with config.yml{fg(15)}\n")
        return False

    config = get_site_config(config_file_path=config_path)
    if not config["theme"]: # Set to default theme if no theme is set
        config["theme"] = "dimension"


    theme_path = locate_theme_directory(config["theme"], {"config": config})
    if os.path.exists(config["theme"]): # Theme is at cwd i.e. ./aerial
        theme_path = config["theme"]
    elif os.path.exists(os.path.join("..", config["theme"])): # Theme is one level up i.e. ../aerial
        theme_path = os.path.join("..", config["theme"])
    elif os.path.exists(os.path.join(THEMES_FOLDER, config["theme"])): # Theme is in package theme folder i.e. THEME_FOLDER/aerial
        theme_path = os.path.join(THEMES_FOLDER, config["theme"])
    else:
        print(f"{fg(1)}Could not find theme at any of the possible locations\n\t{config['theme']}\n\t{os.path.join('..', config['theme'])}\n\t{os.path.join(THEMES_FOLDER, config['theme'])} {fg(15)}\n")
        return False

    if os.path.exists("content"):
        content_path = "content"
    elif os.getcwd().split(os.sep)[-1] == "content": # Inside the current content folder
        content_path = os.getcwd()
    else:
        return False

    # The content for the template in the generated section
    default_section_page_templte = f"""\n{{% for page in {section_name} %}} <!--Lets you iterate through each page -->
            {{{{ page[0] }}}} <!--Metadata access -->
            {{{{ page[1] | safe }}}} <!--content access -->
{{% endfor %}}
\n"""

    # Begin creating content folder and theme file
    if not os.path.exists(os.path.join(content_path, section_name)): # If the content folder doesn't already exist
        if not os.path.exists(os.path.join(theme_path, "sections", f"{section_name}.jinja")) and not os.path.exists(os.path.join(theme_path, "sections", f"{section_name}.html")): # If jinja theme doesn't already exist
            os.mkdir(os.path.join(content_path, section_name))
            with open(os.path.join(theme_path, "sections", f"{section_name}.jinja"), 'w+') as section_file:
                section_file.write(default_section_page_templte)
        else: # Theme file already existed
            print(f"{fg(1)}Could not create path, path already exists at either: \n\t{os.path.join(theme_path, 'sections', f'{section_name}.jinja')}\n\tor\n\t{os.path.join(theme_path, 'sections', f'{section_name}.jinja')}\n{fg(15)}")
            return False
    else: # Content folder already existed
        print(f"{fg(1)}Could not create path, path already exists at {os.path.join(content_path, section_name)}\n{fg(15)}")
        return False

    print(f"Section successfully created\n\nTheme file created at:\n\t{os.path.join(theme_path, 'sections', f'{section_name}.jinja')}\nContent folder created at:\n\t{os.path.join(content_path, section_name)}")
    return True


def main():
    """The primary entrypoint for the ezcv cli"""
    args = docopt(usage, version="0.2.1")

    if len(argv) == 1: # Print usage if no arguments are given
        print("\n", usage)
        exit()

    if args["--preview"] and not args["build"]:
        preview()

    elif args["init"]:
        if args["<theme>"] and args["<name>"]: # Both a theme and name are specified
            init(args["<theme>"], args["<name>"])
        elif args["<name>"]: # Only a name is specified
            init(name = args["<name>"])
        else: # No values are specified
            init()

    elif args["build"]:
        if not args["--dir"]:
            generate_site()
        else:
            generate_site(args["--dir"])

        if args["--preview"]: # If preview flag is specified
            preview()

        if not args["--dir"] and not args["--preview"]: # No flags provided
            print("\n", usage)
            exit()

    elif args["theme"]:
        if args["--section"]:
            created = new_section(args["--section"])
            if not created:
                print(f"{fg(1)}Failed to create section {args['--section']} {fg(15)}")
        elif args["<theme>"]:
            theme(args["--list"], args["--copy"], args["<theme>"])
        elif args["--copy"]: # If copy is flagged, but no theme is provided
            if os.path.exists("config.yml"):
                theme(args["--list"], args["--copy"], get_site_config()["theme"])
            else: # If no theme, or config.yml file is present
                theme(args["--list"], args["--copy"], "freelancer")
        elif args["--list"]:
            theme(args["--list"])
        else: # If theme argument is called with no other flags
            print("\n", usage)
            exit()

    elif args["--preview"]: # If preview flag is specified with no other flags
        preview()

    else: # No top level argument is provided
        print("\n", usage)
        exit()

if __name__ == "__main__": # For testing
    main()

Functions

def init(theme='dimension', name='John Doe')

Initializes an ezcv site

Parameters

theme : (str, optional)
The theme to use in the config, by default "dimension"
name : (str, optional)
The name to use in the config, by default "John Doe"
Expand source code
def init(theme="dimension", name="John Doe"):
    """Initializes an ezcv site

    Parameters
    ----------
    theme : (str, optional)
        The theme to use in the config, by default "dimension"

    name : (str, optional)
        The name to use in the config, by default "John Doe"
    """
    print(f"Generating site at {os.path.abspath(name)}")

    shutil.copytree(os.path.join(os.path.dirname(__file__), "example_site"), os.path.abspath(name))

    # Generate initial config.yml file
    with open(os.path.join(name, "config.yml"), "w+") as config_file:
        config_file.write(f"# See https://ezcv.readthedocs.io for documentation\nname: {name}\ntheme: {theme}\nresume: false")

    if theme != "dimension":
        # Check if theme is remote theme, and download it if it is
        
        remote_themes = get_remote_themes()
        if remote_themes.get(theme, False):
            original_directory = os.path.abspath(os.getcwd()) # Store CWD
            os.chdir(os.path.abspath(name))                   # Go into new site folder
            setup_remote_theme(theme, remote_themes[theme])   # Download theme 
            os.chdir(original_directory)                      # Navigate back to original cwd

    print(f"Site generated and is available at {os.path.abspath(name)}")
def main()

The primary entrypoint for the ezcv cli

Expand source code
def main():
    """The primary entrypoint for the ezcv cli"""
    args = docopt(usage, version="0.2.1")

    if len(argv) == 1: # Print usage if no arguments are given
        print("\n", usage)
        exit()

    if args["--preview"] and not args["build"]:
        preview()

    elif args["init"]:
        if args["<theme>"] and args["<name>"]: # Both a theme and name are specified
            init(args["<theme>"], args["<name>"])
        elif args["<name>"]: # Only a name is specified
            init(name = args["<name>"])
        else: # No values are specified
            init()

    elif args["build"]:
        if not args["--dir"]:
            generate_site()
        else:
            generate_site(args["--dir"])

        if args["--preview"]: # If preview flag is specified
            preview()

        if not args["--dir"] and not args["--preview"]: # No flags provided
            print("\n", usage)
            exit()

    elif args["theme"]:
        if args["--section"]:
            created = new_section(args["--section"])
            if not created:
                print(f"{fg(1)}Failed to create section {args['--section']} {fg(15)}")
        elif args["<theme>"]:
            theme(args["--list"], args["--copy"], args["<theme>"])
        elif args["--copy"]: # If copy is flagged, but no theme is provided
            if os.path.exists("config.yml"):
                theme(args["--list"], args["--copy"], get_site_config()["theme"])
            else: # If no theme, or config.yml file is present
                theme(args["--list"], args["--copy"], "freelancer")
        elif args["--list"]:
            theme(args["--list"])
        else: # If theme argument is called with no other flags
            print("\n", usage)
            exit()

    elif args["--preview"]: # If preview flag is specified with no other flags
        preview()

    else: # No top level argument is provided
        print("\n", usage)
        exit()
def new_section(section_name: str) ‑> bool

Creates a new section (content folder, and section template in the theme)

Parameters

section_name : str
The name you want to give to the section, used to generate folder and template filename

Notes

  • Needs to be run from the main folder of a site, or at most one folder deep (i.e. the content or Images folder)

Returns

bool
True if the creation was successful and False if it failed early
Expand source code
def new_section(section_name:str) -> bool:
    """Creates a new section (content folder, and section template in the theme)

    Parameters
    ----------
    section_name : str
        The name you want to give to the section, used to generate folder and template filename

    Notes
    -----
    - Needs to be run from the main folder of a site, or at most one folder deep (i.e. the content or Images folder)

    Returns
    -------
    bool
        True if the creation was successful and False if it failed early
    """    
    if os.path.exists("config.yml"):
        config_path = "config.yml"
    elif os.path.exists("../config.yml") :
        config_path = "../config.yml"
    else:
        print(f"{fg(1)}You are not in a project root folder, please run from folder with config.yml{fg(15)}\n")
        return False

    config = get_site_config(config_file_path=config_path)
    if not config["theme"]: # Set to default theme if no theme is set
        config["theme"] = "dimension"


    theme_path = locate_theme_directory(config["theme"], {"config": config})
    if os.path.exists(config["theme"]): # Theme is at cwd i.e. ./aerial
        theme_path = config["theme"]
    elif os.path.exists(os.path.join("..", config["theme"])): # Theme is one level up i.e. ../aerial
        theme_path = os.path.join("..", config["theme"])
    elif os.path.exists(os.path.join(THEMES_FOLDER, config["theme"])): # Theme is in package theme folder i.e. THEME_FOLDER/aerial
        theme_path = os.path.join(THEMES_FOLDER, config["theme"])
    else:
        print(f"{fg(1)}Could not find theme at any of the possible locations\n\t{config['theme']}\n\t{os.path.join('..', config['theme'])}\n\t{os.path.join(THEMES_FOLDER, config['theme'])} {fg(15)}\n")
        return False

    if os.path.exists("content"):
        content_path = "content"
    elif os.getcwd().split(os.sep)[-1] == "content": # Inside the current content folder
        content_path = os.getcwd()
    else:
        return False

    # The content for the template in the generated section
    default_section_page_templte = f"""\n{{% for page in {section_name} %}} <!--Lets you iterate through each page -->
            {{{{ page[0] }}}} <!--Metadata access -->
            {{{{ page[1] | safe }}}} <!--content access -->
{{% endfor %}}
\n"""

    # Begin creating content folder and theme file
    if not os.path.exists(os.path.join(content_path, section_name)): # If the content folder doesn't already exist
        if not os.path.exists(os.path.join(theme_path, "sections", f"{section_name}.jinja")) and not os.path.exists(os.path.join(theme_path, "sections", f"{section_name}.html")): # If jinja theme doesn't already exist
            os.mkdir(os.path.join(content_path, section_name))
            with open(os.path.join(theme_path, "sections", f"{section_name}.jinja"), 'w+') as section_file:
                section_file.write(default_section_page_templte)
        else: # Theme file already existed
            print(f"{fg(1)}Could not create path, path already exists at either: \n\t{os.path.join(theme_path, 'sections', f'{section_name}.jinja')}\n\tor\n\t{os.path.join(theme_path, 'sections', f'{section_name}.jinja')}\n{fg(15)}")
            return False
    else: # Content folder already existed
        print(f"{fg(1)}Could not create path, path already exists at {os.path.join(content_path, section_name)}\n{fg(15)}")
        return False

    print(f"Section successfully created\n\nTheme file created at:\n\t{os.path.join(theme_path, 'sections', f'{section_name}.jinja')}\nContent folder created at:\n\t{os.path.join(content_path, section_name)}")
    return True
def preview()

Creates a temporary folder of the site's files and then previews it in browser

Expand source code
def preview():
    """Creates a temporary folder of the site's files and then previews it in browser"""
    with tempfile.TemporaryDirectory() as temp_dir:
        generate_site(temp_dir, preview=True)
        try:
            input("Press enter when done previewing")
        except EOFError:
            print(f"\nKeyboard interupt detected, ending preview and removing {temp_dir}")
            return
        except KeyboardInterrupt:
            print(f"\nKeyboard interupt detected, ending preview and removing {temp_dir}")
            return

        print(f"Ending preview and removing {temp_dir}")
def theme(list_themes: bool = False, copy_theme: bool = False, theme: str = '')

Used to get information about the available themes and/or copy a theme folder

Parameters

list_themes : bool, optional
Whether or not to list the available themes, by default False
copy_theme : bool, optional
Whether or not to copy provided theme, by default False
theme : str, optional
The theme to copy, by default "" (which will copy the dimension theme)
Expand source code
def theme(list_themes: bool = False, copy_theme:bool = False, theme:str = ""):
    """Used to get information about the available themes and/or copy a theme folder

    Parameters
    ----------
    list_themes : bool, optional
        Whether or not to list the available themes, by default False

    copy_theme : bool, optional
        Whether or not to copy provided theme, by default False

    theme : str, optional
        The theme to copy, by default "" (which will copy the dimension theme)
    """
    if not theme:
        theme = "dimension"

    if copy_theme:
        if os.path.exists(os.path.join(THEMES_FOLDER, theme)): # If the theme exists in the themes folder
            try: # Try to copy the theme to ./<theme>
                shutil.copytree(os.path.join(THEMES_FOLDER, theme), theme)
            except FileExistsError: # If a folder exists at ./<theme> remove and then re-copy
                shutil.rmtree(theme)
                shutil.copytree(os.path.join(THEMES_FOLDER, theme), theme)
            print(f"Copied {os.path.join(THEMES_FOLDER, theme)} to .{os.sep}{theme}")
        else: # Theme could not be found
            print(f"Theme {theme} not found and was unable to be copied")

    if list_themes:
        # Get local themes
        print(f"\nAvailable local themes\n{'='*22}")
        for theme in os.listdir(THEMES_FOLDER):
            if not theme == "remotes.yml":
                print(f"  - {theme}")

        # Get remote themes
        print(f"\nAvailable remote themes\n{'='*23}")
        import yaml
        with open(os.path.join(THEMES_FOLDER, "remotes.yml")) as remote_themes:
            for theme in yaml.safe_load(remote_themes):
                print(f"  - {theme}")
        print() # empty newline after list