diff --git a/features/data/configs/dayone2.yaml b/features/data/configs/dayone2.yaml new file mode 100644 index 00000000..0831af75 --- /dev/null +++ b/features/data/configs/dayone2.yaml @@ -0,0 +1,12 @@ +default_hour: 9 +default_minute: 0 +editor: '' +template: false +encrypt: false +highlight: true +journals: + default: features/journals/dayone2.json +linewrap: 80 +tagsymbols: '@' +timeformat: '%Y-%m-%d %H:%M' +indent_character: "|" diff --git a/features/data/journals/dayone2.json b/features/data/journals/dayone2.json new file mode 100644 index 00000000..4f3ff20e --- /dev/null +++ b/features/data/journals/dayone2.json @@ -0,0 +1,27 @@ +{ + "entries": [ + { + "audios": [], + "creationDate": "2020-01-10T12:22:12Z", + "photos": [], + "starred": false, + "tags": [], + "text": "Entry Number One.", + "timeZone": "Europe/London", + "uuid": "EBB5A5F4057F461E8F176E18AB7E0493" + }, + { + "audios": [], + "creationDate": "2020-01-10T12:21:48Z", + "photos": [], + "starred": true, + "tags": [], + "text": "Entry Number Two.\n\nAnd a bit of text over here.", + "timeZone": "Europe/London", + "uuid": "EC61E5D4E66C44E6AC796E863E8BF7E7" + } + ], + "metadata": { + "version": "1.2" + } +} \ No newline at end of file diff --git a/features/dayone2.feature b/features/dayone2.feature new file mode 100644 index 00000000..5c809479 --- /dev/null +++ b/features/dayone2.feature @@ -0,0 +1,15 @@ +Feature: Day One 2.0 implementation details. + + + Scenario: Loading a Day One 2.0 journal + Given we use the config "dayone2.yaml" + When we run "jrnl -n 2" + Then we should get no error + and the output should be + """ + 2020-01-10 12:21 Entry Number Two. + | And a bit of text over here. + + 2020-01-10 12:22 Entry Number One. + """ + diff --git a/jrnl/Journal.py b/jrnl/Journal.py index e5bf4ecc..11788d3d 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -385,6 +385,12 @@ def open_journal(name, config, legacy=False): sys.exit(1) + elif config["journal"].strip("/").endswith(".json"): + # Loads a Day One v2.0 journal + from .dayone2 import DayOne2 + + return DayOne2(**config).open() + if not config["encrypt"]: if legacy: return LegacyJournal(name, **config).open() diff --git a/jrnl/cli.py b/jrnl/cli.py index 1c53dc29..433560db 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -133,20 +133,9 @@ def parse_args(args=None): metavar="TYPE", dest="import_", choices=plugins.IMPORT_FORMATS, - help="Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.".format( - plugins.util.oxford_list(plugins.IMPORT_FORMATS) - ), + help="Import entries into your journal. TYPE can be one of "\ + "{0}".format(plugins.util.oxford_list(plugins.IMPORT_FORMATS)), default=False, - const="jrnl", - nargs="?", - ) - exporting.add_argument( - "-i", - metavar="INPUT", - dest="input", - help="Optionally specifies input file when using --import.", - default=False, - const=None, ) exporting.add_argument( "--encrypt", @@ -181,14 +170,14 @@ def guess_mode(args, config): compose = True export = False import_ = False - if args.import_ is not False: + if args.import_: compose = False export = False import_ = True elif ( - args.decrypt is not False - or args.encrypt is not False - or args.export is not False + args.decrypt + or args.encrypt + or args.export or any((args.short, args.tags, args.edit)) ): compose = False @@ -357,6 +346,10 @@ def run(manual_args=None): else: sys.exit() + # Import mode + if args.import_: + return plugins.get_importer(args.import_, args.text) + # This is where we finally open the journal! try: journal = open_journal(journal_name, config) @@ -364,12 +357,8 @@ def run(manual_args=None): print(f"[Interrupted while opening journal]", file=sys.stderr) sys.exit(1) - # Import mode - if mode_import: - plugins.get_importer(args.import_).import_(journal, args.input) - # Writing mode - elif mode_compose: + if mode_compose: raw = " ".join(args.text).strip() log.debug('Appending raw line "%s" to journal "%s"', raw, journal_name) journal.new_entry(raw) diff --git a/jrnl/dayone2.py b/jrnl/dayone2.py new file mode 100644 index 00000000..ec8e6c9a --- /dev/null +++ b/jrnl/dayone2.py @@ -0,0 +1,29 @@ +import json + +from datetime import datetime + +from .Entry import Entry +from . import Journal + + +class DayOne2(Journal.PlainJournal): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def _parse(self, json_data): + + entries = [] + + json_string = json.loads(json_data) + + for entry in json_string["entries"]: + entries.append( + Entry( + self, + date=datetime.strptime(entry["creationDate"], "%Y-%m-%dT%H:%M:%SZ"), + text=entry["text"], + starred=entry["starred"], + ) + ) + + return entries diff --git a/jrnl/plugins/__init__.py b/jrnl/plugins/__init__.py index 00ee2498..cb1c1ff9 100644 --- a/jrnl/plugins/__init__.py +++ b/jrnl/plugins/__init__.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # encoding: utf-8 +from .dayone2_importer import DayOne2Importer from .text_exporter import TextExporter from .jrnl_importer import JRNLImporter from .json_exporter import JSONExporter @@ -20,7 +21,9 @@ __exporters = [ YAMLExporter, FancyExporter, ] + template_exporters -__importers = [JRNLImporter] + +__importers = [DayOne2Importer, + JRNLImporter] __exporter_types = {name: plugin for plugin in __exporters for name in plugin.names} __importer_types = {name: plugin for plugin in __importers for name in plugin.names} @@ -36,8 +39,8 @@ def get_exporter(format): return None -def get_importer(format): +def get_importer(file_format, path): for importer in __importers: - if hasattr(importer, "names") and format in importer.names: - return importer + if hasattr(importer, "names") and file_format in importer.names: + return importer(path) return None diff --git a/jrnl/plugins/dayone2_importer.py b/jrnl/plugins/dayone2_importer.py new file mode 100644 index 00000000..dea3df7e --- /dev/null +++ b/jrnl/plugins/dayone2_importer.py @@ -0,0 +1,45 @@ +from datetime import datetime + +from jrnl.Entry import Entry +from jrnl.plugins.json_importer import JSONImporter + + +class DayOne2Importer(JSONImporter): + + names = ["dayone2"] + extension = "json" + + def __init__(self, path): + self.type = 'Day One 2' + self.path = path + self.keys = ['audios', 'creationDate', 'photos', + 'starred', 'tags', 'text', 'timeZone', 'uuid'] + JSONImporter.__init__(self) + self.convert_journal() + + def convert_journal(self): + print(self._convert()) + + def validate_schema(self): + for key in self.json['entries'][0]: + try: + assert key in self.keys + + except AssertionError: + raise KeyError(f"{self.path} is not the expected Day One 2 format.") + print(f"{self.path} validated as Day One 2.") + return True + + def parse_json(self): + entries = [] + for entry in self.json["entries"]: + entries.append( + Entry( + self, + date=datetime.strptime(entry["creationDate"], "%Y-%m-%dT%H:%M:%SZ"), + text=entry["text"], + starred=entry["starred"], + ) + ) + self.journal.entries = entries + return entries diff --git a/jrnl/plugins/jrnl_importer.py b/jrnl/plugins/jrnl_importer.py index 972114d4..d7dd3bf1 100644 --- a/jrnl/plugins/jrnl_importer.py +++ b/jrnl/plugins/jrnl_importer.py @@ -10,13 +10,16 @@ class JRNLImporter: names = ["jrnl"] - @staticmethod - def import_(journal, input=None): + def __init__(self, input): + self.input = input + self.import_(self.input) + + def import_(self, journal, input=None): """Imports from an existing file if input is specified, and standard input otherwise.""" old_cnt = len(journal.entries) old_entries = journal.entries - if input: + if self.input: with open(input, "r", encoding="utf-8") as f: other_journal_txt = f.read() else: diff --git a/jrnl/plugins/json_importer.py b/jrnl/plugins/json_importer.py new file mode 100644 index 00000000..bff811fa --- /dev/null +++ b/jrnl/plugins/json_importer.py @@ -0,0 +1,50 @@ +import json +import os + +from abc import abstractmethod +from pathlib import Path + +from jrnl.Journal import PlainJournal +from jrnl.plugins.text_exporter import TextExporter + + +class JSONImporter(PlainJournal, TextExporter): + """This importer reads a JSON file and returns a dict. """ + + def __init__(self): + PlainJournal.__init__(self) + self.journal = PlainJournal() + self.path = self.path[0] + self.filename = os.path.splitext(self.path)[0] + self.json = self.import_file() + + def __str__(self): + return f"{self.type} journal with {len(self.journal)} " \ + f"entries located at {self.path}" + + def _convert(self): + if self.validate_schema(): + self.data = self.parse_json() + self.create_file(self.filename + '.txt') + return self.export(self.journal, self.filename + '.txt') + + def import_file(self): + """Reads a JSON file and returns a dict.""" + if os.path.exists(self.path): + try: + with open(self.path) as f: + return json.load(f) + except json.JSONDecodeError: + print(f"{self.path} is not valid JSON.") + elif Path(self.path).suffix != '.json': + print(f"{self.path} must be a JSON file.") + else: + print(f"{self.path} does not exist.") + + @abstractmethod + def parse_json(self): + raise NotImplementedError + + @abstractmethod + def validate_schema(self): + raise NotImplementedError