This commit is contained in:
MinchinWeb 2014-12-13 22:10:59 -07:00
commit 35d52bdbee
22 changed files with 245 additions and 88 deletions

View file

@ -5,8 +5,8 @@ python:
- "3.3" - "3.3"
- "3.4" - "3.4"
install: install:
- "pip install -e . --use-mirrors" - "pip install -e ."
- "pip install pycrypto>=2.6 --use-mirrors" - "pip install pycrypto>=2.6"
- "pip install -q behave" - "pip install -q behave"
# command to run tests # command to run tests
script: script:

View file

@ -4,6 +4,12 @@ Changelog
### 1.9 (July 21, 2014) ### 1.9 (July 21, 2014)
* __1.9.8__ Fixes a problem with temporary files on windows
* __1.9.7__ Fixes writing non-ascii entries on the prompt
* __1.9.6__ Fuzzy time parsing improvements (thanks to @pcarranza)
* __1.9.5__ Multi-word tags for DayOne Journals
* __1.9.4__ Fixed: Order of journal entries in file correct after --edit'ing
* __1.9.3__ Fixed: Tags at the beginning of lines
* __1.9.2__ Fixed: Tag search ignores email-addresses (thanks to @mjhoffman65) * __1.9.2__ Fixed: Tag search ignores email-addresses (thanks to @mjhoffman65)
* __1.9.1__ Fixed: Dates in the future can be parsed as well. * __1.9.1__ Fixed: Dates in the future can be parsed as well.
* __1.9.0__ Improved: Greatly improved date parsing. Also added an `-on` option for filtering * __1.9.0__ Improved: Greatly improved date parsing. Also added an `-on` option for filtering

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -30,7 +30,12 @@ A note on security
While jrnl follows best practises, true security is an illusion. Specifically, jrnl will leave traces in your memory and your shell history -- it's meant to keep journals secure in transit, for example when storing it on an `untrusted <http://techcrunch.com/2014/04/09/condoleezza-rice-joins-dropboxs-board/>`_ services such as Dropbox. If you're concerned about security, disable history logging for journal in your ``.bashrc`` :: While jrnl follows best practises, true security is an illusion. Specifically, jrnl will leave traces in your memory and your shell history -- it's meant to keep journals secure in transit, for example when storing it on an `untrusted <http://techcrunch.com/2014/04/09/condoleezza-rice-joins-dropboxs-board/>`_ services such as Dropbox. If you're concerned about security, disable history logging for journal in your ``.bashrc`` ::
HISTINGNORE="jrnl *" HISTIGNORE="jrnl *"
If you are using zsh instead of bash, you can get the same behaviour adding this to your ``zshrc`` ::
setopt HIST_IGNORE_SPACE
alias jrnl=" jrnl"
Manual decryption Manual decryption
----------------- -----------------

View file

@ -6,7 +6,11 @@ Getting started
Installation Installation
------------ ------------
Install *jrnl* using pip :: On OS X, the easiest way to install *jrnl* is using `Homebrew <http://brew.sh/>`_ ::
brew install jrnl
On other platforms, install *jrnl* using pip ::
pip install jrnl pip install jrnl

View file

@ -0,0 +1,16 @@
{
"default_hour": 9,
"timeformat": "%Y-%m-%d %H:%M",
"linewrap": 80,
"encrypt": false,
"editor": "",
"default_minute": 0,
"highlight": true,
"password": "",
"journals": {
"simple": "features/journals/simple.journal",
"work": "features/journals/work.journal",
"ideas": "features/journals/nothing.journal"
},
"tagsymbols": "@"
}

View file

@ -34,3 +34,8 @@ Feature: Multiple journals
Then journal "ideas" should not exist Then journal "ideas" should not exist
When we run "jrnl ideas 23 july 2012: sell my junk on ebay and make lots of money" When we run "jrnl ideas 23 july 2012: sell my junk on ebay and make lots of money"
Then journal "ideas" should have 1 entry Then journal "ideas" should have 1 entry
Scenario: Gracefully handle a config without a default journal
Given we use the config "multiple_without_default.json"
When we run "jrnl fork this repo and fix something"
Then we should see the message "You have not specified a journal. Either provide a default journal in your config file, or specify one of your journals on the command line."

View file

@ -59,3 +59,10 @@ Feature: Zapped bugs should stay dead.
2014-04-24 09:00 Ran 6.2 miles today in 1:02:03. 2014-04-24 09:00 Ran 6.2 miles today in 1:02:03.
| I'm feeling sore because I forgot to stretch. | I'm feeling sore because I forgot to stretch.
""" """
Scenario: Writing an entry at the prompt with non-ascii characters
# https://github.com/maebert/jrnl/issues/295
Given we use the config "basic.json"
When we run "jrnl" and enter "Crème brûlée & Mötorhead"
Then we should get no error
and the journal should contain "Crème brûlée & Mötorhead"

