Commit bba6db12 authored by berumuron's avatar berumuron
Browse files

Move functions and classes in a package

parent 88902412
......@@ -2,30 +2,23 @@
import os
import sys
import shutil
import distutils.dir_util
import locale
import yaml
import datetime
import uuid
import re
from enum import Enum
import markdown
import pytz
import boopsy
import boopfeed
import boop.boopsy as boopsy
import boop.boopfeed as boopfeed
import boop.environment as environment
import boop.configuration as config
import boop.utils as utils
from boop.page import Page
locale.setlocale(locale.LC_ALL, "")
class Environment(Enum):
DEVELOPMENT = 1
PRODUCTION = 2
class ProgramError(Exception):
"""Exception raised during the program execution.
......@@ -41,286 +34,14 @@ class ProgramError(Exception):
pass
class Article:
def __init__(self, slug, article_content, configuration={}, serie=None):
# We convert the file's content into HTML
md = markdown.Markdown(extensions=["meta", "fenced_code", "attr_list"])
content = md.convert(article_content)
# We get the local variables from the Markdown file (metadata)
# which will be accessible in the article template
self.meta = {}
self.meta.update(configuration)
self.meta["ARTICLE_SLUG"] = slug
self.meta["ARTICLE_TEMPLATE"] = "article"
for key, values in md.Meta.items():
article_key = f"ARTICLE_{key.upper()}"
# The Markdown's meta extension extract all the values in arrays, even
# if there is only one value.
if len(values) > 1:
self.meta[article_key] = values
else:
self.meta[article_key] = values[0]
self.meta["ARTICLE_CONTENT"] = content
self.meta["ARTICLE_SERIE"] = serie
# And we initialize a template for the article
article_template_filepath = os.path.join(
"templates", f"{self.meta['ARTICLE_TEMPLATE']}.html"
)
self.template = boopsy.Template(article_template_filepath)
def render(self):
return self.template.render(self.meta)
def slug(self):
return self.meta["ARTICLE_SLUG"]
def title(self):
return self.meta.get("ARTICLE_TITLE", "An article")
def author(self):
if "ARTICLE_AUTHOR" in self.meta:
return self.meta["ARTICLE_AUTHOR"]
elif "SITE_AUTHOR" in self.meta:
return self.meta["SITE_AUTHOR"]
else:
return None
def url(self):
site_url = self.meta.get("SITE_URL", "")
return f"{site_url}{self.slug()}.html"
def uuid(self):
return str(uuid.uuid5(uuid.NAMESPACE_URL, self.url()))
def content(self):
return self.meta["ARTICLE_CONTENT"]
def timezone(self):
site_timezone = self.meta.get("SITE_TIMEZONE", "UTC")
return pytz.timezone(site_timezone)
def date(self):
if "ARTICLE_DATE" not in self.meta:
return datetime.datetime.now(tz=self.timezone())
article_date = datetime.datetime.strptime(
self.meta["ARTICLE_DATE"], "%Y-%m-%d %H:%M"
)
return article_date.astimezone(tz=self.timezone())
def update(self):
if "ARTICLE_UPDATE" not in self.meta:
return None
article_update = datetime.datetime.strptime(
self.meta["ARTICLE_UPDATE"], "%Y-%m-%d %H:%M"
)
return article_update.astimezone(tz=self.timezone())
def serie(self):
return self.meta["ARTICLE_SERIE"]
class Page:
def __init__(self, slug, page_content, configuration={}):
# We initialize the page meta variables which will be accessible in the
# template
self.meta = {}
self.meta.update(configuration)
self.meta["PAGE_SLUG"] = slug
self.meta["PAGE_TEMPLATE"] = "page"
yaml_conf, page_content = extract_yaml_configuration(page_content)
for key, value in yaml_conf.items():
article_key = f"PAGE_{key.upper()}"
self.meta[article_key] = value
self.meta["PAGE_CONTENT"] = page_content
# And we initialize a template for the page
page_template_filepath = os.path.join(
"templates", f"{self.meta['PAGE_TEMPLATE']}.html"
)
self.template = None
if os.path.exists(page_template_filepath):
self.template = boopsy.Template(page_template_filepath)
def render(self):
if self.template is None:
return self.content()
else:
return self.template.render(self.meta)
def slug(self):
return self.meta["PAGE_SLUG"]
def title(self):
return self.meta.get("PAGE_TITLE", "A page")
def url(self):
site_url = self.meta.get("SITE_URL", "")
return f"{site_url}{self.slug()}.html"
def uuid(self):
return str(uuid.uuid5(uuid.NAMESPACE_URL, self.url()))
def content(self):
return self.meta["PAGE_CONTENT"]
YAML_FRONT_MATTER_REGEX = re.compile("(---\n.*\n)(---\n)(.*)", re.M | re.S)
def extract_yaml_configuration(content):
match = YAML_FRONT_MATTER_REGEX.match(content)
if match:
yaml_conf = yaml.load(match.group(1))
return yaml_conf, match.group(3)
else:
return {}, content
def add_articles_to_feed(feed, articles):
"""Add article as standard entries to the feed.
"""
for article in articles:
entry = {
"title": article.title(),
"id": f"urn:uuid:{article.uuid()}",
"article_link": article.url(),
"published": article.date(),
"content": article.content(),
}
article_author = article.author()
if article_author:
entry["author"] = article_author
article_update = article.update()
if article_update:
entry["updated"] = article_update
feed.add_entry(entry)
def build_article_from_filepath(filepath, configuration, serie=None):
"""Read a file and initialize an Article from it.
"""
filepath_no_ext, _ = os.path.splitext(filepath)
slug = os.path.basename(filepath_no_ext)
with open(filepath, "r") as article_file:
return Article(
slug, article_file.read(), configuration=configuration, serie=serie
)
def list_dirs(path):
"""List all the directories within a path.
"""
for filename in os.listdir(path):
full_path = os.path.join(path, filename)
if os.path.isdir(full_path):
yield filename
def match_extensions(filename, extensions):
"""Return True if filename extension is part of extensions array.
"""
if len(extensions) > 0:
_, ext = os.path.splitext(filename)
if ext[1:] not in extensions:
return False
return True
def list_files(path, only=[]):
"""List all the files within a path.
"""
for filename in os.listdir(path):
full_path = os.path.join(path, filename)
# We don't want to yield directories
if not os.path.isfile(full_path):
continue
# If file extension is not part of "only" array, we don't yield it. If
# only is empty, it means we accept all extensions.
if not match_extensions(full_path, only):
continue
yield filename
def dir_tree(path, only=[]):
"""List all the tree directory under the given path.
"""
for filename in os.listdir(path):
full_path = os.path.join(path, filename)
if os.path.isdir(full_path):
for sub_filename in dir_tree(full_path, only=only):
yield os.path.join(filename, sub_filename)
elif match_extensions(filename, only):
yield filename
def mkdirs_for_file(filepath):
"""Create parent directories for a given file.
"""
path = os.path.dirname(filepath)
os.makedirs(path, exist_ok=True)
def init_output_folder(output_path):
"""Create the output directory, empty it if it contains files.
"""
if os.path.isdir(output_path):
# it should be a directory...
shutil.rmtree(output_path)
elif os.path.exists(output_path):
# ... but it also can be a simple file if user created it manually!
os.remove(output_path)
os.mkdir(output_path)
def load_configuration(configuration_path, output_path):
configuration = {
"SITE_URL": "filesystem",
"SITE_TITLE": "A website",
"SITE_TIMEZONE": "UTC",
}
if os.path.exists(configuration_path):
with open(configuration_path) as conf_file:
conf_data = yaml.load(conf_file)
for key, value in conf_data.items():
if key == "development":
continue
site_key = f"SITE_{key.upper()}"
configuration[site_key] = value
if environment == Environment.DEVELOPMENT and "development" in conf_data:
for key, value in conf_data["development"].items():
site_key = f"SITE_{key.upper()}"
configuration[site_key] = value
configuration["SITE_UUID"] = str(
uuid.uuid5(uuid.NAMESPACE_URL, configuration["SITE_URL"])
)
if "SITE_AUTHOR" not in configuration:
configuration["SITE_AUTHOR"] = f"{configuration['SITE_TITLE']}'s author"
if configuration["SITE_URL"] == "filesystem":
configuration["SITE_URL"] = f"file://{os.path.abspath(output_path)}/"
return configuration
def main(environment):
# Make sure ./site folder is empty
output_path = os.path.join(os.curdir, "site")
init_output_folder(output_path)
utils.init_output_folder(output_path)
# Load the configuration (it can be overidden by a configuration.yml file)
configuration_path = os.path.join(os.curdir, "configuration.yml")
configuration = load_configuration(configuration_path, output_path)
configuration = config.load(configuration_path, environment, output_path)
# STEP 1: we load all the Articles and Pages
pages = []
......@@ -339,7 +60,7 @@ def main(environment):
# Check that pages directory exists and load Pages from it
pages_path = os.path.join(os.curdir, "pages")
if os.path.isdir(pages_path):
for filename in dir_tree(pages_path, only=["html"]):
for filename in utils.dir_tree(pages_path, only=["html"]):
# We read the page file and initialize a Page from it
slug, _ = os.path.splitext(filename)
page_filepath = os.path.join(pages_path, filename)
......@@ -353,7 +74,7 @@ def main(environment):
# We iterate on the directories of articles dir. If they contain
# "serie.html" file, it means they contain a serie. Otherwise, we list
# articles as usual.
dirs_with_articles = list(list_dirs(articles_path)) + ["."]
dirs_with_articles = list(utils.list_dirs(articles_path)) + ["."]
for dirname in dirs_with_articles:
dirpath = os.path.realpath(os.path.join(articles_path, dirname))
......@@ -368,9 +89,9 @@ def main(environment):
pages.append(serie)
articles_by_series[serie] = []
for filename in list_files(dirpath, only=["md"]):
for filename in utils.list_files(dirpath, only=["md"]):
article_filepath = os.path.join(dirpath, filename)
article = build_article_from_filepath(
article = utils.build_article_from_filepath(
article_filepath, configuration, serie
)
articles.append(article)
......@@ -382,7 +103,7 @@ def main(environment):
output_filepath = os.path.join(output_path, f"{page.slug()}.html")
# Create dir structure for the file
mkdirs_for_file(output_filepath)
utils.mkdirs_for_file(output_filepath)
# And write the file
with open(output_filepath, "w") as output_file:
......@@ -436,7 +157,7 @@ def main(environment):
"updated": current_datetime,
}
)
add_articles_to_feed(feed, articles)
utils.add_articles_to_feed(feed, articles)
# And write it on the filesystem
feed_filepath = os.path.join(feed_dirpath, "all.atom.xml")
......@@ -458,7 +179,7 @@ def main(environment):
"updated": current_datetime,
}
)
add_articles_to_feed(feed, serie_articles)
utils.add_articles_to_feed(feed, serie_articles)
# And write it on the filesystem
feed.write(feed_filepath)
......@@ -472,10 +193,7 @@ def main(environment):
if __name__ == "__main__":
environment = Environment.PRODUCTION
if len(sys.argv) == 2 and sys.argv[1] == "--development":
environment = Environment.DEVELOPMENT
environment = environment.load_from_argv(sys.argv)
try:
main(environment)
print("Boop!")
......
import os
import markdown
import datetime
import uuid
import pytz
import boop.boopsy as boopsy
class Article:
def __init__(self, slug, article_content, configuration={}, serie=None):
# We convert the file's content into HTML
md = markdown.Markdown(extensions=["meta", "fenced_code", "attr_list"])
content = md.convert(article_content)
# We get the local variables from the Markdown file (metadata)
# which will be accessible in the article template
self.meta = {}
self.meta.update(configuration)
self.meta["ARTICLE_SLUG"] = slug
self.meta["ARTICLE_TEMPLATE"] = "article"
for key, values in md.Meta.items():
article_key = f"ARTICLE_{key.upper()}"
# The Markdown's meta extension extract all the values in arrays, even
# if there is only one value.
if len(values) > 1:
self.meta[article_key] = values
else:
self.meta[article_key] = values[0]
self.meta["ARTICLE_CONTENT"] = content
self.meta["ARTICLE_SERIE"] = serie
# And we initialize a template for the article
article_template_filepath = os.path.join(
"templates", f"{self.meta['ARTICLE_TEMPLATE']}.html"
)
self.template = boopsy.Template(article_template_filepath)
def render(self):
return self.template.render(self.meta)
def slug(self):
return self.meta["ARTICLE_SLUG"]
def title(self):
return self.meta.get("ARTICLE_TITLE", "An article")
def author(self):
if "ARTICLE_AUTHOR" in self.meta:
return self.meta["ARTICLE_AUTHOR"]
elif "SITE_AUTHOR" in self.meta:
return self.meta["SITE_AUTHOR"]
else:
return None
def url(self):
site_url = self.meta.get("SITE_URL", "")
return f"{site_url}{self.slug()}.html"
def uuid(self):
return str(uuid.uuid5(uuid.NAMESPACE_URL, self.url()))
def content(self):
return self.meta["ARTICLE_CONTENT"]
def timezone(self):
site_timezone = self.meta.get("SITE_TIMEZONE", "UTC")
return pytz.timezone(site_timezone)
def date(self):
if "ARTICLE_DATE" not in self.meta:
return datetime.datetime.now(tz=self.timezone())
article_date = datetime.datetime.strptime(
self.meta["ARTICLE_DATE"], "%Y-%m-%d %H:%M"
)
return article_date.astimezone(tz=self.timezone())
def update(self):
if "ARTICLE_UPDATE" not in self.meta:
return None
article_update = datetime.datetime.strptime(
self.meta["ARTICLE_UPDATE"], "%Y-%m-%d %H:%M"
)
return article_update.astimezone(tz=self.timezone())
def serie(self):
return self.meta["ARTICLE_SERIE"]
import os
import yaml
import uuid
from boop.environment import Environment
def load(configuration_path, environment, output_path):
configuration = {
"SITE_URL": "filesystem",
"SITE_TITLE": "A website",
"SITE_TIMEZONE": "UTC",
}
if os.path.exists(configuration_path):
with open(configuration_path) as conf_file:
conf_data = yaml.load(conf_file)
for key, value in conf_data.items():
if key == "development":
continue
site_key = f"SITE_{key.upper()}"
configuration[site_key] = value
if environment == Environment.DEVELOPMENT and "development" in conf_data:
for key, value in conf_data["development"].items():
site_key = f"SITE_{key.upper()}"
configuration[site_key] = value
configuration["SITE_UUID"] = str(
uuid.uuid5(uuid.NAMESPACE_URL, configuration["SITE_URL"])
)
if "SITE_AUTHOR" not in configuration:
configuration["SITE_AUTHOR"] = f"{configuration['SITE_TITLE']}'s author"
if configuration["SITE_URL"] == "filesystem":
configuration["SITE_URL"] = f"file://{os.path.abspath(output_path)}/"
return configuration
from enum import Enum
class Environment(Enum):
DEVELOPMENT = 1
PRODUCTION = 2
def load_from_argv(argv):
if "--development" in argv:
return Environment.DEVELOPMENT
else:
return Environment.PRODUCTION
import os
import uuid
import re
import yaml
import boop.boopsy as boopsy
class Page:
def __init__(self, slug, page_content, configuration={}):
# We initialize the page meta variables which will be accessible in the
# template
self.meta = {}
self.meta.update(configuration)
self.meta["PAGE_SLUG"] = slug
self.meta["PAGE_TEMPLATE"] = "page"
yaml_conf, page_content = extract_yaml_configuration(page_content)
for key, value in yaml_conf.items():
article_key = f"PAGE_{key.upper()}"
self.meta[article_key] = value
self.meta["PAGE_CONTENT"] = page_content
# And we initialize a template for the page
page_template_filepath = os.path.join(
"templates", f"{self.meta['PAGE_TEMPLATE']}.html"
)
self.template = None
if os.path.exists(page_template_filepath):
self.template = boopsy.Template(page_template_filepath)
def render(self):
if self.template is None:
return self.content()
else:
return self.template.render(self.meta)
def slug(self):
return self.meta["PAGE_SLUG"]
def title(self):
return self.meta.get("PAGE_TITLE", "A page")
def url(self):
site_url = self.meta.get("SITE_URL", "")
return f"{site_url}{self.slug()}.html"
def uuid(self):
return str(uuid.uuid5(uuid.NAMESPACE_URL, self.url()))
def content(self):
return self.meta["PAGE_CONTENT"]
YAML_FRONT_MATTER_REGEX = re.compile("(---\n.*\n)(---\n)(.*)", re.M | re.S)
def extract_yaml_configuration(content):
match = YAML_FRONT_MATTER_REGEX.match(content)
if match:
yaml_conf = yaml.load(match.group(1))
return yaml_conf, match.group(3)
else:
return {}, content
import os
import shutil
from boop.article import Article
def init_output_folder(output_path):
"""Create the output directory, empty it if it contains files.
"""
if os.path.isdir(output_path):
# it should be a directory...
shutil.rmtree(output_path)
elif os.path.exists(output_path):
# ... but it also can be a simple file if user created it manually!
os.remove(output_path)
os.mkdir(output_path</