Module ezcv.core

The module containing all primary functionality of ezcv including:

  • Content parsing
  • HTML generation
  • Site exporting

Functions

generate_site(): The primary entrypoint to generating a site

get_site_config() -> defaultdict: Gets the site config from provided file path and returns defaultdict of values

Module Variables

SECTIONS_LIST (list[str]): The list of the first party supported sections

Examples

Generating a site using all settings defined in "config.yml"

from ezcv.core import generate_site

generate_site()

Generating a site overriding the theme in "config.yml", output directory and specifying to show a preview of the site

from ezcv.core import generate_site

generate_site(output_folder="my_site", theme = "aerial", preview = True)
Expand source code
"""The module containing all primary functionality of ezcv including:

- Content parsing
- HTML generation
- Site exporting

Functions
---------
generate_site():
    The primary entrypoint to generating a site

get_site_config() -> defaultdict:
    Gets the site config from provided file path and returns defaultdict of values


Module Variables
----------------
SECTIONS_LIST (list[str]):
    The list of the first party supported sections

Examples
--------
#### Generating a site using all settings defined in "config.yml"
```
from ezcv.core import generate_site

generate_site()
```

#### Generating a site overriding the theme in "config.yml", output directory and specifying to show a preview of the site
```
from ezcv.core import generate_site

generate_site(output_folder="my_site", theme = "aerial", preview = True)
```
"""

# Standard Lib Dependencies
import os                           # Used for path validation
import shutil                       # Used for file/folder copying and removal
import webbrowser                   # Used to automatically open the default system browser
from collections import defaultdict # Used to instatiate dictionaries with default arguments on unspecified keys
from typing import Union            # Used to add additional typehints to help with documentation and usage on functions

# Internal Dependencies
from ezcv.themes import *
from ezcv.content import *
from ezcv.filters import inject_filters

# Third Party Dependencies
import yaml                         # Used for config file parsing
import jinja2                       # used as middlewear for generating templates
from tqdm import tqdm               # Used to generate progress bars during iteration

# The global list of currently supported first party sections
SECTIONS_LIST = ["projects", "education", "work_experience", "volunteering_experience", "gallery"]

def get_site_config(config_file_path:str = "config.yml", remotes_file_path:str = os.path.join(THEMES_FOLDER, "remotes.yml")) -> defaultdict:
    """Gets the site config from provided file path and returns defaultdict of values

    Parameters
    ----------
    config_file_path : str, optional
        The path to the config file, by default "config.yml"

    Returns
    -------
    defaultdict:
        The configuration, if any key is not present it defaults to False
    """
    if not os.path.exists(config_file_path):
        raise FileNotFoundError(f"Config file at {config_file_path} was not found")

    with open(config_file_path, "r") as config_file:
        config = yaml.safe_load(config_file)

    config["remotes"] = get_remote_themes()

    # Convert config dict to defaultdict so that all empty values are False instead of giving KeyNotFoundError
    default_dict_config = defaultdict(lambda: False, config)

    return default_dict_config


def _render_section(theme_folder:str, section_name:str, site_context:dict) -> str:
    """Renders the particular section provided using the environment provided

    Parameters
    ----------
    theme_folder : (str)
        The absolute path to the theme

    section_name : (str)
        The name of the section to render i.e. projects, education, work_experience etc.

    site_context: (dict)
        The dictionary containing the site's context

    Returns
    -------
    str:
        The rendered template of the section
    """
    try:
        contents = site_context["sections"][section_name]
    except KeyError:
        print(f"Could not find content for section '{section_name}', skipping")
        return ""

    # Initialize jinja loaders
    theme_loader = jinja2.FileSystemLoader(theme_folder)
    environment = jinja2.Environment(loader=theme_loader, autoescape=True, trim_blocks=True) # Grab all files in theme_folder

    inject_filters(environment) # Add in custom filters

    # If a section template exists set it to the path, else False i.e. if <theme folder>/sections/<section name>.jinja exists set it to that
    section_template_file = f"sections/{section_name}.jinja"
    if section_template_file:
        if len(contents) > 0: # If there is any markdown content
            try:
                theme = environment.get_template(section_template_file)
            except jinja2.TemplateNotFound: # If current section is not supported
                print(f"Section {section_name} template is not available")
                return ""
            return theme.render({section_name:contents, "config": site_context["config"]})
        else:
            return ""
    else:
        print(f"Section {section_name} template is not available")
        return ""