View file

@ -2,9 +2,8 @@ from behave import *
from jrnl import cli, Journal, util from jrnl import cli, Journal, util
from dateutil import parser as date_parser from dateutil import parser as date_parser
import os import os
import sys import codecs
import json import json
import pytz
import keyring import keyring
keyring.set_keyring(keyring.backends.file.PlaintextKeyring()) keyring.set_keyring(keyring.backends.file.PlaintextKeyring())
try: try:
@ -30,7 +29,7 @@ def _parse_args(command):
def read_journal(journal_name="default"): def read_journal(journal_name="default"):
with open(cli.CONFIG_PATH) as config_file: with open(cli.CONFIG_PATH) as config_file:
config = json.load(config_file) config = json.load(config_file)
with open(config['journals'][journal_name]) as journal_file: with codecs.open(config['journals'][journal_name], 'r', 'utf-8') as journal_file:
journal = journal_file.read() journal = journal_file.read()
return journal return journal
@ -57,7 +56,7 @@ def run_with_input(context, command, inputs=None):
buffer = StringIO(text.strip()) buffer = StringIO(text.strip())
util.STDIN = buffer util.STDIN = buffer
try: try:
cli.run(args or None) cli.run(args)
context.exit_status = 0 context.exit_status = 0
except SystemExit as e: except SystemExit as e:
context.exit_status = e.code context.exit_status = e.code
@ -66,7 +65,7 @@ def run_with_input(context, command, inputs=None):
def run(context, command): def run(context, command):
args = _parse_args(command) args = _parse_args(command)
try: try:
cli.run(args or None) cli.run(args)
context.exit_status = 0 context.exit_status = 0
except SystemExit as e: except SystemExit as e:
context.exit_status = e.code context.exit_status = e.code
@ -124,10 +123,8 @@ def check_output(context, text=None):
def check_output_time_inline(context, text): def check_output_time_inline(context, text):
out = context.stdout_capture.getvalue() out = context.stdout_capture.getvalue()
local_tz = tzlocal.get_localzone() local_tz = tzlocal.get_localzone()
utc_time = date_parser.parse(text) local_time = date_parser.parse(text).astimezone(local_tz).strftime("%Y-%m-%d %H:%M")
date = utc_time + local_tz._utcoffset assert local_time in out, local_time
local_date = date.strftime("%Y-%m-%d %H:%M")
assert local_date in out, local_date
@then('the output should contain "{text}"') @then('the output should contain "{text}"')
def check_output_inline(context, text): def check_output_inline(context, text):
@ -186,7 +183,7 @@ def config_var(context, key, value, journal=None):
@then('the journal should have {number:d} entry') @then('the journal should have {number:d} entry')
@then('journal "{journal_name}" should have {number:d} entries') @then('journal "{journal_name}" should have {number:d} entries')
@then('journal "{journal_name}" should have {number:d} entry') @then('journal "{journal_name}" should have {number:d} entry')
def check_journal_content(context, number, journal_name="default"): def check_num_entries(context, number, journal_name="default"):
journal = open_journal(journal_name) journal = open_journal(journal_name)
assert len(journal.entries) == number assert len(journal.entries) == number

View file

@ -31,12 +31,22 @@ Feature: Tagging
@c++ : 1 @c++ : 1
@c# : 1 @c# : 1
""" """
Scenario: An email should not be a tag Scenario: An email should not be a tag
Given we use the config "tags-237.json" Given we use the config "tags-237.json"
When we run "jrnl --tags" When we run "jrnl --tags"
Then we should get no error Then we should get no error
and the output should be and the output should be
""" """
@newline : 1 @newline : 1
@email : 1 @email : 1
""" """
Scenario: Entry cans start and end with tags
Given we use the config "basic.json"
When we run "jrnl today: @foo came over, we went to a @bar"
When we run "jrnl --tags"
Then the output should be
"""
@foo : 1
@bar : 1
"""

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
from __future__ import absolute_import from __future__ import absolute_import, unicode_literals
from . import Entry from . import Entry
from . import Journal from . import Journal
import os import os
@ -65,7 +65,7 @@ class DayOne(Journal.Journal):
'Entry Text': entry.title + "\n" + entry.body, 'Entry Text': entry.title + "\n" + entry.body,
'Time Zone': str(tzlocal.get_localzone()), 'Time Zone': str(tzlocal.get_localzone()),
'UUID': entry.uuid, 'UUID': entry.uuid,
'Tags': [tag.strip(self.config['tagsymbols']) for tag in entry.tags] 'Tags': [tag.strip(self.config['tagsymbols']).replace("_", " ") for tag in entry.tags]
} }
plistlib.writePlist(entry_plist, filename) plistlib.writePlist(entry_plist, filename)
for entry in self._deleted_entries: for entry in self._deleted_entries:
@ -75,7 +75,7 @@ class DayOne(Journal.Journal):
def editable_str(self): def editable_str(self):
"""Turns the journal into a string of entries that can be edited """Turns the journal into a string of entries that can be edited
manually and later be parsed with eslf.parse_editable_str.""" manually and later be parsed with eslf.parse_editable_str."""
return u"\n".join([u"# {0}\n{1}".format(e.uuid, e.__unicode__()) for e in self.entries]) return "\n".join(["# {0}\n{1}".format(e.uuid, e.__unicode__()) for e in self.entries])
def parse_editable_str(self, edited): def parse_editable_str(self, edited):
"""Parses the output of self.editable_str and updates it's entries.""" """Parses the output of self.editable_str and updates it's entries."""

