Beginning with documentation of code

This commit is contained in:
JayPiKay 2018-12-25 11:04:16 +01:00
parent d5b2048869
commit 611b35e035
8 changed files with 364 additions and 11 deletions

.gitignore vendored
View File

@ -13,7 +13,7 @@ dist/

docs/Makefile Normal file
View File

@ -0,0 +1,19 @@
# Minimal makefile for Sphinx documentation
# You can set these variables from the command line.
SPHINXBUILD = sphinx-build
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile

docs/ Normal file
View File

@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-
# Configuration file for the Sphinx documentation builder.
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
import os
import sys
sys.path.insert(0, os.path.abspath('..'))
# -- Project information -----------------------------------------------------
project = u'GoatNet :: Retro Blog'
copyright = u'2018, Julian Knauer'
author = u'Julian Knauer'
# The short X.Y version
version = u''
# The full version, including alpha/beta/rc tags
release = u''
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = None
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'GoatNetRetroBlogdoc'
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
# Latex figure (float) alignment
# 'figure_align': 'htbp',
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'GoatNetRetroBlog.tex', u'GoatNet :: Retro Blog Documentation',
u'Julian Knauer', 'manual'),
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'goatnetretroblog', u'GoatNet :: Retro Blog Documentation',
[author], 1)
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'GoatNetRetroBlog', u'GoatNet -- Retro Blog Documentation',
author, 'GoatNetRetroBlog', 'One line description of project.',
# -- Options for Epub output -------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = project
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
# epub_identifier = ''
# A unique identification for the text.
# epub_uid = ''
# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']
# -- Extension configuration -------------------------------------------------

docs/goatnet.rst Normal file
View File

@ -0,0 +1,12 @@
.. automodule:: goatnet
.. automodule:: goatnet.blogstore

docs/index.rst Normal file
View File

@ -0,0 +1,21 @@
.. GoatNet -- Retro Blog documentation master file, created by
sphinx-quickstart on Tue Dec 25 10:17:59 2018.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to GoatNet :: Retro Blog's documentation!
.. toctree::
:maxdepth: 2
:caption: Contents:
Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -1,10 +1,8 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
# Copyright © 2018 jpk <>
# Distributed under terms of the MIT license.
GoatNet Blog Application
.. moduleauthor:: Julian Knauer <>
import os
@ -13,6 +11,14 @@ from flask import Flask
def create_app(test_config=None):
"""Create and initialize the application context.
test_config (dict): Application configuration
The Flask application.
# create and configure the app
app = Flask(__name__, instance_relative_config=True)

View File

@ -1,3 +1,21 @@
Blog posts are stored as YAML files.
.. [1] Blog post file structure.
The YAML file is structured as follows::
author: <name of author|username>
title: <title of blog entry>
created: <unix timestamp in hex>
modified: <unix timestamp in hex>
body: |
<A reStructuredText formatted text>
.. moduleauthor:: Julian Knauer <>
import os
import shutil
@ -11,21 +29,46 @@ from flask import current_app, g
from flask.cli import with_appcontext
#: Definition of literal scalar string formatter.
literal = ruamel.yaml.scalarstring.LiteralScalarString
def transform_newline(line):
"""Replace Windows newline with Unix newline.
line (string): Input to search and replace all windows newlines.
The input is transformed with Unix style newlines.
return line.replace('\r\n', '\n')
class BlogStore():
"""BlogStore manages and organizes the blog posts and files.
def __init__(self, path):
"""Initialize the blog file store.
path (string): Points to the directory storing the blog YAML files.
self.path = path
def read_file(filename):
"""Open YAML file and return the post content
"""Open YAML file and return the post content.
filename (string): Blog post YAML file to open.
A dict mapping of the blog posts meta data and the post body
The YAML file is structured as shown in [1]_.
with open(filename, 'r') as ymlfile:
yaml = YAML(typ='safe')
@ -33,6 +76,14 @@ class BlogStore():
return data
def locate_file(self, entry):
"""Locate the filename and path of the entry.
entry (str): A unix timestamp in hex representation.
The full path to the file within the blog storage filesystem.
ts = int(entry, base=16)
date_dir = datetime.utcfromtimestamp(ts).strftime('%Y/%m')
filename = os.path.join(self.path, date_dir, f'{entry}.yaml')
@ -41,6 +92,11 @@ class BlogStore():
return None
def get_posts(self):
"""Return all blog posts found within the blog storage filesystem.
A list of dict returned by :func:`read_file`.
posts = []
for root, _, files in os.walk(self.path):
for filename in files:
@ -51,6 +107,19 @@ class BlogStore():
return posts
def get_post(self, entry):
"""Get a single post by its Unix timestamp hexadecimal represented ID.
entry (str): Unix timestamp hexadecimal representation.
The function returns either ``None`` or a dict returned by
If the entry is not correctly formatted as unix timestring in
hexadecimal or the entry cannot be located in the blog store
filesystem, the function will return ``None``.
ts = int(entry, base=16)
@ -63,10 +132,14 @@ class BlogStore():
return None
def update_post(self, entry, post):
"""Modify the blog post and update the file.
entry (str): Unix timestamp hexadecimal representation.
post (dict): Blog post as provided by :func:`read_file`.
post['meta']['modified'] = hex(int(time.time()))[2:]
post['body'] = literal(post['body'])
if 'entry' in post:
del post['entry']
filename = self.locate_file(entry)
with open(os.path.join(self.path, filename), 'w') as ymlfile:
@ -74,6 +147,17 @@ class BlogStore():
yaml.dump(post, ymlfile, transform=transform_newline)
def create_post(self, title, body, author):
"""Create a new blog post and save it in the blog post filesystem.
The YAML file is structured as shown in [1]_.
title (str): Title of the blog post.
body (str): A `reStructuredText`_ formatted content.
author (str): Username or name of the author.
.. _reStructuredText:
tstamp = hex(int(time.time()))[2:]
post = {
'meta': {
@ -94,18 +178,39 @@ class BlogStore():
yaml.dump(post, ymlfile, transform=transform_newline)
def delete_post(self, entry):
"""Delete a blog post and the file.
As a safe guard, the ``filename`` of the blog post is checked against
the ``path`` set during setup of :mod:`BlogStore`. The ``filename``
must be within ``path`` in order to be deleted.
entry (str): Unix timestamp hexadecimal representation.
filename = self.locate_file(entry)
if self.path in filename:
def get_blogstore():
"""Return a :mod:`BlogStore` object per application.
The :mod:`BlogStore` object is initialized with the parameter set in the
configuration file for the application. The initialized :mod:`BlogStore`
object is stored in the global variable store :mod:`flask.g`.
Per application context a single :mod:`BlogStore` is initialized and
if 'blogstore' not in g:
g.blogstore = BlogStore(current_app.config['BLOGSTORE'])
return g.blogstore
def init_blogstore():
"""Create the filesystem structure for the blog store.
blogstore = get_blogstore()
@ -116,6 +221,10 @@ def init_blogstore():
def close_blogstore(e=None):
"""Context aware tear down of the blog store.
Removes the :mod:`BlogStore` from the :mod:`flask.g` global variable store.
blogstore = g.pop('blogstore', None)
if blogstore is not None:
@ -124,6 +233,8 @@ def close_blogstore(e=None):
def purge_blogstore():
"""Delete all blog posts and directories.
if os.path.exists(current_app.config['BLOGSTORE']):

View File

@ -1,3 +1,4 @@
@ -8,13 +9,17 @@ email-validator==1.0.3
@ -23,3 +28,4 @@ ruamel.yaml==0.15.81