def _render_page(theme_folder:str, page:str, site_context:dict) -> str:
    """Renders the page provided from the specified theme

    Parameters
    ----------
    theme_folder : (str)
        The absolute path to the theme

    page : (str)
        The filename inside the theme folder to render i.e. 'index.jinja'

    site_context : (dict)
        A dictionary containing the config values, and all sections html

    Returns
    -------
    str:
        The rendered html of the page
    """
    # Initialize jinja loaders
    theme_loader = jinja2.FileSystemLoader(theme_folder)
    environment = jinja2.Environment(loader=theme_loader, autoescape=True, trim_blocks=True) # Grab all files in theme_folder

    inject_filters(environment) # Add in custom filters

    # Render template and return contents
    theme = environment.get_template(page)
    return theme.render(site_context)


def _export(site_context:dict, theme_folder:str, output_folder:str = "site", pages:list=["index.jinja"]):
    """Generates all the site html from pages specified and outputs them to the output folder

    Parameters
    ----------
    site_context : (dict)
        The site context containing all sections html and dict + config dict

    theme_folder : (str)
        The absolute path to the folder for the theme to use

    output_folder : (str, optional)
        The folder to output the HTML files to, by default "site"

    pages : (list, optional)
        The list of pages to use, by default ["index.jinja"]

    Raises
    ------
    FileNotFoundError
        If the provided theme folder does not exist
    """
    if not os.path.exists(theme_folder): # Error out if provided theme folder does not exist
        raise FileNotFoundError(f"The provided theme folder does not exist: {theme_folder}")

    # Copy source files
    try:
        shutil.copytree(theme_folder, output_folder, ignore=shutil.ignore_patterns("*.jinja"))

    except FileExistsError:
        shutil.rmtree(output_folder)
        shutil.copytree(theme_folder, output_folder, ignore=shutil.ignore_patterns("*.jinja"))
    
    # Copy images
    output_image_dir = os.path.join(output_folder, "images")
    if os.path.exists("images"):
        if not os.path.exists(output_image_dir): # Create output_folder/images if it's not present
            os.mkdir(output_image_dir)
        for file in os.listdir("images"): # Copy file from source images folder to output image directory
            # TODO: Add catch for if image already exists
            shutil.copyfile(os.path.join("images", file), os.path.join(output_image_dir, file))

    # Copy Gallery images
    output_fallery_image_dir = os.path.join(output_folder, "images", "gallery")
    if os.path.exists(os.path.join("content", "gallery")):
        if not os.path.exists(output_fallery_image_dir): # Create output_folder/images/gallery if it's not present
            os.mkdir(output_fallery_image_dir)
        for file in os.listdir(os.path.join("content", "gallery")): # Copy file from source images folder to output image directory
            # TODO: Add catch for if image already exists
            shutil.copyfile(os.path.join("content", "gallery", file), os.path.join(output_fallery_image_dir, file))

    # Iterate through top level pages and write to the output folder
    print("\nGenerating output html from theme")
    pages_iterator = tqdm(pages)
    pages_iterator.set_description_str("Generating top level pages")
    for page in pages_iterator:  # Write new pages
        try:
            html = _render_page(theme_folder, page, site_context)
        except jinja2.UndefinedError as e:
            print(e)
            raise ValueError("A required configuration value is missing")
        if page.endswith(".jinja"):
            page = f"{page[:-6:]}.html"
        pages_iterator.set_description_str(f"Writing {page}")
        pages_iterator.refresh()
        with open(f"{output_folder}{os.sep}{page}", "w+") as outfile:
            outfile.write(html)

