Merge branch 'master' of https://github.com/maebert/jrnl
|
@ -5,8 +5,8 @@ python:
|
|||
- "3.3"
|
||||
- "3.4"
|
||||
install:
|
||||
- "pip install -e . --use-mirrors"
|
||||
- "pip install pycrypto>=2.6 --use-mirrors"
|
||||
- "pip install -e ."
|
||||
- "pip install pycrypto>=2.6"
|
||||
- "pip install -q behave"
|
||||
# command to run tests
|
||||
script:
|
||||
|
|
|
@ -4,6 +4,12 @@ Changelog
|
|||
|
||||
### 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.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
|
||||
|
|
BIN
docs/_themes/jrnl/static/img/favicon-152.png
vendored
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 12 KiB |
BIN
docs/_themes/jrnl/static/img/favicon.ico
vendored
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
BIN
docs/_themes/jrnl/static/img/logo.png
vendored
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 5.3 KiB |
BIN
docs/_themes/jrnl/static/img/logo@2x.png
vendored
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 14 KiB |
|
@ -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`` ::
|
||||
|
||||
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
|
||||
-----------------
|
||||
|
|
|
@ -6,7 +6,11 @@ Getting started
|
|||
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
|
||||
|
||||
|
|
16
features/data/configs/multiple_without_default.json
Normal 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": "@"
|
||||
}
|
|
@ -34,3 +34,8 @@ Feature: Multiple journals
|
|||
Then journal "ideas" should not exist
|
||||
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
|
||||
|
||||
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."
|
||||
|
|
|
@ -59,3 +59,10 @@ Feature: Zapped bugs should stay dead.
|
|||
2014-04-24 09:00 Ran 6.2 miles today in 1:02:03.
|
||||
| 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"
|
||||
|
|
|
@ -2,9 +2,8 @@ from behave import *
|
|||
from jrnl import cli, Journal, util
|
||||
from dateutil import parser as date_parser
|
||||
import os
|
||||
import sys
|
||||
import codecs
|
||||
import json
|
||||
import pytz
|
||||
import keyring
|
||||
keyring.set_keyring(keyring.backends.file.PlaintextKeyring())
|
||||
try:
|
||||
|
@ -30,7 +29,7 @@ def _parse_args(command):
|
|||
def read_journal(journal_name="default"):
|
||||
with open(cli.CONFIG_PATH) as 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()
|
||||
return journal
|
||||
|
||||
|
@ -57,7 +56,7 @@ def run_with_input(context, command, inputs=None):
|
|||
buffer = StringIO(text.strip())
|
||||
util.STDIN = buffer
|
||||
try:
|
||||
cli.run(args or None)
|
||||
cli.run(args)
|
||||
context.exit_status = 0
|
||||
except SystemExit as e:
|
||||
context.exit_status = e.code
|
||||
|
@ -66,7 +65,7 @@ def run_with_input(context, command, inputs=None):
|
|||
def run(context, command):
|
||||
args = _parse_args(command)
|
||||
try:
|
||||
cli.run(args or None)
|
||||
cli.run(args)
|
||||
context.exit_status = 0
|
||||
except SystemExit as e:
|
||||
context.exit_status = e.code
|
||||
|
@ -124,10 +123,8 @@ def check_output(context, text=None):
|
|||
def check_output_time_inline(context, text):
|
||||
out = context.stdout_capture.getvalue()
|
||||
local_tz = tzlocal.get_localzone()
|
||||
utc_time = date_parser.parse(text)
|
||||
date = utc_time + local_tz._utcoffset
|
||||
local_date = date.strftime("%Y-%m-%d %H:%M")
|
||||
assert local_date in out, local_date
|
||||
local_time = date_parser.parse(text).astimezone(local_tz).strftime("%Y-%m-%d %H:%M")
|
||||
assert local_time in out, local_time
|
||||
|
||||
@then('the output should contain "{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('journal "{journal_name}" should have {number:d} entries')
|
||||
@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)
|
||||
assert len(journal.entries) == number
|
||||
|
||||
|
|
|
@ -31,12 +31,22 @@ Feature: Tagging
|
|||
@c++ : 1
|
||||
@c# : 1
|
||||
"""
|
||||
Scenario: An email should not be a tag
|
||||
Given we use the config "tags-237.json"
|
||||
When we run "jrnl --tags"
|
||||
Then we should get no error
|
||||
and the output should be
|
||||
"""
|
||||
@newline : 1
|
||||
@email : 1
|
||||
"""
|
||||
Scenario: An email should not be a tag
|
||||
Given we use the config "tags-237.json"
|
||||
When we run "jrnl --tags"
|
||||
Then we should get no error
|
||||
and the output should be
|
||||
"""
|
||||
@newline : 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
|
||||
"""
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from . import Entry
|
||||
from . import Journal
|
||||
import os
|
||||
|
@ -65,7 +65,7 @@ class DayOne(Journal.Journal):
|
|||
'Entry Text': entry.title + "\n" + entry.body,
|
||||
'Time Zone': str(tzlocal.get_localzone()),
|
||||
'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)
|
||||
for entry in self._deleted_entries:
|
||||
|
@ -75,7 +75,7 @@ class DayOne(Journal.Journal):
|
|||
def editable_str(self):
|
||||
"""Turns the journal into a string of entries that can be edited
|
||||
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):
|
||||
"""Parses the output of self.editable_str and updates it's entries."""
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import re
|
||||
import textwrap
|
||||
from datetime import datetime
|
||||
|
@ -16,9 +17,15 @@ class Entry:
|
|||
self.starred = starred
|
||||
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):
|
||||
fulltext = " ".join([self.title, self.body]).lower()
|
||||
tags = re.findall(r'(?u)\s([{tags}][-+*#/\w]+)'.format(tags=self.journal.config['tagsymbols']), fulltext, re.UNICODE)
|
||||
fulltext = " " + " ".join([self.title, self.body]).lower()
|
||||
tagsymbols = self.journal.config['tagsymbols']
|
||||
tags = re.findall( Entry.tag_regex(tagsymbols), fulltext )
|
||||
self.tags = tags
|
||||
return set(tags)
|
||||
|
||||
|
@ -28,7 +35,7 @@ class Entry:
|
|||
title = date_str + " " + self.title.rstrip("\n ")
|
||||
if self.starred:
|
||||
title += " *"
|
||||
return u"{title}{sep}{body}\n".format(
|
||||
return "{title}{sep}{body}\n".format(
|
||||
title=title,
|
||||
sep="\n" if self.body.rstrip("\n ") else "",
|
||||
body=self.body.rstrip("\n ")
|
||||
|
@ -58,7 +65,7 @@ class Entry:
|
|||
if short:
|
||||
return title
|
||||
else:
|
||||
return u"{title}{sep}{body}\n".format(
|
||||
return "{title}{sep}{body}\n".format(
|
||||
title=title,
|
||||
sep="\n" if has_body else "",
|
||||
body=body if has_body else "",
|
||||
|
@ -95,7 +102,7 @@ class Entry:
|
|||
space = "\n"
|
||||
md_head = "###"
|
||||
|
||||
return u"{md} {date}, {title} {body} {space}".format(
|
||||
return "{md} {date}, {title} {body} {space}".format(
|
||||
md=md_head,
|
||||
date=date_str,
|
||||
title=self.title,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from . import Entry
|
||||
from . import util
|
||||
from . import time
|
||||
|
@ -165,9 +165,9 @@ class Journal(object):
|
|||
lambda match: util.colorize(match.group(0)),
|
||||
pp, re.UNICODE)
|
||||
else:
|
||||
pp = re.sub(r"(?u)([{tags}]\w+)".format(tags=self.config['tagsymbols']),
|
||||
lambda match: util.colorize(match.group(0)),
|
||||
pp)
|
||||
pp = re.sub( Entry.Entry.tag_regex(self.config['tagsymbols']),
|
||||
lambda match: util.colorize(match.group(0)),
|
||||
pp)
|
||||
return pp
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -176,7 +176,7 @@ class Journal(object):
|
|||
def write(self, filename=None):
|
||||
"""Dumps the journal into the config file, overwriting it"""
|
||||
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']:
|
||||
journal = self._encrypt(journal)
|
||||
with open(filename, 'wb') as journal_file:
|
||||
|
@ -269,7 +269,7 @@ class Journal(object):
|
|||
def editable_str(self):
|
||||
"""Turns the journal into a string of entries that can be edited
|
||||
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):
|
||||
"""Parses the output of self.editable_str and updates it's entries."""
|
||||
|
|
|
@ -8,7 +8,7 @@ jrnl is a simple journal application for your command line.
|
|||
from __future__ import absolute_import
|
||||
|
||||
__title__ = 'jrnl'
|
||||
__version__ = '1.9.2'
|
||||
__version__ = '1.9.8'
|
||||
__author__ = 'Manuel Ebert'
|
||||
__license__ = 'MIT License'
|
||||
__copyright__ = 'Copyright 2013 - 2014 Manuel Ebert'
|
||||
|
|
58
jrnl/cli.py
|
@ -7,7 +7,7 @@
|
|||
license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from . import Journal
|
||||
from . import DayOneJournal
|
||||
from . import util
|
||||
|
@ -17,16 +17,19 @@ import jrnl
|
|||
import os
|
||||
import argparse
|
||||
import sys
|
||||
import logging
|
||||
|
||||
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')
|
||||
PYCRYPTO = install.module_exists("Crypto")
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_args(args=None):
|
||||
parser = argparse.ArgumentParser()
|
||||
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('-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.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)):
|
||||
# Any sign of displaying stuff?
|
||||
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?
|
||||
compose = False
|
||||
|
||||
|
@ -90,6 +93,7 @@ def decrypt(journal, filename=None):
|
|||
def touch_journal(filename):
|
||||
"""If filename does not exist, touch the file"""
|
||||
if not os.path.exists(filename):
|
||||
log.debug('Creating journal file %s', filename)
|
||||
util.prompt("[Journal created at {0}]".format(filename))
|
||||
open(filename, 'a').close()
|
||||
|
||||
|
@ -114,8 +118,15 @@ def update_config(config, new_config, scope, force_local=False):
|
|||
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):
|
||||
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]
|
||||
if args.version:
|
||||
version_str = "{0} version {1}".format(jrnl.__title__, jrnl.__version__)
|
||||
|
@ -123,8 +134,10 @@ def run(manual_args=None):
|
|||
sys.exit(0)
|
||||
|
||||
if not os.path.exists(CONFIG_PATH):
|
||||
log.debug('Configuration file not found, installing jrnl...')
|
||||
config = install.install_jrnl(CONFIG_PATH)
|
||||
else:
|
||||
log.debug('Reading configuration from file %s', CONFIG_PATH)
|
||||
config = util.load_and_fix_json(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)))
|
||||
sys.exit(0)
|
||||
|
||||
log.debug('Using configuration "%s"', config)
|
||||
original_config = config.copy()
|
||||
# check if the configuration is supported by available modules
|
||||
if config['encrypt'] and not PYCRYPTO:
|
||||
|
@ -151,15 +165,34 @@ def run(manual_args=None):
|
|||
except:
|
||||
pass
|
||||
|
||||
log.debug('Using journal "%s"', 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
|
||||
log.debug('Updating configuration with specific jourlnal overrides %s', journal_conf)
|
||||
config.update(journal_conf)
|
||||
else: # But also just give them a string to point to the journal file
|
||||
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']))
|
||||
touch_journal(config['journal'])
|
||||
log.debug('Using journal path %(journal)s', 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?
|
||||
if "win32" in sys.platform:
|
||||
_exit_multiline_code = "on a blank line, press Ctrl+Z and then Enter"
|
||||
|
@ -183,22 +216,12 @@ def run(manual_args=None):
|
|||
else:
|
||||
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
|
||||
if mode_compose:
|
||||
raw = " ".join(args.text).strip()
|
||||
if util.PY2 and type(raw) is not unicode:
|
||||
raw = raw.decode(sys.getfilesystemencoding())
|
||||
log.debug('Appending raw line "%s" to journal "%s"', raw, journal_name)
|
||||
journal.new_entry(raw)
|
||||
util.prompt("[Entry added to {0} journal]".format(journal_name))
|
||||
journal.write()
|
||||
|
@ -233,20 +256,20 @@ def run(manual_args=None):
|
|||
elif args.encrypt is not False:
|
||||
encrypt(journal, filename=args.encrypt)
|
||||
# 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)
|
||||
install.save_config(original_config, config_path=CONFIG_PATH)
|
||||
|
||||
elif args.decrypt is not False:
|
||||
decrypt(journal, filename=args.decrypt)
|
||||
# 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)
|
||||
install.save_config(original_config, config_path=CONFIG_PATH)
|
||||
|
||||
elif args.edit:
|
||||
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)
|
||||
other_entries = [e for e in old_entries if e not in journal.entries]
|
||||
# Edit
|
||||
|
@ -259,10 +282,11 @@ def run(manual_args=None):
|
|||
if num_deleted:
|
||||
prompts.append("{0} {1} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries"))
|
||||
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:
|
||||
util.prompt("[{0}]".format(", ".join(prompts).capitalize()))
|
||||
journal.entries += other_entries
|
||||
journal.sort()
|
||||
journal.write()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import os
|
||||
import json
|
||||
from .util import u, slugify
|
||||
|
@ -29,7 +29,7 @@ def to_tag_list(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(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
|
||||
|
||||
|
||||
|
@ -81,7 +81,7 @@ def export(journal, format, output=None):
|
|||
"markdown": to_md
|
||||
}
|
||||
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
|
||||
return write_files(journal, output, format)
|
||||
else:
|
||||
|
@ -90,9 +90,9 @@ def export(journal, format, output=None):
|
|||
try:
|
||||
with codecs.open(output, "w", "utf-8") as f:
|
||||
f.write(content)
|
||||
return u"[Journal exported to {0}]".format(output)
|
||||
return "[Journal exported to {0}]".format(output)
|
||||
except IOError as e:
|
||||
return u"[ERROR: {0} {1}]".format(e.filename, e.strerror)
|
||||
return "[ERROR: {0} {1}]".format(e.filename, e.strerror)
|
||||
else:
|
||||
return content
|
||||
|
||||
|
@ -111,4 +111,4 @@ def write_files(journal, path, format):
|
|||
content = e.__unicode__()
|
||||
with codecs.open(full_path, "w", "utf-8") as f:
|
||||
f.write(content)
|
||||
return u"[Journal exported individual files in {0}]".format(path)
|
||||
return "[Journal exported individual files in {0}]".format(path)
|
||||
|
|
|
@ -48,7 +48,10 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None):
|
|||
return None
|
||||
|
||||
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:
|
||||
date = datetime(*date[:6])
|
||||
|
||||
|
|
23
jrnl/util.py
|
@ -13,6 +13,7 @@ import tempfile
|
|||
import subprocess
|
||||
import codecs
|
||||
import unicodedata
|
||||
import logging
|
||||
|
||||
PY3 = sys.version_info[0] == 3
|
||||
PY2 = sys.version_info[0] == 2
|
||||
|
@ -22,6 +23,8 @@ STDOUT = sys.stdout
|
|||
TEST = False
|
||||
__cached_tz = None
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def getpass(prompt="Password: "):
|
||||
if not TEST:
|
||||
|
@ -71,16 +74,19 @@ def py2encode(s):
|
|||
|
||||
def prompt(msg):
|
||||
"""Prints a message to the std err stream defined in util."""
|
||||
if not msg:
|
||||
return
|
||||
if not msg.endswith("\n"):
|
||||
msg += "\n"
|
||||
STDERR.write(u(msg))
|
||||
|
||||
def py23_input(msg=""):
|
||||
STDERR.write(u(msg))
|
||||
return STDIN.readline().strip()
|
||||
prompt(msg)
|
||||
return u(STDIN.readline()).strip()
|
||||
|
||||
def py23_read(msg=""):
|
||||
return STDIN.read()
|
||||
prompt(msg)
|
||||
return u(STDIN.read())
|
||||
|
||||
def yesno(prompt, default=True):
|
||||
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:
|
||||
json_str = f.read()
|
||||
config = fixed = None
|
||||
log.debug('Configuration file %s read correctly', json_path)
|
||||
config = None
|
||||
try:
|
||||
return json.loads(json_str)
|
||||
except ValueError as e:
|
||||
log.debug('Could not parse configuration %s: %s', json_str, e,
|
||||
exc_info=True)
|
||||
# Attempt to fix extra ,
|
||||
json_str = re.sub(r",[ \n]*}", "}", json_str)
|
||||
# Attempt to fix missing ,
|
||||
json_str = re.sub(r"([^{,]) *\n *(\")", r"\1,\n \2", json_str)
|
||||
try:
|
||||
log.debug('Attempting to reload automatically fixed configuration file %s',
|
||||
json_str)
|
||||
config = json.loads(json_str)
|
||||
with open(json_path, 'w') as f:
|
||||
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.]")
|
||||
return config
|
||||
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("[Entry was NOT added to your journal]")
|
||||
sys.exit(1)
|
||||
|
||||
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:
|
||||
if template:
|
||||
f.write(template)
|
||||
|
|
102
setup.py
|
@ -51,13 +51,8 @@ except ImportError:
|
|||
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__))
|
||||
|
||||
|
||||
def get_version(filename="jrnl/__init__.py"):
|
||||
with open(os.path.join(base_dir, filename)) as initfile:
|
||||
for line in initfile.readlines():
|
||||
|
@ -65,6 +60,71 @@ def get_version(filename="jrnl/__init__.py"):
|
|||
if m:
|
||||
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 = {
|
||||
"pyreadline>=2.0": not readline_available and "win32" in sys.platform,
|
||||
"readline>=6.2": not readline_available and "win32" not in sys.platform,
|
||||
|
@ -92,24 +152,24 @@ setup(
|
|||
},
|
||||
long_description=__doc__,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'jrnl = jrnl:run',
|
||||
],
|
||||
"console_scripts": [
|
||||
"jrnl = jrnl:run",
|
||||
]
|
||||
},
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Natural Language :: English',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Topic :: Office/Business :: News/Diary',
|
||||
'Topic :: Text Processing'
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Natural Language :: English",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 2.6",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3.3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Topic :: Office/Business :: News/Diary",
|
||||
"Topic :: Text Processing"
|
||||
],
|
||||
# metadata for upload to PyPI
|
||||
author = "Manuel Ebert",
|
||||
|
|