View file

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
from __future__ import unicode_literals
import re import re
import textwrap import textwrap
from datetime import datetime from datetime import datetime
@ -16,9 +17,15 @@ class Entry:
self.starred = starred self.starred = starred
self.modified = False self.modified = False
@staticmethod
def tag_regex(tagsymbols):
pattern = r'(?u)\s([{tags}][-+*#/\w]+)'.format(tags=tagsymbols)
return re.compile( pattern, re.UNICODE )
def parse_tags(self): def parse_tags(self):
fulltext = " ".join([self.title, self.body]).lower() fulltext = " " + " ".join([self.title, self.body]).lower()
tags = re.findall(r'(?u)\s([{tags}][-+*#/\w]+)'.format(tags=self.journal.config['tagsymbols']), fulltext, re.UNICODE) tagsymbols = self.journal.config['tagsymbols']
tags = re.findall( Entry.tag_regex(tagsymbols), fulltext )
self.tags = tags self.tags = tags
return set(tags) return set(tags)
@ -28,7 +35,7 @@ class Entry:
title = date_str + " " + self.title.rstrip("\n ") title = date_str + " " + self.title.rstrip("\n ")
if self.starred: if self.starred:
title += " *" title += " *"
return u"{title}{sep}{body}\n".format( return "{title}{sep}{body}\n".format(
title=title, title=title,
sep="\n" if self.body.rstrip("\n ") else "", sep="\n" if self.body.rstrip("\n ") else "",
body=self.body.rstrip("\n ") body=self.body.rstrip("\n ")
@ -58,7 +65,7 @@ class Entry:
if short: if short:
return title return title
else: else:
return u"{title}{sep}{body}\n".format( return "{title}{sep}{body}\n".format(
title=title, title=title,
sep="\n" if has_body else "", sep="\n" if has_body else "",
body=body if has_body else "", body=body if has_body else "",
@ -95,7 +102,7 @@ class Entry:
space = "\n" space = "\n"
md_head = "###" md_head = "###"
return u"{md} {date}, {title} {body} {space}".format( return "{md} {date}, {title} {body} {space}".format(
md=md_head, md=md_head,
date=date_str, date=date_str,
title=self.title, title=self.title,

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
from __future__ import absolute_import from __future__ import absolute_import, unicode_literals
from . import Entry from . import Entry
from . import util from . import util
from . import time from . import time
@ -165,9 +165,9 @@ class Journal(object):
lambda match: util.colorize(match.group(0)), lambda match: util.colorize(match.group(0)),
pp, re.UNICODE) pp, re.UNICODE)
else: else:
pp = re.sub(r"(?u)([{tags}]\w+)".format(tags=self.config['tagsymbols']), pp = re.sub( Entry.Entry.tag_regex(self.config['tagsymbols']),
lambda match: util.colorize(match.group(0)), lambda match: util.colorize(match.group(0)),
pp) pp)
return pp return pp
def __repr__(self): def __repr__(self):
@ -176,7 +176,7 @@ class Journal(object):
def write(self, filename=None): def write(self, filename=None):
"""Dumps the journal into the config file, overwriting it""" """Dumps the journal into the config file, overwriting it"""
filename = filename or self.config['journal'] filename = filename or self.config['journal']
journal = u"\n".join([e.__unicode__() for e in self.entries]) journal = "\n".join([e.__unicode__() for e in self.entries])
if self.config['encrypt']: if self.config['encrypt']:
journal = self._encrypt(journal) journal = self._encrypt(journal)
with open(filename, 'wb') as journal_file: with open(filename, 'wb') as journal_file:
@ -269,7 +269,7 @@ class Journal(object):
def editable_str(self): def editable_str(self):
"""Turns the journal into a string of entries that can be edited """Turns the journal into a string of entries that can be edited
manually and later be parsed with eslf.parse_editable_str.""" manually and later be parsed with eslf.parse_editable_str."""
return u"\n".join([e.__unicode__() for e in self.entries]) return "\n".join([e.__unicode__() for e in self.entries])
def parse_editable_str(self, edited): def parse_editable_str(self, edited):
"""Parses the output of self.editable_str and updates it's entries.""" """Parses the output of self.editable_str and updates it's entries."""

View file

@ -8,7 +8,7 @@ jrnl is a simple journal application for your command line.
from __future__ import absolute_import from __future__ import absolute_import
__title__ = 'jrnl' __title__ = 'jrnl'
__version__ = '1.9.2' __version__ = '1.9.8'
__author__ = 'Manuel Ebert' __author__ = 'Manuel Ebert'
__license__ = 'MIT License' __license__ = 'MIT License'
__copyright__ = 'Copyright 2013 - 2014 Manuel Ebert' __copyright__ = 'Copyright 2013 - 2014 Manuel Ebert'

View file

@ -7,7 +7,7 @@
license: MIT, see LICENSE for more details. license: MIT, see LICENSE for more details.
""" """
from __future__ import absolute_import from __future__ import absolute_import, unicode_literals
from . import Journal from . import Journal
from . import DayOneJournal from . import DayOneJournal
from . import util from . import util
@ -17,16 +17,19 @@ import jrnl
import os import os
import argparse import argparse
import sys import sys
import logging
xdg_config = os.environ.get('XDG_CONFIG_HOME') xdg_config = os.environ.get('XDG_CONFIG_HOME')
CONFIG_PATH = os.path.join(xdg_config, "jrnl") if xdg_config else os.path.expanduser('~/.jrnl_config') CONFIG_PATH = os.path.join(xdg_config, "jrnl") if xdg_config else os.path.expanduser('~/.jrnl_config')
PYCRYPTO = install.module_exists("Crypto") PYCRYPTO = install.module_exists("Crypto")
log = logging.getLogger(__name__)
def parse_args(args=None): def parse_args(args=None):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('-v', '--version', dest='version', action="store_true", help="prints version information and exits") parser.add_argument('-v', '--version', dest='version', action="store_true", help="prints version information and exits")
parser.add_argument('-ls', dest='ls', action="store_true", help="displays accessible journals") parser.add_argument('-ls', dest='ls', action="store_true", help="displays accessible journals")
parser.add_argument('-d', '--debug', dest='debug', action='store_true', help='execute in debug mode')
composing = parser.add_argument_group('Composing', 'To write an entry simply write it on the command line, e.g. "jrnl yesterday at 1pm: Went to the gym."') composing = parser.add_argument_group('Composing', 'To write an entry simply write it on the command line, e.g. "jrnl yesterday at 1pm: Went to the gym."')
composing.add_argument('text', metavar='', nargs="*") composing.add_argument('text', metavar='', nargs="*")
@ -61,7 +64,7 @@ def guess_mode(args, config):
elif any((args.start_date, args.end_date, args.on_date, args.limit, args.strict, args.starred)): elif any((args.start_date, args.end_date, args.on_date, args.limit, args.strict, args.starred)):
# Any sign of displaying stuff? # Any sign of displaying stuff?
compose = False compose = False
elif args.text and all(word[0] in config['tagsymbols'] for word in u" ".join(args.text).split()): elif args.text and all(word[0] in config['tagsymbols'] for word in " ".join(args.text).split()):
# No date and only tags? # No date and only tags?
compose = False compose = False
@ -90,6 +93,7 @@ def decrypt(journal, filename=None):
def touch_journal(filename): def touch_journal(filename):
"""If filename does not exist, touch the file""" """If filename does not exist, touch the file"""
if not os.path.exists(filename): if not os.path.exists(filename):
log.debug('Creating journal file %s', filename)
util.prompt("[Journal created at {0}]".format(filename)) util.prompt("[Journal created at {0}]".format(filename))
open(filename, 'a').close() open(filename, 'a').close()
@ -114,8 +118,15 @@ def update_config(config, new_config, scope, force_local=False):
config.update(new_config) config.update(new_config)
def configure_logger(debug=False):
logging.basicConfig(level=logging.DEBUG if debug else logging.INFO,
format='%(levelname)-8s %(name)-12s %(message)s')
logging.getLogger('parsedatetime').setLevel(logging.INFO) # disable parsedatetime debug logging
def run(manual_args=None): def run(manual_args=None):
args = parse_args(manual_args) args = parse_args(manual_args)
configure_logger(args.debug)
args.text = [p.decode('utf-8') if util.PY2 and not isinstance(p, unicode) else p for p in args.text] args.text = [p.decode('utf-8') if util.PY2 and not isinstance(p, unicode) else p for p in args.text]
if args.version: if args.version:
version_str = "{0} version {1}".format(jrnl.__title__, jrnl.__version__) version_str = "{0} version {1}".format(jrnl.__title__, jrnl.__version__)
@ -123,8 +134,10 @@ def run(manual_args=None):
sys.exit(0) sys.exit(0)
if not os.path.exists(CONFIG_PATH): if not os.path.exists(CONFIG_PATH):
log.debug('Configuration file not found, installing jrnl...')
config = install.install_jrnl(CONFIG_PATH) config = install.install_jrnl(CONFIG_PATH)
else: else:
log.debug('Reading configuration from file %s', CONFIG_PATH)
config = util.load_and_fix_json(CONFIG_PATH) config = util.load_and_fix_json(CONFIG_PATH)
install.upgrade_config(config, config_path=CONFIG_PATH) install.upgrade_config(config, config_path=CONFIG_PATH)
@ -132,6 +145,7 @@ def run(manual_args=None):
print(util.py2encode(list_journals(config))) print(util.py2encode(list_journals(config)))
sys.exit(0) sys.exit(0)
log.debug('Using configuration "%s"', config)
original_config = config.copy() original_config = config.copy()
# check if the configuration is supported by available modules # check if the configuration is supported by available modules
if config['encrypt'] and not PYCRYPTO: if config['encrypt'] and not PYCRYPTO:
@ -151,15 +165,34 @@ def run(manual_args=None):
except: except:
pass pass
log.debug('Using journal "%s"', journal_name)
journal_conf = config['journals'].get(journal_name) journal_conf = config['journals'].get(journal_name)
if type(journal_conf) is dict: # We can override the default config on a by-journal basis if type(journal_conf) is dict: # We can override the default config on a by-journal basis
log.debug('Updating configuration with specific jourlnal overrides %s', journal_conf)
config.update(journal_conf) config.update(journal_conf)
else: # But also just give them a string to point to the journal file else: # But also just give them a string to point to the journal file
config['journal'] = journal_conf config['journal'] = journal_conf
if config['journal'] is None:
util.prompt("You have not specified a journal. Either provide a default journal in your config file, or specify one of your journals on the command line.")
sys.exit(1)
config['journal'] = os.path.expanduser(os.path.expandvars(config['journal'])) config['journal'] = os.path.expanduser(os.path.expandvars(config['journal']))
touch_journal(config['journal']) touch_journal(config['journal'])
log.debug('Using journal path %(journal)s', config)
mode_compose, mode_export = guess_mode(args, config) mode_compose, mode_export = guess_mode(args, config)
# open journal file or folder
if os.path.isdir(config['journal']):
if config['journal'].strip("/").endswith(".dayone") or \
"entries" in os.listdir(config['journal']):
journal = DayOneJournal.DayOne(**config)
else:
util.prompt("[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal']))
sys.exit(1)
else:
journal = Journal.Journal(journal_name, **config)
# How to quit writing? # How to quit writing?
if "win32" in sys.platform: if "win32" in sys.platform:
_exit_multiline_code = "on a blank line, press Ctrl+Z and then Enter" _exit_multiline_code = "on a blank line, press Ctrl+Z and then Enter"
@ -183,22 +216,12 @@ def run(manual_args=None):
else: else:
mode_compose = False mode_compose = False
# open journal file or folder
if os.path.isdir(config['journal']):
if config['journal'].strip("/").endswith(".dayone") or \
"entries" in os.listdir(config['journal']):
journal = DayOneJournal.DayOne(**config)
else:
util.prompt(u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal']))
sys.exit(1)
else:
journal = Journal.Journal(journal_name, **config)
# Writing mode # Writing mode
if mode_compose: if mode_compose:
raw = " ".join(args.text).strip() raw = " ".join(args.text).strip()
if util.PY2 and type(raw) is not unicode: if util.PY2 and type(raw) is not unicode:
raw = raw.decode(sys.getfilesystemencoding()) raw = raw.decode(sys.getfilesystemencoding())
log.debug('Appending raw line "%s" to journal "%s"', raw, journal_name)
journal.new_entry(raw) journal.new_entry(raw)
util.prompt("[Entry added to {0} journal]".format(journal_name)) util.prompt("[Entry added to {0} journal]".format(journal_name))
journal.write() journal.write()
@ -233,20 +256,20 @@ def run(manual_args=None):
elif args.encrypt is not False: elif args.encrypt is not False:
encrypt(journal, filename=args.encrypt) encrypt(journal, filename=args.encrypt)
# Not encrypting to a separate file: update config! # Not encrypting to a separate file: update config!
if not args.encrypt: if not args.encrypt or args.encrypt == config['journal']:
update_config(original_config, {"encrypt": True}, journal_name, force_local=True) update_config(original_config, {"encrypt": True}, journal_name, force_local=True)
install.save_config(original_config, config_path=CONFIG_PATH) install.save_config(original_config, config_path=CONFIG_PATH)
elif args.decrypt is not False: elif args.decrypt is not False:
decrypt(journal, filename=args.decrypt) decrypt(journal, filename=args.decrypt)
# Not decrypting to a separate file: update config! # Not decrypting to a separate file: update config!
if not args.decrypt: if not args.decrypt or args.decrypt == config['journal']:
update_config(original_config, {"encrypt": False}, journal_name, force_local=True) update_config(original_config, {"encrypt": False}, journal_name, force_local=True)
install.save_config(original_config, config_path=CONFIG_PATH) install.save_config(original_config, config_path=CONFIG_PATH)
elif args.edit: elif args.edit:
if not config['editor']: if not config['editor']:
util.prompt(u"[You need to specify an editor in {0} to use the --edit function.]".format(CONFIG_PATH)) util.prompt("[You need to specify an editor in {0} to use the --edit function.]".format(CONFIG_PATH))
sys.exit(1) sys.exit(1)
other_entries = [e for e in old_entries if e not in journal.entries] other_entries = [e for e in old_entries if e not in journal.entries]
# Edit # Edit
@ -259,10 +282,11 @@ def run(manual_args=None):
if num_deleted: if num_deleted:
prompts.append("{0} {1} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries")) prompts.append("{0} {1} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries"))
if num_edited: if num_edited:
prompts.append("{0} {1} modified".format(num_edited, "entry" if num_deleted == 1 else "entries")) prompts.append("{0} {1} modified".format(num_edited, "entry" if num_edited == 1 else "entries"))
if prompts: if prompts:
util.prompt("[{0}]".format(", ".join(prompts).capitalize())) util.prompt("[{0}]".format(", ".join(prompts).capitalize()))
journal.entries += other_entries journal.entries += other_entries
journal.sort()
journal.write() journal.write()
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
from __future__ import absolute_import from __future__ import absolute_import, unicode_literals
import os import os
import json import json
from .util import u, slugify from .util import u, slugify
@ -29,7 +29,7 @@ def to_tag_list(journal):
elif min(tag_counts)[0] == 0: elif min(tag_counts)[0] == 0:
tag_counts = filter(lambda x: x[0] > 1, tag_counts) tag_counts = filter(lambda x: x[0] > 1, tag_counts)
result += '[Removed tags that appear only once.]\n' result += '[Removed tags that appear only once.]\n'
result += "\n".join(u"{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True)) result += "\n".join("{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True))
return result return result
@ -81,7 +81,7 @@ def export(journal, format, output=None):
"markdown": to_md "markdown": to_md
} }
if format not in maps: if format not in maps:
return u"[ERROR: can't export to '{0}'. Valid options are 'md', 'txt', and 'json']".format(format) return "[ERROR: can't export to '{0}'. Valid options are 'md', 'txt', and 'json']".format(format)
if output and os.path.isdir(output): # multiple files if output and os.path.isdir(output): # multiple files
return write_files(journal, output, format) return write_files(journal, output, format)
else: else:
@ -90,9 +90,9 @@ def export(journal, format, output=None):
try: try:
with codecs.open(output, "w", "utf-8") as f: with codecs.open(output, "w", "utf-8") as f:
f.write(content) f.write(content)
return u"[Journal exported to {0}]".format(output) return "[Journal exported to {0}]".format(output)
except IOError as e: except IOError as e:
return u"[ERROR: {0} {1}]".format(e.filename, e.strerror) return "[ERROR: {0} {1}]".format(e.filename, e.strerror)
else: else:
return content return content
@ -111,4 +111,4 @@ def write_files(journal, path, format):
content = e.__unicode__() content = e.__unicode__()
with codecs.open(full_path, "w", "utf-8") as f: with codecs.open(full_path, "w", "utf-8") as f:
f.write(content) f.write(content)
return u"[Journal exported individual files in {0}]".format(path) return "[Journal exported individual files in {0}]".format(path)

View file

@ -48,7 +48,10 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None):
return None return None
if flag is 1: # Date found, but no time. Use the default time. if flag is 1: # Date found, but no time. Use the default time.
date = datetime(*date[:3], hour=default_hour or 0, minute=default_minute or 0) date = datetime(*date[:3],
hour=23 if inclusive else default_hour or 0,
minute=59 if inclusive else default_minute or 0,
second=59 if inclusive else 0)
else: else:
date = datetime(*date[:6]) date = datetime(*date[:6])

View file

@ -13,6 +13,7 @@ import tempfile
import subprocess import subprocess
import codecs import codecs
import unicodedata import unicodedata
import logging
PY3 = sys.version_info[0] == 3 PY3 = sys.version_info[0] == 3
PY2 = sys.version_info[0] == 2 PY2 = sys.version_info[0] == 2
@ -22,6 +23,8 @@ STDOUT = sys.stdout
TEST = False TEST = False
__cached_tz = None __cached_tz = None
log = logging.getLogger(__name__)
def getpass(prompt="Password: "): def getpass(prompt="Password: "):
if not TEST: if not TEST:
@ -71,16 +74,19 @@ def py2encode(s):
def prompt(msg): def prompt(msg):
"""Prints a message to the std err stream defined in util.""" """Prints a message to the std err stream defined in util."""
if not msg:
return
if not msg.endswith("\n"): if not msg.endswith("\n"):
msg += "\n" msg += "\n"
STDERR.write(u(msg)) STDERR.write(u(msg))
def py23_input(msg=""): def py23_input(msg=""):
STDERR.write(u(msg)) prompt(msg)
return STDIN.readline().strip() return u(STDIN.readline()).strip()
def py23_read(msg=""): def py23_read(msg=""):
return STDIN.read() prompt(msg)
return u(STDIN.read())
def yesno(prompt, default=True): def yesno(prompt, default=True):
prompt = prompt.strip() + (" [Y/n]" if default else " [y/N]") prompt = prompt.strip() + (" [Y/n]" if default else " [y/N]")
@ -93,27 +99,34 @@ def load_and_fix_json(json_path):
""" """
with open(json_path) as f: with open(json_path) as f:
json_str = f.read() json_str = f.read()
config = fixed = None log.debug('Configuration file %s read correctly', json_path)
config = None
try: try:
return json.loads(json_str) return json.loads(json_str)
except ValueError as e: except ValueError as e:
log.debug('Could not parse configuration %s: %s', json_str, e,
exc_info=True)
# Attempt to fix extra , # Attempt to fix extra ,
json_str = re.sub(r",[ \n]*}", "}", json_str) json_str = re.sub(r",[ \n]*}", "}", json_str)
# Attempt to fix missing , # Attempt to fix missing ,
json_str = re.sub(r"([^{,]) *\n *(\")", r"\1,\n \2", json_str) json_str = re.sub(r"([^{,]) *\n *(\")", r"\1,\n \2", json_str)
try: try:
log.debug('Attempting to reload automatically fixed configuration file %s',
json_str)
config = json.loads(json_str) config = json.loads(json_str)
with open(json_path, 'w') as f: with open(json_path, 'w') as f:
json.dump(config, f, indent=2) json.dump(config, f, indent=2)
log.debug('Fixed configuration saved in file %s', json_path)
prompt("[Some errors in your jrnl config have been fixed for you.]") prompt("[Some errors in your jrnl config have been fixed for you.]")
return config return config
except ValueError as e: except ValueError as e:
log.debug('Could not load fixed configuration: %s', e, exc_info=True)
prompt("[There seems to be something wrong with your jrnl config at {0}: {1}]".format(json_path, e.message)) prompt("[There seems to be something wrong with your jrnl config at {0}: {1}]".format(json_path, e.message))
prompt("[Entry was NOT added to your journal]") prompt("[Entry was NOT added to your journal]")
sys.exit(1) sys.exit(1)
def get_text_from_editor(config, template=""): def get_text_from_editor(config, template=""):
tmpfile = os.path.join(tempfile.mktemp(prefix="jrnl")) _, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt")
with codecs.open(tmpfile, 'w', "utf-8") as f: with codecs.open(tmpfile, 'w', "utf-8") as f:
if template: if template:
f.write(template) f.write(template)

102
setup.py
View file

@ -51,13 +51,8 @@ except ImportError:
readline_available = False readline_available = False
if sys.argv[-1] == 'publish':
os.system("python setup.py sdist upload")
sys.exit()
base_dir = os.path.dirname(os.path.abspath(__file__)) base_dir = os.path.dirname(os.path.abspath(__file__))
def get_version(filename="jrnl/__init__.py"): def get_version(filename="jrnl/__init__.py"):
with open(os.path.join(base_dir, filename)) as initfile: with open(os.path.join(base_dir, filename)) as initfile:
for line in initfile.readlines(): for line in initfile.readlines():
@ -65,6 +60,71 @@ def get_version(filename="jrnl/__init__.py"):
if m: if m:
return m.group(1) return m.group(1)
def get_changelog(filename="CHANGELOG.md"):
changelog = {}
current_version = None
with open(os.path.join(base_dir, filename)) as changelog_file:
for line in changelog_file.readlines():
if line.startswith("* __"):
parts = line.strip("* ").split(" ", 1)
if len(parts) == 2:
current_version, changes = parts[0].strip("_\n"), parts[1]
changelog[current_version] = [changes.strip()]
else:
current_version = parts[0].strip("_\n")
changelog[current_version] = []
elif line.strip() and current_version and not line.startswith("#"):
changelog[current_version].append(line.strip(" *\n"))
return changelog
def dist_pypi():
os.system("python setup.py sdist upload")
sys.exit()
def dist_github():
"""Creates a release on the maebert/jrnl repository on github"""
import requests
import keyring
import getpass
version = get_version()
version_tuple = version.split(".")
changes_since_last_version = ["* __{}__: {}".format(key, "\n".join(changes)) for key, changes in get_changelog().items() if key.startswith("{}.{}".format(*version_tuple))]
changes_since_last_version = "\n".join(sorted(changes_since_last_version, reverse=True))
payload = {
"tag_name": version,
"target_commitish": "master",
"name": version,
"body": "Changes in Version {}.{}: \n\n{}".format(version_tuple[0], version_tuple[1], changes_since_last_version)
}
print("Preparing release {}...".format(version))
username = keyring.get_password("github", "__default_user") or raw_input("Github username: ")
password = keyring.get_password("github", username) or getpass.getpass()
otp = raw_input("One Time Token: ")
response = requests.post("https://api.github.com/repos/maebert/jrnl/releases", headers={"X-GitHub-OTP": otp}, json=payload, auth=(username, password))
if response.status_code in (403, 404):
print("Authentication error.")
else:
keyring.set_password("github", "__default_user", username)
keyring.set_password("github", username, password)
if response.status_code > 299:
if "message" in response.json():
print("Error: {}".format(response.json()['message']))
for error_dict in response.json().get('errors', []):
print("*", error_dict)
else:
print("Unkown error")
print(response.text)
else:
print("Release created.")
sys.exit()
if sys.argv[-1] == 'publish':
dist_pypi()
if sys.argv[-1] == 'github_release':
dist_github()
conditional_dependencies = { conditional_dependencies = {
"pyreadline>=2.0": not readline_available and "win32" in sys.platform, "pyreadline>=2.0": not readline_available and "win32" in sys.platform,
"readline>=6.2": not readline_available and "win32" not in sys.platform, "readline>=6.2": not readline_available and "win32" not in sys.platform,
@ -92,24 +152,24 @@ setup(
}, },
long_description=__doc__, long_description=__doc__,
entry_points={ entry_points={
'console_scripts': [ "console_scripts": [
'jrnl = jrnl:run', "jrnl = jrnl:run",
], ]
}, },
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', "Development Status :: 5 - Production/Stable",
'Environment :: Console', "Environment :: Console",
'Intended Audience :: End Users/Desktop', "Intended Audience :: End Users/Desktop",
'License :: OSI Approved :: MIT License', "License :: OSI Approved :: MIT License",
'Natural Language :: English', "Natural Language :: English",
'Operating System :: OS Independent', "Operating System :: OS Independent",
'Programming Language :: Python', "Programming Language :: Python",
'Programming Language :: Python :: 2.6', "Programming Language :: Python :: 2.6",
'Programming Language :: Python :: 2.7', "Programming Language :: Python :: 2.7",
'Programming Language :: Python :: 3.3', "Programming Language :: Python :: 3.3",
'Programming Language :: Python :: 3.4', "Programming Language :: Python :: 3.4",
'Topic :: Office/Business :: News/Diary', "Topic :: Office/Business :: News/Diary",
'Topic :: Text Processing' "Topic :: Text Processing"
], ],
# metadata for upload to PyPI # metadata for upload to PyPI
author = "Manuel Ebert", author = "Manuel Ebert",