#TODO: add extra_filters to generate_site
def generate_site(output_folder:str="site", theme:str = "dimension", sections: list = [], config_file_path="config.yml", preview:bool = False):
    """The primary entrypoint to generating a site

    Parameters
    ----------
    output_folder : (str, optional)
        The folder to output the site files to, by default "site"

    theme : (str, optional)
        The name of the theme to use, by default "dimension"

    sections : (list[str], optional)
        A list of the sections to include in export, by default []

    config_file_path : (str, optional)
        The path to the site's config yaml file, by default "config.yml"

    preview : (bool, optional)
        If true then the index.html will be auto opened in the system webbrowser, by default False

    Notes
    -----
    - theme options are: 
        - aerial; https://html5up.net/aerial
        - base; included theme that can be used for debugging
        - creative; https://startbootstrap.com/theme/creative
        - dimension; https://html5up.net/dimension
        - ethereal; https://html5up.net/ethereal
        - freelancer; https://startbootstrap.com/theme/freelancer
        - identity; https://html5up.net/identity
        - read_only; https://html5up.net/read-only
        - solid_state; https://html5up.net/solid-state
        - strata; https://html5up.net/strata
    - Available sections are:
        - Projects (projects)
        - Education (education)
        - Volunteering experience (volunteering_experience)
        - Work Experience (work_experience)
    - If sections is an empty list then the theme's section directory will be searched for themes

    Raises
    ------
    FileNotFoundError
        If the provided theme folder does not exist

    Examples
    --------
    Generating a site with all default settings
    ```
    from ezprez.core import generate_site

    generate_site()
    ```

    Generating a site that outputs to /resume and only generating the projects section
    ```
    from ezprez.core import generate_site

    generate_site(output_folder="my_site", sections=["projects"])
    ```
    """
    print(f"Exporting site to {output_folder}")
    pages = [] # Filled with a list of all the pages to render

    # The data passed to render all pages
    site_context:dict[str, Union[list, defaultdict, dict]] = {"config": get_site_config(config_file_path)}

    if site_context["config"]["ignore_exif_data"]:
        Image.ignore_exif_data = True

    # If no theme argument, and a theme is defined in the site config file
    if site_context["config"]["theme"] and theme == "dimension": 
        theme = site_context["config"]["theme"]

    # Find theme directory based on name, or download it if it's a remote theme
    theme_folder = locate_theme_directory(theme, site_context)

    # Initialize sections key in site context to empty dict
    site_context["sections"] = {}

    # Get a list of the section names, and section theme directories
    sections = get_theme_section_directories(theme_folder, sections)
    sections_content_dirs = get_content_directories()

    # Go through all section content files to get content (i.e. ./sections/education/*.md) 
    for section in sections_content_dirs: 
        # Get content to store in site_context["sections"][section]
        site_context["sections"][section.split(os.sep)[-1]] = get_section_content(section, site_context["config"]["examples"])

    # Get a list of all the top level pages in the theme folder and add them to the pages list
    for top_level_file in os.listdir(theme_folder):
        if top_level_file == "resume.jinja" and not site_context["config"]["resume"]: # Ignore resume.jinja if resume config var is False
            continue

        if top_level_file.endswith(".jinja") or top_level_file.endswith(".html"):
            pages.append(top_level_file)
    
    # Go through each section, render the html and add it to the site context
    print("\nGenerating content from sections")
    sections_iterator = tqdm(sections)
    sections_iterator.set_description_str("Writing section content")
    for section in sections_iterator: 
        html = _render_section(theme_folder, section, site_context)
        site_context[f"{section}_html"] = html

    # Generate and export all the pages of a site
    _export(site_context, theme_folder, output_folder, pages)

    if preview:
        browser_types = ["chromium-browser", "chromium", "chrome", "google-chrome", "firefox", "mozilla", "opera", "safari"] # A list of all the types of browsers to try
        for browser_name in browser_types:
            try:
                webbrowser.get(browser_name) # Search for browser
                break # Browser has been found
            except webbrowser.Error:
                continue
        webbrowser.open(f"file:///{os.path.abspath(output_folder)}/index.html")

Functions

def generate_site(output_folder: str = 'site', theme: str = 'dimension', sections: list = [], config_file_path='config.yml', preview: bool = False)

The primary entrypoint to generating a site

Parameters

output_folder : (str, optional)
The folder to output the site files to, by default "site"
theme : (str, optional)
The name of the theme to use, by default "dimension"
sections : (list[str], optional)
A list of the sections to include in export, by default []
config_file_path : (str, optional)
The path to the site's config yaml file, by default "config.yml"
preview : (bool, optional)
If true then the index.html will be auto opened in the system webbrowser, by default False

Notes

Raises

FileNotFoundError
If the provided theme folder does not exist

Examples

Generating a site with all default settings

from ezprez.core import generate_site

generate_site()

Generating a site that outputs to /resume and only generating the projects section

from ezprez.core import generate_site

