From 52eaef25d6e0e4bc12d0142bbebc4a63ce88ff8e Mon Sep 17 00:00:00 2001 From: Eshan Date: Sat, 30 May 2020 15:43:10 -0400 Subject: [PATCH 1/5] Fix set_keychain errors (#964) * fix keyring problems * black * remove else and use stderr * black * add tests * black * change description of nokeyring * dumb syntax error --- features/encryption.feature | 24 ++++++++++++++++++++++++ features/steps/core.py | 21 +++++++++++++++++++++ jrnl/util.py | 11 +++++++---- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/features/encryption.feature b/features/encryption.feature index 787fa850..02f8b423 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -55,3 +55,27 @@ Then the config for journal "simple" should have "encrypt" set to "bool:True" When we run "jrnl simple -n 1" Then the output should contain "2013-06-10 15:40 Life is good" + + Scenario: Encrypt journal with no keyring backend and do not store in keyring + Given we use the config "basic.yaml" + When we disable the keychain + and we run "jrnl test entry" + and we run "jrnl --encrypt" and enter + """ + password + password + n + """ + Then we should get no error + + Scenario: Encrypt journal with no keyring backend and do store in keyring + Given we use the config "basic.yaml" + When we disable the keychain + and we run "jrnl test entry" + and we run "jrnl --encrypt" and enter + """ + password + password + y + """ + Then we should get no error diff --git a/features/steps/core.py b/features/steps/core.py index 79591f57..3befe4bd 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -41,6 +41,22 @@ class TestKeyring(keyring.backend.KeyringBackend): self.keys[servicename][username] = None +class NoKeyring(keyring.backend.KeyringBackend): + """A keyring that simulated an environment with no keyring backend.""" + + priority = 2 + keys = defaultdict(dict) + + def set_password(self, servicename, username, password): + raise keyring.errors.NoKeyringError + + def get_password(self, servicename, username): + raise keyring.errors.NoKeyringError + + def delete_password(self, servicename, username): + raise keyring.errors.NoKeyringError + + # set the keyring for keyring lib keyring.set_keyring(TestKeyring()) @@ -208,6 +224,11 @@ def set_keychain(context, journal, password): keyring.set_password("jrnl", journal, password) +@when("we disable the keychain") +def disable_keychain(context): + keyring.core.set_keyring(NoKeyring()) + + @then("we should get an error") def has_error(context): assert context.exit_status != 0, context.exit_status diff --git a/jrnl/util.py b/jrnl/util.py index db86a5ee..8ebaea41 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -59,9 +59,6 @@ def create_password( if yesno("Do you want to store the password in your keychain?", default=True): set_keychain(journal_name, pw) - else: - set_keychain(journal_name, None) - return pw @@ -107,7 +104,13 @@ def set_keychain(journal_name, password): except keyring.errors.PasswordDeleteError: pass else: - keyring.set_password("jrnl", journal_name, password) + try: + keyring.set_password("jrnl", journal_name, password) + except keyring.errors.NoKeyringError: + print( + "Keyring backend not found. Please install one of the supported backends by visiting: https://pypi.org/project/keyring/", + file=sys.stderr, + ) def yesno(prompt, default=True): From 3a94a4b215ea7c693889f7db5bcb8a48c3e4b70c Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Sat, 30 May 2020 19:47:13 +0000 Subject: [PATCH 2/5] Increment version to v2.4.3-beta --- jrnl/__version__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jrnl/__version__.py b/jrnl/__version__.py index 8b8ae09f..2f5c17c2 100644 --- a/jrnl/__version__.py +++ b/jrnl/__version__.py @@ -1 +1 @@ -__version__ = "v2.4.2" +__version__ = "v2.4.3-beta" diff --git a/pyproject.toml b/pyproject.toml index 3a2072a6..673f8bcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "jrnl" -version = "v2.4.2" +version = "v2.4.3-beta" description = "Collect your thoughts and notes without leaving the command line." authors = [ "Manuel Ebert ", From 61f5c3f2e38ca09adc07a06518b2f9e4c8810c36 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Sat, 6 Jun 2020 13:41:15 -0600 Subject: [PATCH 3/5] Add extended metadata support for DayOne Classic (#928) * Updating changelog [ci skip] * Incrementing version to v2.4 [ci skip] * [DayOne] remove extra spaces from the titles of edited DayOne entries Otherwise, a leading space was being introduced * [DayOne] maintain existing tags stored in DayOne metadata * [DayOne] brings back extended DayOne attributes * [DayOne] maintain metadata on edited entries Fixes #358, See also #159 * [DayOne Exporter] apply black formatting * [JSON Exporter] add support for extended DayOne Metadata * [DayOne] [Tests] test that extended DayOne metadata is added to new entries Co-authored-by: Jrnl Bot --- .gitignore | 1 + CHANGELOG.md | 19 +++++++ features/dayone.feature | 12 +++++ features/steps/export_steps.py | 22 ++++++-- jrnl/DayOneJournal.py | 93 ++++++++++++++++++++++++++++++++-- jrnl/plugins/json_exporter.py | 21 ++++++++ 6 files changed, 160 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index db6df82b..afb0d874 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ obj env/ env*/ venv*/ +.venv*/ # PyCharm Project files .idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md index e33b6dd5..6c4761c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [v2.4](https://pypi.org/project/jrnl/v2.4/) (2020-04-25) + +[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.4-beta...v2.4) + +**Implemented enhancements:** + +- Upgrade license to GPLv3 [\#918](https://github.com/jrnl-org/jrnl/pull/918) ([wren](https://github.com/wren)) + +**Build:** + +- Update makefile to match pipeline better [\#919](https://github.com/jrnl-org/jrnl/pull/919) ([wren](https://github.com/wren)) + +**Updated documentation:** + +- Clean up readme file [\#924](https://github.com/jrnl-org/jrnl/pull/924) ([wren](https://github.com/wren)) +- Clarify that editing config isn't always destructive [\#923](https://github.com/jrnl-org/jrnl/pull/923) ([Epskampie](https://github.com/Epskampie)) + +# Changelog + ## [Unreleased](https://github.com/jrnl-org/jrnl/) [Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.4.2...HEAD) diff --git a/features/dayone.feature b/features/dayone.feature index 8e50b42b..bf74dc8d 100644 --- a/features/dayone.feature +++ b/features/dayone.feature @@ -63,3 +63,15 @@ Feature: Dayone specific implementation details. Then we should get no error and the output should be parsable as json and the json output should contain entries.0.uuid = "4BB1F46946AD439996C9B59DE7C4DDC1" + + Scenario: Writing into Dayone adds extended metadata + Given we use the config "dayone.yaml" + When we run "jrnl 01 may 1979: Being born hurts." + and we run "jrnl --export json" + Then "entries" in the json output should have 5 elements + and the json output should contain entries.0.creator.software_agent + and the json output should contain entries.0.creator.os_agent + and the json output should contain entries.0.creator.host_name + and the json output should contain entries.0.creator.generation_date + and the json output should contain entries.0.creator.device_agent + and "entries.0.creator.software_agent" in the json output should contain "jrnl" diff --git a/features/steps/export_steps.py b/features/steps/export_steps.py index 0aa180fd..6c8cd282 100644 --- a/features/steps/export_steps.py +++ b/features/steps/export_steps.py @@ -32,13 +32,21 @@ def check_output_field_not_key(context, field, key): @then('"{field}" in the json output should contain "{key}"') def check_output_field_key(context, field, key): out = context.stdout_capture.getvalue() - out_json = json.loads(out) - assert field in out_json - assert key in out_json[field] + struct = json.loads(out) + + for node in field.split("."): + try: + struct = struct[int(node)] + except ValueError: + assert node in struct + struct = struct[node] + + assert key in struct +@then("the json output should contain {path}") @then('the json output should contain {path} = "{value}"') -def check_json_output_path(context, path, value): +def check_json_output_path(context, path, value=None): """ E.g. the json output should contain entries.0.title = "hello" """ @@ -50,7 +58,11 @@ def check_json_output_path(context, path, value): struct = struct[int(node)] except ValueError: struct = struct[node] - assert struct == value, struct + + if value is not None: + assert struct == value, struct + else: + assert struct is not None @then("the output should be a valid XML string") diff --git a/jrnl/DayOneJournal.py b/jrnl/DayOneJournal.py index 1ee44b97..dd1cbc24 100644 --- a/jrnl/DayOneJournal.py +++ b/jrnl/DayOneJournal.py @@ -9,11 +9,13 @@ import re import time import uuid from xml.parsers.expat import ExpatError +import socket +import platform import pytz import tzlocal -from . import Entry, Journal +from . import __title__, __version__, Entry, Journal from . import time as jrnl_time @@ -71,6 +73,41 @@ class DayOne(Journal.Journal): for tag in dict_entry.get("Tags", []) ] + """Extended DayOne attributes""" + try: + entry.creator_device_agent = dict_entry["Creator"][ + "Device Agent" + ] + except: + pass + try: + entry.creator_generation_date = dict_entry["Creator"][ + "Generation Date" + ] + except: + entry.creator_generation_date = date + try: + entry.creator_host_name = dict_entry["Creator"]["Host Name"] + except: + pass + try: + entry.creator_os_agent = dict_entry["Creator"]["OS Agent"] + except: + pass + try: + entry.creator_software_agent = dict_entry["Creator"][ + "Software Agent" + ] + except: + pass + try: + entry.location = dict_entry["Location"] + except: + pass + try: + entry.weather = dict_entry["Weather"] + except: + pass self.entries.append(entry) self.sort() return self @@ -85,6 +122,20 @@ class DayOne(Journal.Journal): if not hasattr(entry, "uuid"): entry.uuid = uuid.uuid1().hex + if not hasattr(entry, "creator_device_agent"): + entry.creator_device_agent = "" # iPhone/iPhone5,3 + if not hasattr(entry, "creator_generation_date"): + entry.creator_generation_date = utc_time + if not hasattr(entry, "creator_host_name"): + entry.creator_host_name = socket.gethostname() + if not hasattr(entry, "creator_os_agent"): + entry.creator_os_agent = "{}/{}".format( + platform.system(), platform.release() + ) + if not hasattr(entry, "creator_software_agent"): + entry.creator_software_agent = "{}/{}".format( + __title__, __version__ + ) fn = ( Path(self.config["journal"]) @@ -102,10 +153,23 @@ class DayOne(Journal.Journal): tag.strip(self.config["tagsymbols"]).replace("_", " ") for tag in entry.tags ], + "Creator": { + "Device Agent": entry.creator_device_agent, + "Generation Date": entry.creator_generation_date, + "Host Name": entry.creator_host_name, + "OS Agent": entry.creator_os_agent, + "Software Agent": entry.creator_software_agent, + }, } + if hasattr(entry, "location"): + entry_plist["Location"] = entry.location + if hasattr(entry, "weather"): + entry_plist["Weather"] = entry.weather + # plistlib expects a binary object with fn.open(mode="wb") as f: plistlib.dump(entry_plist, f, fmt=plistlib.FMT_XML, sort_keys=False) + for entry in self._deleted_entries: filename = os.path.join( self.config["journal"], "entries", entry.uuid + ".doentry" @@ -147,7 +211,7 @@ class DayOne(Journal.Journal): if line.endswith("*"): current_entry.starred = True line = line[:-1] - current_entry.title = line[len(date_blob) - 1 :] + current_entry.title = line[len(date_blob) - 1 :].strip() current_entry.date = new_date elif current_entry: current_entry.body += line + "\n" @@ -159,10 +223,33 @@ class DayOne(Journal.Journal): # Now, update our current entries if they changed for entry in entries: entry._parse_text() - matched_entries = [e for e in self.entries if e.uuid.lower() == entry.uuid] + matched_entries = [ + e for e in self.entries if e.uuid.lower() == entry.uuid.lower() + ] + # tags in entry body if matched_entries: # This entry is an existing entry match = matched_entries[0] + + # merge existing tags with tags pulled from the entry body + entry.tags = list(set(entry.tags + match.tags)) + + # extended Dayone metadata + if hasattr(match, "creator_device_agent"): + entry.creator_device_agent = match.creator_device_agent + if hasattr(match, "creator_generation_date"): + entry.creator_generation_date = match.creator_generation_date + if hasattr(match, "creator_host_name"): + entry.creator_host_name = match.creator_host_name + if hasattr(match, "creator_os_agent"): + entry.creator_os_agent = match.creator_os_agent + if hasattr(match, "creator_software_agent"): + entry.creator_software_agent = match.creator_software_agent + if hasattr(match, "location"): + entry.location = match.location + if hasattr(match, "weather"): + entry.weather = match.weather + if match != entry: self.entries.remove(match) entry.modified = True diff --git a/jrnl/plugins/json_exporter.py b/jrnl/plugins/json_exporter.py index a5c70b97..8dfe057b 100644 --- a/jrnl/plugins/json_exporter.py +++ b/jrnl/plugins/json_exporter.py @@ -24,6 +24,27 @@ class JSONExporter(TextExporter): } if hasattr(entry, "uuid"): entry_dict["uuid"] = entry.uuid + if ( + hasattr(entry, "creator_device_agent") + or hasattr(entry, "creator_generation_date") + or hasattr(entry, "creator_host_name") + or hasattr(entry, "creator_os_agent") + or hasattr(entry, "creator_software_agent") + ): + entry_dict["creator"] = {} + if hasattr(entry, "creator_device_agent"): + entry_dict["creator"]["device_agent"] = entry.creator_device_agent + if hasattr(entry, "creator_generation_date"): + entry_dict["creator"]["generation_date"] = str( + entry.creator_generation_date + ) + if hasattr(entry, "creator_host_name"): + entry_dict["creator"]["host_name"] = entry.creator_host_name + if hasattr(entry, "creator_os_agent"): + entry_dict["creator"]["os_agent"] = entry.creator_os_agent + if hasattr(entry, "creator_software_agent"): + entry_dict["creator"]["software_agent"] = entry.creator_software_agent + return entry_dict @classmethod From 79aad7c04de3ef85f13221bfe9dfcf8860f09a20 Mon Sep 17 00:00:00 2001 From: "Guy B. deBros" Date: Sat, 6 Jun 2020 15:43:28 -0400 Subject: [PATCH 4/5] Extensive modifications to overview.md (#957) * Extensive modifications to overview.md. I tried to add clarity and details while maintaining the spirit of the original document. However, it might be a bit too 'dry' now. I'd be happy to liven it up a bit. I'm only serious when I feel like I _have_ to be. One of my opinions (which may be at odds with yours) is that the documentation should emphasize `jrnl`'s advantages without downplaying any other existing solutions. If I have time, I'd like to add more information about the documented benefits of journaling, particularly the mental health aspects. That will probably need its own page, but I'm new here, so I don't want to overstay my welcome. :) * More changes to overview.md in response to feedback "why keep a journal" section removed -- it could be re-added as its own page, but it's not front-page material * More changes to overview.md: - fixed up the headings - added information about multi-platform support This is a work in progress and _not_ ready for prime-time. - need to add to Command-Line Interface section, among other things * More changes to overview.md: - moved "`jrnl` is a simple..." to "Command-Line Interface" section --- docs/overview.md | 52 ++++++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index 86211814..67feedcf 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,27 +1,41 @@ # Overview -## What is jrnl? +## Features -`jrnl` is a simple journal application for -your command line. Journals are stored as human readable plain text -files - you can put them into a Dropbox folder for instant syncing and -you can be assured that your journal will still be readable in 2050, -when all your fancy iPad journal applications will long be forgotten. +### Command-Line Interface -Optionally, your journal can be encrypted using the [256-bit -AES](http://en.wikipedia.org/wiki/Advanced_Encryption_Standard). +`jrnl` is a simple but powerful plain text journal application for the command +line. Everything happens on the command line. -## Why keep a journal? +### Text-Based -Journals aren't just for people who have too much -time on their summer vacation. A journal helps you to keep track of the -things you get done and how you did them. Your imagination may be -limitless, but your memory isn't. +`jrnl` stores your journals as human-readable, future-proof plain text files. +You can store them wherever you want, including in shared folders to keep them +synchronized between devices. And because journal files are stored as plain +text, you can rest assured that your journals will be readable for centuries. -For personal use, make it a good habit to write at least 20 words a day. -Just to reflect what made this day special, why you haven't wasted it. +### Support for Multiple Journals + +`jrnl` allows you to work with multiple journals, each of which is stored as a +single file using date and time tags to identify individual entries. `jrnl` +makes it easy to find the entries you want, and only the ones you want, so that +you can read them or edit them. -For professional use, consider a text-based journal to be the perfect -complement to your GTD todo list - a documentation of what and how -you've done it. Or use it as a quick way to keep a change log. Or use it -to keep a lab book. +### Support for External Editors + +`jrnl` allows you to search for specific entries and edit them in your favorite +text editor. + +### Encryption + +`jrnl` includes support for [256-bit AES +encryption](http://en.wikipedia.org/wiki/Advanced_Encryption_Standard) using +[cryptography.io](https://cryptography.io). + +### Multi-Platform Support + +`jrnl` is compatible with most operating systems. Pre-compiled binaries are available through several distribution channels, and you can build from source. See the installation page for more information. + +### Open-Source + +`jrnl` is written in [Python](https://www.python.org) and maintained by a [friendly community](https://github.com/jrnl-org/jrnl) of open-source software enthusiasts. From d756f09e47aa76b114a690a3a893068c2dde6f80 Mon Sep 17 00:00:00 2001 From: Eshan Date: Sat, 6 Jun 2020 15:46:06 -0400 Subject: [PATCH 5/5] Create directory if it doesn't exist (#963) * create dir if it doesn't exist * switch order of print and creation * makedirs instead of mkdir and stderr printing * add test * black reformatting --- features/core.feature | 6 ++++++ jrnl/EncryptedJournal.py | 5 ++++- jrnl/Journal.py | 5 ++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/features/core.feature b/features/core.feature index 3008cfce..ab61eb87 100644 --- a/features/core.feature +++ b/features/core.feature @@ -113,3 +113,9 @@ Feature: Basic reading and writing to a journal 2013-06-10 15:40 Life is good. """ And we should get no error + + Scenario: Journal directory does not exist + Given we use the config "missing_directory.yaml" + When we run "jrnl Life is good" + and we run "jrnl -n 1" + Then the output should contain "Life is good" diff --git a/jrnl/EncryptedJournal.py b/jrnl/EncryptedJournal.py index 2a6df460..ec4ceee8 100644 --- a/jrnl/EncryptedJournal.py +++ b/jrnl/EncryptedJournal.py @@ -40,8 +40,11 @@ class EncryptedJournal(Journal): """Opens the journal file defined in the config and parses it into a list of Entries. Entries have the form (date, title, body).""" filename = filename or self.config["journal"] - + dirname = os.path.dirname(filename) if not os.path.exists(filename): + if not os.path.isdir(dirname): + os.makedirs(dirname) + print(f"[Directory {dirname} created]", file=sys.stderr) self.create_file(filename) self.password = util.create_password(self.name) print( diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 23fa80a0..72b3b9cd 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -74,8 +74,11 @@ class Journal: """Opens the journal file defined in the config and parses it into a list of Entries. Entries have the form (date, title, body).""" filename = filename or self.config["journal"] - + dirname = os.path.dirname(filename) if not os.path.exists(filename): + if not os.path.isdir(dirname): + os.makedirs(dirname) + print(f"[Directory {dirname} created]", file=sys.stderr) self.create_file(filename) print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr)