Merge branch 'develop' into change-cwd-bug

This commit is contained in:
Jonathan Wren 2020-06-06 12:53:41 -07:00 committed by GitHub
commit 15ad02dc85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 261 additions and 35 deletions

1
.gitignore vendored
View file

@ -43,6 +43,7 @@ obj
env/
env*/
venv*/
.venv*/
# PyCharm Project files
.idea/

View file

@ -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)

View file

@ -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.

View file

@ -114,6 +114,12 @@ Feature: Basic reading and writing to a journal
"""
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"
Scenario: Installation with relative journal and referencing from another folder
Given we use the config "missingconfig"
When we run "jrnl hello world" and enter

View file

@ -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"

View file

@ -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

View file

@ -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())
@ -213,6 +229,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

View file

@ -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")

View file

@ -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

View file

@ -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(

View file

@ -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)

View file

@ -1 +1 @@
__version__ = "v2.4.2"
__version__ = "v2.4.3-beta"

View file

@ -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

View file

@ -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):

View file

@ -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 <manuel@1450.me>",