generate_site(output_folder="my_site", sections=["projects"])
Expand source code
def generate_site(output_folder:str="site", theme:str = "dimension", sections: list = [], config_file_path="config.yml", preview:bool = False):
    """The primary entrypoint to generating a site

    Parameters
    ----------
    output_folder : (str, optional)
        The folder to output the site files to, by default "site"

    theme : (str, optional)
        The name of the theme to use, by default "dimension"

    sections : (list[str], optional)
        A list of the sections to include in export, by default []

    config_file_path : (str, optional)
        The path to the site's config yaml file, by default "config.yml"

    preview : (bool, optional)
        If true then the index.html will be auto opened in the system webbrowser, by default False

    Notes
    -----
    - theme options are: 
        - aerial; https://html5up.net/aerial
        - base; included theme that can be used for debugging
        - creative; https://startbootstrap.com/theme/creative
        - dimension; https://html5up.net/dimension
        - ethereal; https://html5up.net/ethereal
        - freelancer; https://startbootstrap.com/theme/freelancer
        - identity; https://html5up.net/identity
        - read_only; https://html5up.net/read-only
        - solid_state; https://html5up.net/solid-state
        - strata; https://html5up.net/strata
    - Available sections are:
        - Projects (projects)
        - Education (education)
        - Volunteering experience (volunteering_experience)
        - Work Experience (work_experience)
    - If sections is an empty list then the theme's section directory will be searched for themes

    Raises
    ------
    FileNotFoundError
        If the provided theme folder does not exist

    Examples
    --------
    Generating a site with all default settings
    ```
    from ezprez.core import generate_site

    generate_site()
    ```

    Generating a site that outputs to /resume and only generating the projects section
    ```
    from ezprez.core import generate_site

    generate_site(output_folder="my_site", sections=["projects"])
    ```
    """
    print(f"Exporting site to {output_folder}")
    pages = [] # Filled with a list of all the pages to render

    # The data passed to render all pages
    site_context:dict[str, Union[list, defaultdict, dict]] = {"config": get_site_config(config_file_path)}

    if site_context["config"]["ignore_exif_data"]:
        Image.ignore_exif_data = True

    # If no theme argument, and a theme is defined in the site config file
    if site_context["config"]["theme"] and theme == "dimension": 
        theme = site_context["config"]["theme"]

    # Find theme directory based on name, or download it if it's a remote theme
    theme_folder = locate_theme_directory(theme, site_context)

    # Initialize sections key in site context to empty dict
    site_context["sections"] = {}

    # Get a list of the section names, and section theme directories
    sections = get_theme_section_directories(theme_folder, sections)
    sections_content_dirs = get_content_directories()

    # Go through all section content files to get content (i.e. ./sections/education/*.md) 
    for section in sections_content_dirs: 
        # Get content to store in site_context["sections"][section]
        site_context["sections"][section.split(os.sep)[-1]] = get_section_content(section, site_context["config"]["examples"])

    # Get a list of all the top level pages in the theme folder and add them to the pages list
    for top_level_file in os.listdir(theme_folder):
        if top_level_file == "resume.jinja" and not site_context["config"]["resume"]: # Ignore resume.jinja if resume config var is False
            continue

        if top_level_file.endswith(".jinja") or top_level_file.endswith(".html"):
            pages.append(top_level_file)
    
    # Go through each section, render the html and add it to the site context
    print("\nGenerating content from sections")
    sections_iterator = tqdm(sections)
    sections_iterator.set_description_str("Writing section content")
    for section in sections_iterator: 
        html = _render_section(theme_folder, section, site_context)
        site_context[f"{section}_html"] = html

    # Generate and export all the pages of a site
    _export(site_context, theme_folder, output_folder, pages)

    if preview:
        browser_types = ["chromium-browser", "chromium", "chrome", "google-chrome", "firefox", "mozilla", "opera", "safari"] # A list of all the types of browsers to try
        for browser_name in browser_types:
            try:
                webbrowser.get(browser_name) # Search for browser
                break # Browser has been found
            except webbrowser.Error:
                continue
        webbrowser.open(f"file:///{os.path.abspath(output_folder)}/index.html")
def get_site_config(config_file_path: str = 'config.yml', remotes_file_path: str = '/opt/hostedtoolcache/Python/3.9.6/x64/lib/python3.9/site-packages/ezcv/themes/remotes.yml') ‑> collections.defaultdict

Gets the site config from provided file path and returns defaultdict of values

Parameters

config_file_path : str, optional
The path to the config file, by default "config.yml"

Returns

defaultdict:
The configuration, if any key is not present it defaults to False
Expand source code
def get_site_config(config_file_path:str = "config.yml", remotes_file_path:str = os.path.join(THEMES_FOLDER, "remotes.yml")) -> defaultdict:
    """Gets the site config from provided file path and returns defaultdict of values

    Parameters
    ----------
    config_file_path : str, optional
        The path to the config file, by default "config.yml"

    Returns
    -------
    defaultdict:
        The configuration, if any key is not present it defaults to False
    """
    if not os.path.exists(config_file_path):
        raise FileNotFoundError(f"Config file at {config_file_path} was not found")

    with open(config_file_path, "r") as config_file:
        config = yaml.safe_load(config_file)

    config["remotes"] = get_remote_themes()

    # Convert config dict to defaultdict so that all empty values are False instead of giving KeyNotFoundError
    default_dict_config = defaultdict(lambda: False, config)

    return default_dict_config