Plugin architecture

This commit is contained in:
Manuel Ebert 2014-09-27 15:07:22 -07:00
parent a1b5a4099e
commit 6fed042b8b
9 changed files with 287 additions and 188 deletions

44
jrnl/plugins/__init__.py Normal file
View file

@ -0,0 +1,44 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
import glob
import os
import importlib
class PluginMeta(type):
def __init__(cls, name, bases, attrs):
"""Called when a Plugin derived class is imported"""
if not hasattr(cls, 'PLUGINS'):
cls.PLUGINS = []
cls.PLUGIN_NAMES = []
else:
cls.__register_plugin(cls)
def __register_plugin(cls, plugin):
"""Add the plugin to the plugin list and perform any registration logic"""
cls.PLUGINS.append(plugin)
cls.PLUGIN_NAMES.extend(plugin.names)
class BaseExporter(object):
__metaclass__ = PluginMeta
names = []
class BaseImporter(object):
__metaclass__ = PluginMeta
names = []
for module in glob.glob(os.path.dirname(__file__) + "/*.py"):
importlib.import_module("." + os.path.basename(module)[:-3], "jrnl.plugins")
del module
def get_exporter(format):
for exporter in BaseExporter.PLUGINS:
if hasattr(exporter, "names") and format in exporter.names:
return exporter
return None

View file

@ -0,0 +1,38 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .text_exporter import TextExporter
import json
from .util import get_tags_count
class JSONExporter(TextExporter):
"""This Exporter can convert entries and journals into json."""
names = ["json"]
extension = "json"
@classmethod
def entry_to_dict(cls, entry):
return {
'title': entry.title,
'body': entry.body,
'date': entry.date.strftime("%Y-%m-%d"),
'time': entry.date.strftime("%H:%M"),
'starred': entry.starred
}
@classmethod
def export_entry(cls, entry):
"""Returns a json representation of a single entry."""
return json.dumps(cls.entry_to_dict(entry), indent=2) + "\n"
@classmethod
def export_journal(cls, journal):
"""Returns a json representation of an entire journal."""
tags = get_tags_count(journal)
result = {
"tags": dict((tag, count) for count, tag in tags),
"entries": [cls.entry_to_dict(e) for e in journal.entries]
}
return json.dumps(result, indent=2)

View file

@ -0,0 +1,44 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .text_exporter import TextExporter
class MarkdownExporter(TextExporter):
"""This Exporter can convert entries and journals into json."""
names = ["md", "markdown"]
extension = "md"
@classmethod
def export_entry(cls, entry):
"""Returns a markdown representation of a single entry."""
date_str = entry.date.strftime(entry.journal.config['timeformat'])
body_wrapper = "\n" if entry.body else ""
body = body_wrapper + entry.body
return "{md} {date} {title} {body} {space}".format(
md="###",
date=date_str,
title=entry.title,
body=body,
space=""
)
@classmethod
def export_journal(cls, journal):
"""Returns a json representation of an entire journal."""
out = []
year, month = -1, -1
for e in journal.entries:
if not e.date.year == year:
year = e.date.year
out.append(str(year))
out.append("=" * len(str(year)) + "\n")
if not e.date.month == month:
month = e.date.month
out.append(e.date.strftime("%B"))
out.append('-' * len(e.date.strftime("%B")) + "\n")
out.append(cls.export_entry(e))
result = "\n".join(out)
return result

View file

