Merge branch 'master' of https://github.com/maebert/jrnl
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
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`` ::
|
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
|
||||||
-----------------
|
-----------------
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
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
|
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."
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -40,3 +40,13 @@ Feature: Tagging
|
||||||
@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
|
||||||
|
"""
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,7 +165,7 @@ 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
|
||||||
|
@ -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."""
|
||||||
|
|
|
@ -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'
|
||||||
|
|
58
jrnl/cli.py
|
@ -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__":
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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])
|
||||||
|
|
||||||
|
|
23
jrnl/util.py
|
@ -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
|
@ -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",
|
||||||
|
|