@ -0,0 +1,30 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .text_exporter import TextExporter
from .util import get_tags_count
class TagExporter(TextExporter):
"""This Exporter can convert entries and journals into json."""
names = ["tags"]
extension = "tags"
@classmethod
def export_entry(cls, entry):
"""Returns a markdown representation of a single entry."""
return ", ".join(entry.tags)
@classmethod
def export_journal(cls, journal):
"""Returns a json representation of an entire journal."""
tag_counts = get_tags_count(journal)
result = ""
if not tag_counts:
return '[No tags found in journal.]'
elif min(tag_counts)[0] == 0:
tag_counts = filter(lambda x: x[0] > 1, tag_counts)
result += '[Removed tags that appear only once.]\n'
result += "\n".join("{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True))
return result

View file

@ -0,0 +1,62 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
import codecs
from . import BaseExporter
from ..util import u, slugify
import os
class TextExporter(BaseExporter):
"""This Exporter can convert entries and journals into text files."""
names = ["text", "txt"]
extension = "txt"
@classmethod
def export_entry(cls, entry):
"""Returns a unicode representation of a single entry."""
return u.__unicode__()
@classmethod
def export_journal(cls, journal):
"""Returns a unicode representation of an entire journal."""
return journal.pprint()
@classmethod
def write_file(cls, journal, path):
"""Exports a journal into a single file."""
try:
with codecs.open(path, "w", "utf-8") as f:
f.write(cls.export_journal(journal))
return "[Journal exported to {0}]".format(path)
except IOError as e:
return "[ERROR: {0} {1}]".format(e.filename, e.strerror)
@classmethod
def make_filename(cls, entry):
return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), cls.extension))
@classmethod
def write_files(cls, journal, path):
"""Exports a journal into individual files for each entry."""
for entry in journal.entries:
try:
full_path = os.path.join(path, cls.make_filename(entry))
with codecs.open(full_path, "w", "utf-8") as f:
f.write(cls.export_entry(entry))
except IOError as e:
return "[ERROR: {0} {1}]".format(e.filename, e.strerror)
return "[Journal exported to {0}]".format(path)
@classmethod
def export(cls, journal, output=None):
"""Exports to individual files if output is an existing path, or into
a single file if output is a file name, or returns the exporter's
representation as unicode if output is None."""
if output and os.path.isdir(output): # multiple files
return cls.write_files(journal, output)
elif output:
return cls.write_file(journal, output)
else:
return cls.export_journal(journal)

14
jrnl/plugins/util.py Normal file
View file

@ -0,0 +1,14 @@
#!/usr/bin/env python
# encoding: utf-8
def get_tags_count(journal):
"""Returns a set of tuples (count, tag) for all tags present in the journal."""
# Astute reader: should the following line leave you as puzzled as me the first time
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
tags = [tag
for entry in journal.entries
for tag in set(entry.tags)]
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
tag_counts = set([(tags.count(tag), tag) for tag in tags])
return tag_counts

View file

@ -0,0 +1,50 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .json_exporter import JSONExporter
from .util import get_tags_count
from ..util import u
from xml.dom import minidom
class XMLExporter(JSONExporter):
"""This Exporter can convert entries and journals into XML."""
names = ["xml"]
extension = "xml"
@classmethod
def export_entry(cls, entry, doc=None):
"""Returns an XML representation of a single entry."""
doc_el = doc or minidom.Document()
entry_el = doc_el.createElement('entry')
for key, value in cls.entry_to_dict(entry).items():
elem = doc_el.createElement(key)
elem.appendChild(doc_el.createTextNode(u(value)))
entry_el.appendChild(elem)
if not doc:
doc_el.appendChild(entry_el)
return doc_el.toprettyxml()
else:
return entry_el
@classmethod
def export_journal(cls, journal):
"""Returns an XML representation of an entire journal."""
tags = get_tags_count(journal)
doc = minidom.Document()
xml = doc.createElement('journal')
tags_el = doc.createElement('tags')
entries_el = doc.createElement('entries')
for tag in tags:
tag_el = doc.createElement('tag')
tag_el.setAttribute('name', tag[1])
count_node = doc.createTextNode(u(tag[0]))
tag.appendChild(count_node)
tags_el.appendChild(tag)
for entry in journal.entries:
entries_el.appendChild(cls.entry_to_xml(entry, doc))
xml.appendChild(entries_el)
xml.appendChild(tags_el)
doc.appendChild(xml)
return doc.toprettyxml()