mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-20 21:18:32 +02:00
Merge branch 'develop' into interactive_delete
This commit is contained in:
commit
972a00f478
58 changed files with 1910 additions and 840 deletions
64
.build/generate_changelog.sh
Executable file
64
.build/generate_changelog.sh
Executable file
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
BRANCH=$TRAVIS_BRANCH
|
||||
if [[ $TRAVIS_BRANCH == $TRAVIS_TAG ]]; then
|
||||
BRANCH='master'
|
||||
fi
|
||||
|
||||
# Check if branch has been updated since this build started
|
||||
# This tends to happen if multiple things have been merged in at the same time.
|
||||
if [[ -z $TRAVIS_TAG ]]; then
|
||||
git fetch origin
|
||||
if [[ $(git rev-parse "origin/${BRANCH}") != $TRAVIS_COMMIT ]]; then
|
||||
echo "${BRANCH} has been updated since build started. Aborting changelog."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
FILENAME='CHANGELOG.md'
|
||||
|
||||
# get the latest git tags
|
||||
releases="$(git tag --sort=-creatordate | grep -Ev '(alpha|beta|rc)')"
|
||||
release_latest=$(printf '%s' "$releases" | awk 'NR==1')
|
||||
release_secondlatest=$(printf '%s' "$releases" | awk 'NR==2')
|
||||
|
||||
echo "release_latest: ${release_latest}"
|
||||
echo "release_secondlatest: ${release_secondlatest}"
|
||||
|
||||
# delete generated line (or it will be added multiple times)
|
||||
sed -i '/This Changelog was automatically generated by/d' "$FILENAME"
|
||||
|
||||
# delete trailing empty lines
|
||||
sed -i -e :a -e '/^\n*$/{$d;N;};/\n$/ba' "$FILENAME"
|
||||
|
||||
# determine correct tag to go back to
|
||||
if [[ $TRAVIS_TAG == $release_latest ]]; then
|
||||
echo "release build"
|
||||
gittag=${release_secondlatest}
|
||||
elif [[ ! -z $TRAVIS_TAG ]]; then
|
||||
echo "beta elease"
|
||||
gittag=${release_latest}
|
||||
else
|
||||
echo "merge into master or develop"
|
||||
gittag=${release_latest}
|
||||
fi
|
||||
echo "gittag: ${gittag}"
|
||||
|
||||
# find the line the tag starts on, and subtract 1
|
||||
tagline=$(grep -n "^## \[\?$gittag\]\?" "$FILENAME" | awk '{print $1}' FS=':' | head -1)
|
||||
echo "tagline: ${tagline}"
|
||||
[[ ! -z $tagline ]] && sed -i "1,$(expr $tagline - 1)d" "$FILENAME"
|
||||
|
||||
# generate the changelog
|
||||
docker run -it --rm -v "$(pwd)":/usr/local/src/your-app ferrarimarco/github-changelog-generator -t $GITHUB_TOKEN --since-tag $gittag
|
||||
|
||||
# Put back our link (instead of the broken one)
|
||||
sed -i 's!https://pypi.org/project/jrnl/HEAD/!https://github.com/jrnl-org/jrnl/!' "$FILENAME"
|
||||
|
||||
git config --global user.email "jrnl.bot@gmail.com"
|
||||
git config --global user.name "Jrnl Bot"
|
||||
git checkout $BRANCH
|
||||
git add "$FILENAME"
|
||||
git commit -m "Updating changelog [ci skip]"
|
||||
git push https://${GITHUB_TOKEN}@github.com/jrnl-org/jrnl.git $BRANCH
|
||||
|
11
.github_changelog_generator
Normal file
11
.github_changelog_generator
Normal file
|
@ -0,0 +1,11 @@
|
|||
project=jrnl
|
||||
user=jrnl-org
|
||||
base=CHANGELOG.md
|
||||
issues=false
|
||||
issues-wo-labels=false
|
||||
include-labels=bug,enhancement,documentation,build,deprecated
|
||||
release-url=https://pypi.org/project/jrnl/%s/
|
||||
add-sections={ "build": { "prefix": "**Build:**", "labels": ["build"]}, "docs": { "prefix": "**Updated documentation:**", "labels": ["documentation"]}}
|
||||
exclude-tags-regex=(alpha|beta|rc)
|
||||
verbose=false
|
||||
|
165
.travis.yml
165
.travis.yml
|
@ -1,34 +1,149 @@
|
|||
dist: xenial # required for Python >= 3.7
|
||||
os: linux
|
||||
language: python
|
||||
python:
|
||||
- 3.6
|
||||
- 3.7
|
||||
|
||||
cache:
|
||||
- pip
|
||||
|
||||
git:
|
||||
depth: false
|
||||
autocrlf: false
|
||||
|
||||
before_install:
|
||||
- pip install poetry~=0.12.17
|
||||
- date
|
||||
|
||||
install:
|
||||
# we run `poetry version` here to appease poetry about '0.0.0-source'
|
||||
- poetry version
|
||||
- pip install poetry
|
||||
- poetry install
|
||||
script:
|
||||
- poetry run python --version
|
||||
|
||||
script:
|
||||
- poetry run behave
|
||||
before_deploy:
|
||||
- poetry config http-basic.pypi $PYPI_USER $PYPI_PASS
|
||||
- poetry version $TRAVIS_TAG
|
||||
- poetry build
|
||||
deploy:
|
||||
- provider: script
|
||||
script: poetry publish
|
||||
skip_cleanup: true
|
||||
on:
|
||||
branch: master
|
||||
tags: true
|
||||
after_deploy:
|
||||
- git config --global user.email "jrnl.bot@gmail.com"
|
||||
- git config --global user.name "Jrnl Bot"
|
||||
- git checkout master
|
||||
- git add pyproject.toml
|
||||
- git commit -m "Incrementing version to ${TRAVIS_TAG}"
|
||||
- git push https://${GITHUB_TOKEN}@github.com/jrnl-org/jrnl.git master
|
||||
|
||||
aliases:
|
||||
test_mac: &test_mac
|
||||
os: osx
|
||||
language: shell
|
||||
osx_image: xcode11.2
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.pyenv/versions
|
||||
before_install:
|
||||
- eval "$(pyenv init -)"
|
||||
- pyenv install -s $JRNL_PYTHON_VERSION
|
||||
- pyenv global $JRNL_PYTHON_VERSION
|
||||
- pip install --upgrade pip
|
||||
- pip --version
|
||||
test_windows: &test_windows
|
||||
os: windows
|
||||
language: shell
|
||||
cache:
|
||||
directories:
|
||||
- /c/Python36
|
||||
- /c/Python37
|
||||
- /c/Python38
|
||||
before_install:
|
||||
- choco install python --version $JRNL_PYTHON_VERSION
|
||||
- python -m pip install --upgrade pip
|
||||
- pip --version
|
||||
|
||||
jobs:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- python: nightly
|
||||
|
||||
include:
|
||||
- name: Lint, via Black
|
||||
python: 3.8
|
||||
script:
|
||||
- black --version
|
||||
- black --check . --verbose --diff
|
||||
|
||||
# Python 3.6 Tests
|
||||
- name: Python 3.6 on Linux
|
||||
python: 3.6
|
||||
- <<: *test_mac
|
||||
name: Python 3.6 on MacOS
|
||||
python: 3.6
|
||||
env:
|
||||
- JRNL_PYTHON_VERSION=3.6.8
|
||||
- <<: *test_windows
|
||||
name: Python 3.6 on Windows
|
||||
python: 3.6
|
||||
env:
|
||||
- JRNL_PYTHON_VERSION=3.6.8
|
||||
- PATH=/c/Python36:/c/Python36/Scripts:$PATH
|
||||
- PYTHONIOENCODING=UTF-8
|
||||
|
||||
# Python 3.7 Tests
|
||||
- name: Python 3.7 on Linux
|
||||
python: 3.7
|
||||
- <<: *test_mac
|
||||
name: Python 3.7 on MacOS
|
||||
python: 3.7
|
||||
env:
|
||||
- JRNL_PYTHON_VERSION=3.7.5
|
||||
- <<: *test_windows
|
||||
name: Python 3.7 on Windows
|
||||
python: 3.7
|
||||
env:
|
||||
- JRNL_PYTHON_VERSION=3.7.5
|
||||
- PATH=/c/Python37:/c/Python37/Scripts:$PATH
|
||||
- PYTHONIOENCODING=UTF-8
|
||||
|
||||
# Python 3.8 Tests
|
||||
- name: Python 3.8 on Linux
|
||||
python: 3.8
|
||||
- <<: *test_mac
|
||||
name: Python 3.8 on MacOS
|
||||
python: 3.8
|
||||
env:
|
||||
- JRNL_PYTHON_VERSION=3.8.0
|
||||
- <<: *test_windows
|
||||
name: Python 3.8 on Windows
|
||||
python: 3.8
|
||||
env:
|
||||
- JRNL_PYTHON_VERSION=3.8.0
|
||||
- PATH=/c/Python38:/c/Python38/Scripts:$PATH
|
||||
- PYTHONIOENCODING=UTF-8
|
||||
|
||||
# ... and beyond!
|
||||
- name: Python nightly on Linux
|
||||
python: nightly
|
||||
|
||||
# Specialty tests
|
||||
- name: Python 3.7 on Linux, not UTC
|
||||
python: 3.7
|
||||
env:
|
||||
- TZ=America/Edmonton
|
||||
|
||||
# Changelog for Unreleased changes
|
||||
- stage: Update Changelog
|
||||
if: (tag IS present) OR (branch = develop AND type NOT IN (pull_request))
|
||||
install:
|
||||
- echo 'Skipping install'
|
||||
script:
|
||||
- ./.build/generate_changelog.sh
|
||||
|
||||
- stage: Deploy
|
||||
if: tag IS present
|
||||
before_deploy:
|
||||
- poetry version "$TRAVIS_TAG"
|
||||
- echo __version__ = \"$TRAVIS_TAG\" > jrnl/__version__.py
|
||||
- poetry build
|
||||
script:
|
||||
- echo "Deployment starting..."
|
||||
deploy:
|
||||
- provider: script
|
||||
script: poetry publish
|
||||
skip_cleanup: true
|
||||
on:
|
||||
branch: master
|
||||
tags: true
|
||||
after_deploy:
|
||||
- git config --global user.email "jrnl.bot@gmail.com"
|
||||
- git config --global user.name "Jrnl Bot"
|
||||
- git checkout master
|
||||
- git add pyproject.toml
|
||||
- git commit -m "Incrementing version to ${TRAVIS_TAG} [ci skip]"
|
||||
- git push https://${GITHUB_TOKEN}@github.com/jrnl-org/jrnl.git master
|
||||
|
|
202
CHANGELOG.md
202
CHANGELOG.md
|
@ -1,14 +1,171 @@
|
|||
Changelog
|
||||
=========
|
||||
# Changelog
|
||||
|
||||
## 2.0
|
||||
## [Unreleased](https://github.com/jrnl-org/jrnl/)
|
||||
|
||||
* Cryptographic backend changed from PyCrypto to cryptography.io
|
||||
* Config now respects XDG conventions and may move accordingly
|
||||
* Config now saved as YAML
|
||||
* Config name changed from `journals.jrnl_name.journal` to `journals.jrnl_name.path`
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.2...HEAD)
|
||||
|
||||
### 1.9 (July 21, 2014)
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Update YAML exporter to handle Dayone format [\#773](https://github.com/jrnl-org/jrnl/pull/773) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Listing all entries in DayOne Classic journal throws IndexError [\#786](https://github.com/jrnl-org/jrnl/pull/786) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||
- Add UTC support for failing DayOne tests [\#785](https://github.com/jrnl-org/jrnl/pull/785) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||
|
||||
**Build:**
|
||||
|
||||
- Stop multipe changelog generators from crashing into each other [\#845](https://github.com/jrnl-org/jrnl/pull/845) ([wren](https://github.com/wren))
|
||||
- Don't re-run tests on deployment [\#839](https://github.com/jrnl-org/jrnl/pull/839) ([wren](https://github.com/wren))
|
||||
- Put back build lines in Poetry config [\#838](https://github.com/jrnl-org/jrnl/pull/838) ([wren](https://github.com/wren))
|
||||
- Restore emoji test [\#837](https://github.com/jrnl-org/jrnl/pull/837) ([micahellison](https://github.com/micahellison))
|
||||
- Fix crashing unicode Travis tests on Windows and fail build if Windows tests fail [\#836](https://github.com/jrnl-org/jrnl/pull/836) ([micahellison](https://github.com/micahellison))
|
||||
- Remove poetry from build system in pyproject config to fix `brew install` [\#830](https://github.com/jrnl-org/jrnl/pull/830) ([wren](https://github.com/wren))
|
||||
- Fix all skipped tests on Travis Windows builds by preserving newlines [\#823](https://github.com/jrnl-org/jrnl/pull/823) ([micahellison](https://github.com/micahellison))
|
||||
|
||||
**Updated documentation:**
|
||||
|
||||
- Update site description [\#841](https://github.com/jrnl-org/jrnl/pull/841) ([wren](https://github.com/wren))
|
||||
- Get rid of dumb sex joke [\#840](https://github.com/jrnl-org/jrnl/pull/840) ([wren](https://github.com/wren))
|
||||
- Updating/clarifying template explanation [\#829](https://github.com/jrnl-org/jrnl/pull/829) ([heymajor](https://github.com/heymajor))
|
||||
|
||||
## [v2.2](https://pypi.org/project/jrnl/v2.2/) (2020-02-01)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.1.1...v2.2)
|
||||
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Update YAML exporter to handle Dayone format [\#773](https://github.com/jrnl-org/jrnl/pull/773) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||
- Full text search \(case insensitive\) with "-contains" [\#740](https://github.com/jrnl-org/jrnl/pull/740) ([empireshades](https://github.com/empireshades))
|
||||
- Reduce startup time by 55% [\#719](https://github.com/jrnl-org/jrnl/pull/719) ([maebert](https://github.com/maebert))
|
||||
- Refactor password logic to prevent accidental password leakage [\#708](https://github.com/jrnl-org/jrnl/pull/708) ([pspeter](https://github.com/pspeter))
|
||||
- Password confirmation [\#706](https://github.com/jrnl-org/jrnl/pull/706) ([pspeter](https://github.com/pspeter))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Close temp file before passing it to editor to prevent file locking issues in Windows [\#792](https://github.com/jrnl-org/jrnl/pull/792) ([micahellison](https://github.com/micahellison))
|
||||
- Fix crash while encrypting a journal on first run without saving password [\#789](https://github.com/jrnl-org/jrnl/pull/789) ([dbxnr](https://github.com/dbxnr))
|
||||
|
||||
**Build:**
|
||||
|
||||
- Fix issue where jrnl would always out 'source' for version, fix Poetry config to build and publish properly [\#820](https://github.com/jrnl-org/jrnl/pull/820) ([wren](https://github.com/wren))
|
||||
- Unpin poetry [\#808](https://github.com/jrnl-org/jrnl/pull/808) ([wren](https://github.com/wren))
|
||||
- Fix all skipped tests on Travis Windows builds by preserving newlines [\#823](https://github.com/jrnl-org/jrnl/pull/823) ([micahellison](https://github.com/micahellison))
|
||||
- Change PyPI auth method in build pipeline [\#807](https://github.com/jrnl-org/jrnl/pull/807) ([wren](https://github.com/wren))
|
||||
- Automagically update the changelog you see before your very eyes! [\#806](https://github.com/jrnl-org/jrnl/pull/806) ([wren](https://github.com/wren))
|
||||
- Update Black version and lock file to fix builds on develop branch [\#784](https://github.com/jrnl-org/jrnl/pull/784) ([wren](https://github.com/wren))
|
||||
- Run black formatter on codebase for standardization [\#778](https://github.com/jrnl-org/jrnl/pull/778) ([wren](https://github.com/wren))
|
||||
- Skip Broken Windows Tests [\#772](https://github.com/jrnl-org/jrnl/pull/772) ([wren](https://github.com/wren))
|
||||
- Black Formatter [\#769](https://github.com/jrnl-org/jrnl/pull/769) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||
- Update lock file and testing suite for Python 3.8 [\#765](https://github.com/jrnl-org/jrnl/pull/765) ([wren](https://github.com/wren))
|
||||
- Fix CI config to only deploy once [\#761](https://github.com/jrnl-org/jrnl/pull/761) ([wren](https://github.com/wren))
|
||||
- More Travis-CI Testing [\#759](https://github.com/jrnl-org/jrnl/pull/759) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||
|
||||
**Updated documentation:**
|
||||
|
||||
- Explain how fish can be configured to exclude jrnl commands from history by default [\#809](https://github.com/jrnl-org/jrnl/pull/809) ([aureooms](https://github.com/aureooms))
|
||||
- Remove merge marker in recipes.md [\#782](https://github.com/jrnl-org/jrnl/pull/782) ([markphelps](https://github.com/markphelps))
|
||||
- Fix merge conflict left-over [\#767](https://github.com/jrnl-org/jrnl/pull/767) ([thejspr](https://github.com/thejspr))
|
||||
- Display header in docs on mobile devices [\#763](https://github.com/jrnl-org/jrnl/pull/763) ([maebert](https://github.com/maebert))
|
||||
|
||||
## [v2.1.1](https://pypi.org/project/jrnl/v2.1.1/) (2019-11-26)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.1.post2...v2.1.1)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Support Python 3.6+ [\#710](https://github.com/jrnl-org/jrnl/pull/710) ([pspeter](https://github.com/pspeter))
|
||||
- Drop Python 2 support, add mocks in tests [\#705](https://github.com/jrnl-org/jrnl/pull/705) ([pspeter](https://github.com/pspeter))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Prevent readline usage on Windows, which was causing Active Python crashes on install [\#751](https://github.com/jrnl-org/jrnl/pull/751) ([micahellison](https://github.com/micahellison))
|
||||
- Exit jrnl if no text entered into editor [\#744](https://github.com/jrnl-org/jrnl/pull/744) ([alichtman](https://github.com/alichtman))
|
||||
- Fix crash when no keyring backend available [\#699](https://github.com/jrnl-org/jrnl/pull/699) ([pspeter](https://github.com/pspeter))
|
||||
- Fix parsing Journals using a little-endian date format [\#694](https://github.com/jrnl-org/jrnl/pull/694) ([pspeter](https://github.com/pspeter))
|
||||
|
||||
**Updated documentation:**
|
||||
|
||||
- Update developer documentation [\#752](https://github.com/jrnl-org/jrnl/pull/752) ([micahellison](https://github.com/micahellison))
|
||||
- Create templates for issues and pull requests [\#679](https://github.com/jrnl-org/jrnl/pull/679) ([C0DK](https://github.com/C0DK))
|
||||
- Smaller doc fixes [\#649](https://github.com/jrnl-org/jrnl/pull/649) ([maebert](https://github.com/maebert))
|
||||
- Move to mkdocs [\#611](https://github.com/jrnl-org/jrnl/pull/611) ([maebert](https://github.com/maebert))
|
||||
|
||||
## [v2.1.post2](https://pypi.org/project/jrnl/v2.1.post2/) (2019-11-11)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.0.1...v2.1.post2)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Expand paths that use ~ to full path [\#704](https://github.com/jrnl-org/jrnl/pull/704) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||
|
||||
**Build:**
|
||||
|
||||
- Separate local dev from pipeline releases [\#684](https://github.com/jrnl-org/jrnl/pull/684) ([wren](https://github.com/wren))
|
||||
- Update version handling in source and travis deployments [\#683](https://github.com/jrnl-org/jrnl/pull/683) ([wren](https://github.com/wren))
|
||||
- Use Poetry for dependency management and deployments [\#612](https://github.com/jrnl-org/jrnl/pull/612) ([maebert](https://github.com/maebert))
|
||||
|
||||
**Updated documentation:**
|
||||
|
||||
- Fix typos, spelling [\#734](https://github.com/jrnl-org/jrnl/pull/734) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||
|
||||
## [v2.0.1](https://pypi.org/project/jrnl/v2.0.1/) (2019-09-26)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.0.0...v2.0.1)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Switch to hashmark Markdown headers on export \(Mk II\) [\#639](https://github.com/jrnl-org/jrnl/pull/639) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||
- Add '-not' flag for excluding tags from filter [\#637](https://github.com/jrnl-org/jrnl/pull/637) ([jprof](https://github.com/jprof))
|
||||
- Handle KeyboardInterrupt when installing journal [\#550](https://github.com/jrnl-org/jrnl/pull/550) ([silenc3r](https://github.com/silenc3r))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Change pyYAML required version [\#660](https://github.com/jrnl-org/jrnl/pull/660) ([etnnth](https://github.com/etnnth))
|
||||
|
||||
**Updated documentation:**
|
||||
|
||||
- Fix references to Sphinx in CONTRIBUTING.md [\#655](https://github.com/jrnl-org/jrnl/pull/655) ([maebert](https://github.com/maebert))
|
||||
|
||||
## [v2.0.0](https://pypi.org/project/jrnl/v2.0.0/) (2019-08-24)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/1.9.8...v2.0.0)
|
||||
|
||||
🚨 **BREAKING CHANGES** 🚨
|
||||
|
||||
**Implemented enhancements:**
|
||||
- Change cryptographic backend from PyCrypto to cryptography.io
|
||||
- Config now respects XDG conventions and may move accordingly
|
||||
- Config name changed from `journals.jrnl_name.journal` to `journals.jrnl_name.path`
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Confirm that each journal can be parsed during upgrade, and abort upgrade if not [\#650](https://github.com/jrnl-org/jrnl/pull/650) ([micahellison](https://github.com/micahellison))
|
||||
- Escape dates in square brackets [\#644](https://github.com/jrnl-org/jrnl/pull/644) ([wren](https://github.com/wren))
|
||||
- Create encrypted journal [\#641](https://github.com/jrnl-org/jrnl/pull/641) ([gregorybodnar](https://github.com/gregorybodnar))
|
||||
- Resolve issues around unreadable dates to allow markdown footnotes and prevent accidental deletion [\#623](https://github.com/jrnl-org/jrnl/pull/623) ([micahellison](https://github.com/micahellison))
|
||||
- Update crypto module \#610 [\#621](https://github.com/jrnl-org/jrnl/pull/621) ([wren](https://github.com/wren))
|
||||
- Fix issue \#584 YAMLLoadWarning [\#585](https://github.com/jrnl-org/jrnl/pull/585) ([wren](https://github.com/wren))
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
- Deprecate Python 2 [\#624](https://github.com/jrnl-org/jrnl/pull/624) ([micahellison](https://github.com/micahellison))
|
||||
- Config now saved as YAML (no more JSON)
|
||||
|
||||
**Build:**
|
||||
|
||||
- change pinned label to a super cool emoji ⭐️ [\#646](https://github.com/jrnl-org/jrnl/pull/646) ([wren](https://github.com/wren))
|
||||
- Update Travis build badge and restore pypi badges [\#603](https://github.com/jrnl-org/jrnl/pull/603) ([micahellison](https://github.com/micahellison))
|
||||
|
||||
**Updated documentation:**
|
||||
|
||||
- Mention lack of Day One support and relevant history in readme [\#608](https://github.com/jrnl-org/jrnl/pull/608) ([micahellison](https://github.com/micahellison))
|
||||
- Add a code of conduct file \(rather than adding to contributing\) [\#604](https://github.com/jrnl-org/jrnl/pull/604) ([wren](https://github.com/wren))
|
||||
- Update docs to reflect merging jrnl-plus fork back upstream [\#601](https://github.com/jrnl-org/jrnl/pull/601) ([micahellison](https://github.com/micahellison))
|
||||
- Add instructions for VS Code [\#544](https://github.com/jrnl-org/jrnl/pull/544) ([emceeaich](https://github.com/emceeaich))
|
||||
|
||||
## v1.9 (2014-07-21)
|
||||
|
||||
* __1.9.5__ Multi-word tags for DayOne Journals
|
||||
* __1.9.4__ Fixed: Order of journal entries in file correct after --edit'ing
|
||||
|
@ -17,7 +174,7 @@ Changelog
|
|||
* __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.8 (May 22, 2014)
|
||||
## v1.8 (2014-05-22)
|
||||
|
||||
* __1.8.7__ Fixed: -from and -to filters are inclusive (thanks to @grplyler)
|
||||
* __1.8.6__ Improved: Tags like @C++ and @OS/2 work, too (thanks to @chaitan94)
|
||||
|
@ -28,7 +185,7 @@ Changelog
|
|||
* __1.8.1__ Minor bug fixes
|
||||
* __1.8.0__ Official support for python 3.4
|
||||
|
||||
### 1.7 (December 22, 2013)
|
||||
## v1.7 (2013-12-22)
|
||||
|
||||
* __1.7.22__ Fixed an issue with writing files when exporting entries containing non-ascii characters.
|
||||
* __1.7.21__ jrnl now uses PKCS#7 padding.
|
||||
|
@ -54,7 +211,7 @@ Changelog
|
|||
* __1.7.0__ Edit encrypted or DayOne journals with `jrnl --edit`.
|
||||
|
||||
|
||||
### 1.6 (November 5, 2013)
|
||||
## v1.6 (2013-11-05)
|
||||
|
||||
* __1.6.6__ -v prints the current version, also better strings for windows users. Furthermore, jrnl/jrnl.py moved to jrnl/cli.py
|
||||
* __1.6.5__ Allows composing multi-line entries on the command line or importing files
|
||||
|
@ -64,7 +221,7 @@ Changelog
|
|||
* __1.6.1__ Attempts to fix broken config files automatically
|
||||
* __1.6.0__ Passwords are now saved in the key-chain. The `password` field in `.jrnl_config` is soft-deprecated.
|
||||
|
||||
### 1.5 (August 6, 2013)
|
||||
## v1.5 (2013-08-06)
|
||||
|
||||
* __1.5.7__ The `~` in journal config paths will now expand properly to e.g. `/Users/maebert`
|
||||
* __1.5.6__ Fixed: Fixed a bug where on OS X, the timezone could only be accessed on administrator accounts.
|
||||
|
@ -75,23 +232,23 @@ Changelog
|
|||
* __1.5.1__ Fixed: Fixed a bug introduced in 1.5.0 that caused the entire journal to be printed after composing an entry
|
||||
* __1.5.0__ Exporting, encrypting and displaying tags now takes your filter options into account. So you could export everything before May 2012: `jrnl -to 'may 2012' --export json`. Or encrypt all entries tagged with `@work` into a new journal: `jrnl @work --encrypt work_journal.txt`. Or display all tags of posts where Bob is also tagged: `jrnl @bob --tags`
|
||||
|
||||
### 1.4 (July 22, 2013)
|
||||
## v1.4 (2013-07-22)
|
||||
|
||||
* __1.4.2__ Fixed: Tagging works again
|
||||
* __1.4.0__ Unifies encryption between Python 2 and 3. If you have problems reading encrypted journals afterwards, first decrypt your journal with the __old__ jrnl version (install with `pip install jrnl==1.3.1`, then `jrnl --decrypt`), upgrade jrnl (`pip install jrnl --upgrade`) and encrypt it again (`jrnl --encrypt`).
|
||||
|
||||
### 1.3 (July 17, 2013)
|
||||
## v1.3 (2013-07-17)
|
||||
|
||||
* __1.3.2__ Everything that is not direct output of jrnl will be written stderr to improve integration
|
||||
* __1.3.0__ Export to multiple files
|
||||
* __1.3.0__ Feature to export to given output file
|
||||
|
||||
### 1.2 (July 15, 2013)
|
||||
## v1.2 (2013-07-15)
|
||||
|
||||
* __1.2.0__ Fixed: Timezone support for DayOne
|
||||
|
||||
|
||||
### 1.1 (June 9, 2013)
|
||||
## v1.1 (2013-06-09)
|
||||
|
||||
* __1.1.1__ Fixed: Unicode and Python3 issues resolved.
|
||||
* __1.1.0__
|
||||
|
@ -99,7 +256,7 @@ Changelog
|
|||
* Nicer error message when there is a syntactical error in your config file.
|
||||
* Unicode support
|
||||
|
||||
### 1.0 (March 4, 2013)
|
||||
## v1.0 (2013-03-04)
|
||||
|
||||
* __1.0.5__ Backwards compatibility with `parsedatetime` 0.8.7
|
||||
* __1.0.4__
|
||||
|
@ -122,7 +279,7 @@ Changelog
|
|||
* Fixed: A bug where jrnl would not add entries without timestamp
|
||||
* Fixed: Support for parsedatetime 1.x
|
||||
|
||||
### 0.3 (May 24, 2012)
|
||||
## v0.3 (2012-05-24)
|
||||
|
||||
* __0.3.2__ Converts `\n` to new lines (if using directly on a command line, make sure to wrap your entry with quotes).
|
||||
* __0.3.1__
|
||||
|
@ -135,7 +292,7 @@ Changelog
|
|||
* Fixed: Bug where composed entry is lost when the journal file fails to load
|
||||
* Changed directory structure and install scripts (removing the necessity to make an alias from `jrnl` to `jrnl.py`)
|
||||
|
||||
### 0.2 (April 16, 2012)
|
||||
## v0.2 (2012-04-16)
|
||||
|
||||
* __0.2.4__
|
||||
* Fixed: Parsing of new lines in journal files and entries
|
||||
|
@ -153,7 +310,7 @@ Changelog
|
|||
* Encrypts using CBC
|
||||
* Fixed: `key` has been renamed to `password` in config to avoid confusion. (The key use to encrypt and decrypt a journal is the SHA256-hash of the password.)
|
||||
|
||||
### 0.1 (April 13, 2012)
|
||||
## v0.1 (2012-04-13)
|
||||
|
||||
|
||||
* __0.1.1__
|
||||
|
@ -166,6 +323,9 @@ Changelog
|
|||
* Filtering by tags and dates
|
||||
* Fixed: Now using dedicated classes for Journals and entries
|
||||
|
||||
### 0.0 (March 29, 2012)
|
||||
## v0.0 (2012-03-29)
|
||||
|
||||
* __0.0.1__ Composing entries works. That's pretty much it.
|
||||
|
||||
|
||||
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
||||
|
|
|
@ -45,9 +45,17 @@ p {
|
|||
/* No-one likes lines that are 400 characters long. */
|
||||
div.rst-content {max-width: 54em;}
|
||||
|
||||
.wy-side-nav-search, .wy-nav-top, .wy-menu-vertical li.current {
|
||||
.wy-side-nav-search, .wy-menu-vertical li.current {
|
||||
background-color: transparent;
|
||||
}
|
||||
.wy-nav-top {
|
||||
background-image: linear-gradient(-211deg, #95699C 0%, #604385 100%);
|
||||
}
|
||||
|
||||
.wy-nav-top .fa-bars {
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.wy-side-nav-search a.icon-home {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
|
|
|
@ -56,10 +56,14 @@ setopt HIST_IGNORE_SPACE
|
|||
alias jrnl=" jrnl"
|
||||
```
|
||||
|
||||
The fish shell does not support automatically preventing logging like
|
||||
this. To prevent `jrnl` commands being logged by fish, you must make
|
||||
sure to type a space before every `jrnl` command you enter. To delete
|
||||
existing `jrnl` commands from fish’s history, run
|
||||
If you are using `fish` instead of `bash` or `zsh`, you can get the same behaviour by
|
||||
adding this to your `fish` configuration:
|
||||
|
||||
``` sh
|
||||
abbr jrnl " jrnl"
|
||||
```
|
||||
|
||||
To delete existing `jrnl` commands from `fish`’s history, run
|
||||
`history delete --prefix 'jrnl '`.
|
||||
|
||||
## Manual decryption
|
||||
|
|
|
@ -70,17 +70,59 @@ jrnlimport () {
|
|||
|
||||
### Using templates
|
||||
|
||||
Say you always want to use the same template for creating new entries.
|
||||
If you have an [external editor](../advanced) set up, you can use this:
|
||||
!!! note
|
||||
Templates require an [external editor](../advanced) be configured.
|
||||
|
||||
A template is a code snippet that makes it easier to enter use repeated text
|
||||
each time a new journal entry is started. There are two ways you can utilize
|
||||
templates in your entries.
|
||||
|
||||
#### 1. Command line arguments
|
||||
|
||||
If you had a `template.txt` file with the following contents:
|
||||
|
||||
```sh
|
||||
jrnl < my_template.txt
|
||||
jrnl -1 --edit
|
||||
My Personal Journal
|
||||
Title:
|
||||
|
||||
Body:
|
||||
```
|
||||
|
||||
Another nice solution that allows you to define individual prompts comes
|
||||
from [Jacobo de
|
||||
Vera](https://github.com/maebert/jrnl/issues/194#issuecomment-47402869):
|
||||
The `template.txt` file could be used to create a new entry with these
|
||||
command line arguements:
|
||||
|
||||
```sh
|
||||
jrnl < template.txt # Imports template.txt as the most recent entry
|
||||
jrnl -1 --edit # Opens the most recent entry in the editor
|
||||
```
|
||||
|
||||
#### 2. Include the template file in `jrnl.yaml`
|
||||
|
||||
A more efficient way to work with a template file is to declare the file
|
||||
in your config file by changing the `template` setting from `false` to the
|
||||
template file's path in double quotes:
|
||||
|
||||
```sh
|
||||
...
|
||||
template: "/path/to/template.txt"
|
||||
...
|
||||
```
|
||||
|
||||
Changes can be saved as you continue writing the journal entry and will be
|
||||
logged as a new entry in the journal you specified in the original argument.
|
||||
|
||||
!!! tip
|
||||
To read your journal entry or to verify the entry saved, you can use this
|
||||
command: `jrnl -n 1` (Check out [Import and Export](../export/#export-to-files) for more export options).
|
||||
|
||||
```sh
|
||||
jrnl -n 1
|
||||
```
|
||||
|
||||
### Prompts on shell reload
|
||||
|
||||
If you'd like to be prompted each time you refresh your shell, you can include
|
||||
this in your `.bash_profile`:
|
||||
|
||||
```sh
|
||||
function log_question()
|
||||
|
@ -93,6 +135,11 @@ log_question 'What did I achieve today?'
|
|||
log_question 'What did I make progress with?'
|
||||
```
|
||||
|
||||
Whenever your shell is reloaded, you will be prompted to answer each of the
|
||||
questions in the example above. Each answer will be logged as a separate
|
||||
journal entry at the `default_hour` and `default_minute` listed in your
|
||||
`jrnl.yaml` [config file](../advanced/#configuration-file).
|
||||
|
||||
### Display random entry
|
||||
|
||||
You can use this to select one title at random and then display the whole
|
||||
|
@ -107,10 +154,11 @@ jrnl -on "$(jrnl --short | shuf -n 1 | cut -d' ' -f1,2)"
|
|||
|
||||
## External editors
|
||||
|
||||
To use external editors for writing and editing journal entries, set
|
||||
them up in your `jrnl.yaml` (see `advanced usage <advanced>` for
|
||||
details). Generally, after writing an entry, you will have to save and
|
||||
close the file to save the changes to jrnl.
|
||||
Configure your preferred external editor by updating the `editor` option
|
||||
in your `jrnl.yaml` file. (See [advanced usage](../advanced) for details).
|
||||
|
||||
!!! note
|
||||
To save and log any entry edits, save and close the file.
|
||||
|
||||
### Sublime Text
|
||||
|
||||
|
@ -130,8 +178,6 @@ Similar to Sublime Text, MacVim must be started with a flag that tells
|
|||
the the process to wait until the file is closed before passing control
|
||||
back to journal. In the case of MacVim, this is `-f`:
|
||||
|
||||
<<<<<<< HEAD
|
||||
|
||||
```yaml
|
||||
editor: "mvim -f"
|
||||
```
|
||||
|
|
6
docs/theme/index.html
vendored
6
docs/theme/index.html
vendored
|
@ -34,7 +34,7 @@
|
|||
"operatingSystem": ["macOS", "Windows", "Linux"],
|
||||
"thumbnailUrl": "https://jrnl.sh/img/banner_og.png",
|
||||
"installUrl": "https://jrnl.sh/installation",
|
||||
"softwareVersion": "2.0.0rc2"
|
||||
"softwareVersion": "2.2"
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
@ -42,7 +42,7 @@
|
|||
<body>
|
||||
<header>
|
||||
<aside>
|
||||
<a id="twitter" href="https://twitter.com/intent/tweet?text=Write+your+memoirs+on+the+command+line.+Like+a+boss.+%23jrnl&url=http%3A%2F%2Fjrnl.sh&via=maebert"><i class="icon twitter"></i>Tell your friends</a>
|
||||
<a id="twitter" href="https://twitter.com/intent/tweet?text=Write+your+memoirs+on+the+command+line.+Like+a+boss.+%23jrnl&url=http%3A%2F%2Fjrnl.sh"><i class="icon twitter"></i>Tell your friends</a>
|
||||
</aside>
|
||||
<div id="title">
|
||||
<img id="logo" src="img/jrnl_white.svg" width="90px" height="98px" title="jrnl" />
|
||||
|
@ -58,7 +58,7 @@
|
|||
<nav>
|
||||
<a href="overview">Documentation</a>
|
||||
<a href="http://github.com/jrnl-org/jrnl" title="View on Github">Fork me on GitHub</a>
|
||||
<a id="twitter-nav" href="https://twitter.com/intent/tweet?text=Write+your+memoirs+on+the+command+line.+Like+a+boss.+%23jrnl&url=http%3A%2F%2Fjrnl.sh&via=maebert">Tell your friends on twitter</a>
|
||||
<a id="twitter-nav" href="https://twitter.com/intent/tweet?text=Write+your+memoirs+on+the+command+line.+Like+a+boss.+%23jrnl&url=http%3A%2F%2Fjrnl.sh">Tell your friends on twitter</a>
|
||||
<a href="installation" class="cta">Download</a>
|
||||
</nav>
|
||||
<div class="flex">
|
||||
|
|
|
@ -35,12 +35,12 @@ jrnl today at 3am: I just met Steve Buscemi in a bar! He looked funny.
|
|||
```
|
||||
|
||||
!!! note
|
||||
Most shell contains a certain number of reserved characters, such as `#`
|
||||
and `*`. Unbalanced quotes, parenthesis, and so on will also get into
|
||||
the way of your editing.
|
||||
For writing longer entries, just enter `jrnl`
|
||||
and hit `return`. Only then enter the text of your journal entry.
|
||||
Alternatively, `use an external editor <advanced>`).
|
||||
Most shell contains a certain number of reserved characters, such as `#`
|
||||
and `*`. Unbalanced quotes, parenthesis, and so on will also get into
|
||||
the way of your editing.
|
||||
For writing longer entries, just enter `jrnl`
|
||||
and hit `return`. Only then enter the text of your journal entry.
|
||||
Alternatively, `use an external editor <advanced>`).
|
||||
|
||||
You can also import an entry directly from a file
|
||||
|
||||
|
@ -76,9 +76,9 @@ The following options are equivalent:
|
|||
- `jrnl Best day of my life.*`
|
||||
|
||||
!!! note
|
||||
Just make sure that the asterisk sign is **not** surrounded by
|
||||
whitespaces, e.g. `jrnl Best day of my life! *` will **not** work (the
|
||||
reason being that the `*` sign has a special meaning on most shells).
|
||||
Just make sure that the asterisk sign is **not** surrounded by
|
||||
whitespaces, e.g. `jrnl Best day of my life! *` will **not** work (the
|
||||
reason being that the `*` sign has a special meaning on most shells).
|
||||
|
||||
## Viewing
|
||||
|
||||
|
@ -119,17 +119,17 @@ Will print all entries in which either `@pinkie` or `@WorldDomination`
|
|||
occurred.
|
||||
|
||||
```sh
|
||||
jrnl -n 5 -and @pineapple @lubricant
|
||||
jrnl -n 5 -and @pinkie @WorldDomination
|
||||
```
|
||||
|
||||
the last five entries containing both `@pineapple` **and** `@lubricant`.
|
||||
the last five entries containing both `@pinkie` **and** `@worldDomination`.
|
||||
You can change which symbols you'd like to use for tagging in the
|
||||
configuration.
|
||||
|
||||
!!! note
|
||||
`jrnl @pinkie @WorldDomination` will switch to viewing mode because
|
||||
although **no** command line arguments are given, all the input strings
|
||||
look like tags - _jrnl_ will assume you want to filter by tag.
|
||||
`jrnl @pinkie @WorldDomination` will switch to viewing mode because
|
||||
although **no** command line arguments are given, all the input strings
|
||||
look like tags - _jrnl_ will assume you want to filter by tag.
|
||||
|
||||
## Editing older entries
|
||||
|
||||
|
@ -154,7 +154,7 @@ encrypt) your edited journal after you save and exit the editor.
|
|||
You can also use this feature for deleting entries from your journal
|
||||
|
||||
```sh
|
||||
jrnl @girlfriend -until 'june 2012' --edit
|
||||
jrnl @texas -until 'june 2012' --edit
|
||||
```
|
||||
|
||||
Just select all text, press delete, and everything is gone...
|
||||
|
|
29
features/contains.feature
Normal file
29
features/contains.feature
Normal file
|
@ -0,0 +1,29 @@
|
|||
Feature: Contains
|
||||
|
||||
Scenario: Searching for a string
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl -contains life"
|
||||
Then we should get no error
|
||||
and the output should be
|
||||
"""
|
||||
2013-06-10 15:40 Life is good.
|
||||
| But I'm better.
|
||||
"""
|
||||
|
||||
Scenario: Searching for a string within tag results
|
||||
Given we use the config "tags.yaml"
|
||||
When we run "jrnl @idea -contains software"
|
||||
Then we should get no error
|
||||
and the output should contain "software"
|
||||
|
||||
Scenario: Searching for a string within AND tag results
|
||||
Given we use the config "tags.yaml"
|
||||
When we run "jrnl -and @journal @idea -contains software"
|
||||
Then we should get no error
|
||||
and the output should contain "software"
|
||||
|
||||
Scenario: Searching for a string within NOT tag results
|
||||
Given we use the config "tags.yaml"
|
||||
When we run "jrnl -not @dan -contains software"
|
||||
Then we should get no error
|
||||
and the output should contain "software"
|
|
@ -20,6 +20,7 @@ Feature: Basic reading and writing to a journal
|
|||
When we run "jrnl -n 1"
|
||||
Then the output should contain "2013-07-23 09:00 A cold and stormy day."
|
||||
|
||||
@skip_win
|
||||
Scenario: Writing an empty entry from the editor
|
||||
Given we use the config "editor.yaml"
|
||||
When we open the editor and enter ""
|
||||
|
|
|
@ -6,7 +6,6 @@ highlight: true
|
|||
journals:
|
||||
default: features/journals/bug153.dayone
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
template: false
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
|
|
12
features/data/configs/bug780.yaml
Normal file
12
features/data/configs/bug780.yaml
Normal file
|
@ -0,0 +1,12 @@
|
|||
default_hour: 9
|
||||
default_minute: 0
|
||||
editor: ''
|
||||
encrypt: false
|
||||
highlight: true
|
||||
journals:
|
||||
default: features/journals/bug780.dayone
|
||||
linewrap: 80
|
||||
tagsymbols: '@'
|
||||
template: false
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
|
@ -7,7 +7,6 @@ highlight: true
|
|||
journals:
|
||||
default: features/journals/dayone.dayone
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
||||
|
|
|
@ -7,7 +7,6 @@ highlight: true
|
|||
journals:
|
||||
default: features/journals/empty_folder
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
||||
|
|
|
@ -7,7 +7,6 @@ highlight: true
|
|||
journals:
|
||||
default: features/journals/encrypted.journal
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
||||
|
|
|
@ -7,7 +7,6 @@ template: false
|
|||
journals:
|
||||
default: features/journals/markdown-headings-335.journal
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
||||
|
|
|
@ -13,7 +13,6 @@ journals:
|
|||
encrypt: true
|
||||
journal: features/journals/new_encrypted.journal
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
||||
|
|
|
@ -7,7 +7,6 @@ template: false
|
|||
journals:
|
||||
default: features/journals/tags-216.journal
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
||||
|
|
|
@ -7,7 +7,6 @@ template: false
|
|||
journals:
|
||||
default: features/journals/tags-237.journal
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
||||
|
|
|
@ -7,7 +7,6 @@ template: false
|
|||
journals:
|
||||
default: features/journals/tags.journal
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Activity</key>
|
||||
<string>Stationary</string>
|
||||
<key>Creation Date</key>
|
||||
<date>2019-12-30T21:28:54Z</date>
|
||||
<key>Entry Text</key>
|
||||
<string></string>
|
||||
<key>Starred</key>
|
||||
<false />
|
||||
<key>UUID</key>
|
||||
<string>48A25033B34047C591160A4480197D8B</string>
|
||||
<key>Creator</key>
|
||||
<dict>
|
||||
<key>Device Agent</key>
|
||||
<string>PC</string>
|
||||
<key>Generation Date</key>
|
||||
<date>2019-12-30T21:28:54Z</date>
|
||||
<key>Host Name</key>
|
||||
<string>LE-TREPORT</string>
|
||||
<key>OS Agent</key>
|
||||
<string>Microsoft Windows/10 Home</string>
|
||||
<key>Software Agent</key>
|
||||
<string>Journaley/2.1</string>
|
||||
</dict>
|
||||
<key>Tags</key>
|
||||
<array>
|
||||
<string>i_have_no_body</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,7 +1,5 @@
|
|||
Feature: Dayone specific implementation details.
|
||||
|
||||
# fails when system time is UTC (as on Travis-CI)
|
||||
@skip
|
||||
Scenario: Loading a DayOne Journal
|
||||
Given we use the config "dayone.yaml"
|
||||
When we run "jrnl -from 'feb 2013'"
|
||||
|
@ -15,7 +13,7 @@ Feature: Dayone specific implementation details.
|
|||
2013-07-17 11:38 This entry is starred!
|
||||
"""
|
||||
|
||||
# fails when system time is UTC (as on Travis-CI)
|
||||
# broken still
|
||||
@skip
|
||||
Scenario: Entries without timezone information will be interpreted as in the current timezone
|
||||
Given we use the config "dayone.yaml"
|
||||
|
@ -23,7 +21,6 @@ Feature: Dayone specific implementation details.
|
|||
Then we should get no error
|
||||
and the output should contain "2013-01-17T18:37Z" in the local time
|
||||
|
||||
@skip
|
||||
Scenario: Writing into Dayone
|
||||
Given we use the config "dayone.yaml"
|
||||
When we run "jrnl 01 may 1979: Being born hurts."
|
||||
|
@ -33,8 +30,6 @@ Feature: Dayone specific implementation details.
|
|||
1979-05-01 09:00 Being born hurts.
|
||||
"""
|
||||
|
||||
# fails when system time is UTC (as on Travis-CI)
|
||||
@skip
|
||||
Scenario: Loading tags from a DayOne Journal
|
||||
Given we use the config "dayone.yaml"
|
||||
When we run "jrnl --tags"
|
||||
|
@ -44,8 +39,6 @@ Feature: Dayone specific implementation details.
|
|||
@play : 1
|
||||
"""
|
||||
|
||||
# fails when system time is UTC (as on Travis-CI)
|
||||
@skip
|
||||
Scenario: Saving tags from a DayOne Journal
|
||||
Given we use the config "dayone.yaml"
|
||||
When we run "jrnl A hard day at @work"
|
||||
|
@ -56,8 +49,6 @@ Feature: Dayone specific implementation details.
|
|||
@play : 1
|
||||
"""
|
||||
|
||||
# fails when system time is UTC (as on Travis-CI)
|
||||
@skip
|
||||
Scenario: Filtering by tags from a DayOne Journal
|
||||
Given we use the config "dayone.yaml"
|
||||
When we run "jrnl @work"
|
||||
|
@ -66,8 +57,6 @@ Feature: Dayone specific implementation details.
|
|||
2013-05-17 11:39 This entry has tags!
|
||||
"""
|
||||
|
||||
# fails when system time is UTC (as on Travis-CI)
|
||||
@skip
|
||||
Scenario: Exporting dayone to json
|
||||
Given we use the config "dayone.yaml"
|
||||
When we run "jrnl --export json"
|
||||
|
|
|
@ -13,16 +13,16 @@ Feature: Zapped Dayone bugs stay dead!
|
|||
|
||||
# fails when system time is UTC (as on Travis-CI)
|
||||
@skip
|
||||
Scenario: Title with an embedded period on DayOne journal
|
||||
Given we use the config "dayone.yaml"
|
||||
When we run "jrnl 04-24-2014: "Ran 6.2 miles today in 1:02:03. I'm feeling sore because I forgot to stretch.""
|
||||
Then we should see the message "Entry added"
|
||||
When we run "jrnl -1"
|
||||
Then the output should be
|
||||
"""
|
||||
2014-04-24 09:00 Ran 6.2 miles today in 1:02:03.
|
||||
| I'm feeling sore because I forgot to stretch.
|
||||
"""
|
||||
Scenario: Title with an embedded period on DayOne journal
|
||||
Given we use the config "dayone.yaml"
|
||||
When we run "jrnl 04-24-2014: "Ran 6.2 miles today in 1:02:03. I'm feeling sore because I forgot to stretch.""
|
||||
Then we should see the message "Entry added"
|
||||
When we run "jrnl -1"
|
||||
Then the output should be
|
||||
"""
|
||||
2014-04-24 09:00 Ran 6.2 miles today in 1:02:03.
|
||||
| I'm feeling sore because I forgot to stretch.
|
||||
"""
|
||||
|
||||
Scenario: Opening an folder that's not a DayOne folder gives a nice error message
|
||||
Given we use the config "empty_folder.yaml"
|
||||
|
|
|
@ -3,29 +3,55 @@
|
|||
Given we use the config "encrypted.yaml"
|
||||
When we run "jrnl -n 1" and enter "bad doggie no biscuit"
|
||||
Then the output should contain "Password"
|
||||
and the output should contain "2013-06-10 15:40 Life is good"
|
||||
And the output should contain "2013-06-10 15:40 Life is good"
|
||||
|
||||
Scenario: Decrypting a journal
|
||||
Given we use the config "encrypted.yaml"
|
||||
When we run "jrnl --decrypt" and enter "bad doggie no biscuit"
|
||||
Then the config for journal "default" should have "encrypt" set to "bool:False"
|
||||
Then we should see the message "Journal decrypted"
|
||||
and the journal should have 2 entries
|
||||
And the journal should have 2 entries
|
||||
|
||||
Scenario: Encrypting a journal
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl --encrypt" and enter "swordfish" and "n"
|
||||
When we run "jrnl --encrypt" and enter
|
||||
"""
|
||||
swordfish
|
||||
swordfish
|
||||
n
|
||||
"""
|
||||
Then we should see the message "Journal encrypted"
|
||||
and the config for journal "default" should have "encrypt" set to "bool:True"
|
||||
And the config for journal "default" should have "encrypt" set to "bool:True"
|
||||
When we run "jrnl -n 1" and enter "swordfish"
|
||||
Then the output should contain "Password"
|
||||
and the output should contain "2013-06-10 15:40 Life is good"
|
||||
And the output should contain "2013-06-10 15:40 Life is good"
|
||||
|
||||
Scenario: Mistyping your password
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl --encrypt" and enter
|
||||
"""
|
||||
swordfish
|
||||
sordfish
|
||||
swordfish
|
||||
swordfish
|
||||
n
|
||||
"""
|
||||
Then we should see the message "Passwords did not match"
|
||||
And we should see the message "Journal encrypted"
|
||||
And the config for journal "default" should have "encrypt" set to "bool:True"
|
||||
When we run "jrnl -n 1" and enter "swordfish"
|
||||
Then the output should contain "Password"
|
||||
And the output should contain "2013-06-10 15:40 Life is good"
|
||||
|
||||
Scenario: Storing a password in Keychain
|
||||
Given we use the config "multiple.yaml"
|
||||
When we run "jrnl simple --encrypt" and enter "sabertooth" and "y"
|
||||
When we run "jrnl simple --encrypt" and enter
|
||||
"""
|
||||
sabertooth
|
||||
sabertooth
|
||||
y
|
||||
"""
|
||||
When we set the keychain password of "simple" to "sabertooth"
|
||||
Then the config for journal "simple" should have "encrypt" set to "bool:True"
|
||||
When we run "jrnl simple -n 1"
|
||||
Then we should not see the message "Password"
|
||||
and the output should contain "2013-06-10 15:40 Life is good"
|
||||
Then the output should contain "2013-06-10 15:40 Life is good"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import shutil
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def before_feature(context, feature):
|
||||
|
@ -9,6 +10,10 @@ def before_feature(context, feature):
|
|||
feature.skip("Marked with @skip")
|
||||
return
|
||||
|
||||
if "skip_win" in feature.tags and "win32" in sys.platform:
|
||||
feature.skip("Skipping on Windows")
|
||||
return
|
||||
|
||||
|
||||
def before_scenario(context, scenario):
|
||||
"""Before each scenario, backup all config and journal test data."""
|
||||
|
@ -36,6 +41,10 @@ def before_scenario(context, scenario):
|
|||
scenario.skip("Marked with @skip")
|
||||
return
|
||||
|
||||
if "skip_win" in scenario.effective_tags and "win32" in sys.platform:
|
||||
scenario.skip("Skipping on Windows")
|
||||
return
|
||||
|
||||
|
||||
def after_scenario(context, scenario):
|
||||
"""After each scenario, restore all test data and remove working_dirs."""
|
||||
|
|
|
@ -4,21 +4,20 @@ Feature: Exporting a Journal
|
|||
Given we use the config "tags.yaml"
|
||||
When we run "jrnl --export json"
|
||||
Then we should get no error
|
||||
and the output should be parsable as json
|
||||
and "entries" in the json output should have 2 elements
|
||||
and "tags" in the json output should contain "@idea"
|
||||
and "tags" in the json output should contain "@journal"
|
||||
and "tags" in the json output should contain "@dan"
|
||||
And the output should be parsable as json
|
||||
And "entries" in the json output should have 2 elements
|
||||
And "tags" in the json output should contain "@idea"
|
||||
And "tags" in the json output should contain "@journal"
|
||||
And "tags" in the json output should contain "@dan"
|
||||
|
||||
Scenario: Exporting using filters should only export parts of the journal
|
||||
Given we use the config "tags.yaml"
|
||||
When we run "jrnl -until 'may 2013' --export json"
|
||||
# Then we should get no error
|
||||
Then the output should be parsable as json
|
||||
and "entries" in the json output should have 1 element
|
||||
and "tags" in the json output should contain "@idea"
|
||||
and "tags" in the json output should contain "@journal"
|
||||
and "tags" in the json output should not contain "@dan"
|
||||
And "entries" in the json output should have 1 element
|
||||
And "tags" in the json output should contain "@idea"
|
||||
And "tags" in the json output should contain "@journal"
|
||||
And "tags" in the json output should not contain "@dan"
|
||||
|
||||
Scenario: Exporting using custom templates
|
||||
Given we use the config "basic.yaml"
|
||||
|
@ -83,3 +82,57 @@ Feature: Exporting a Journal
|
|||
More stuff
|
||||
more stuff again
|
||||
"""
|
||||
|
||||
Scenario: Exporting to XML
|
||||
Given we use the config "tags.yaml"
|
||||
When we run "jrnl --export xml"
|
||||
Then the output should be a valid XML string
|
||||
And "entries" node in the xml output should have 2 elements
|
||||
And "tags" in the xml output should contain ["@idea", "@journal", "@dan"]
|
||||
|
||||
Scenario: Exporting tags
|
||||
Given we use the config "tags.yaml"
|
||||
When we run "jrnl --export tags"
|
||||
Then the output should be
|
||||
"""
|
||||
@idea : 2
|
||||
@journal : 1
|
||||
@dan : 1
|
||||
"""
|
||||
|
||||
Scenario: Exporting fancy
|
||||
Given we use the config "tags.yaml"
|
||||
When we run "jrnl --export fancy"
|
||||
Then the output should be
|
||||
"""
|
||||
┎──────────────────────────────────────────────────────────────╮2013-04-09 15:39
|
||||
┃ I have an @idea: ╘═══════════════╕
|
||||
┠╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
┃ (1) write a command line @journal software │
|
||||
┃ (2) ??? │
|
||||
┃ (3) PROFIT! │
|
||||
┖──────────────────────────────────────────────────────────────────────────────┘
|
||||
┎──────────────────────────────────────────────────────────────╮2013-06-10 15:40
|
||||
┃ I met with @dan. ╘═══════════════╕
|
||||
┠╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
┃ As alway's he shared his latest @idea on how to rule the world with me. │
|
||||
┃ inst │
|
||||
┖──────────────────────────────────────────────────────────────────────────────┘
|
||||
"""
|
||||
|
||||
Scenario: Export to yaml
|
||||
Given we use the config "tags.yaml"
|
||||
And we created a directory named "exported_journal"
|
||||
When we run "jrnl --export yaml -o exported_journal"
|
||||
Then "exported_journal" should contain the files ["2013-04-09_i-have-an-idea.md", "2013-06-10_i-met-with-dan.md"]
|
||||
And the content of exported yaml "exported_journal/2013-04-09_i-have-an-idea.md" should be
|
||||
"""
|
||||
title: I have an @idea:
|
||||
date: 2013-04-09 15:39
|
||||
stared: False
|
||||
tags: idea, journal
|
||||
|
||||
(1) write a command line @journal software
|
||||
(2) ???
|
||||
(3) PROFIT!
|
||||
"""
|
||||
|
|
|
@ -42,5 +42,10 @@ Feature: Multiple journals
|
|||
|
||||
Scenario: Don't crash if no file exists for a configured encrypted journal
|
||||
Given we use the config "multiple.yaml"
|
||||
When we run "jrnl new_encrypted Adding first entry" and enter "these three eyes" and "y"
|
||||
Then we should see the message "Journal 'new_encrypted' created"
|
||||
When we run "jrnl new_encrypted Adding first entry" and enter
|
||||
"""
|
||||
these three eyes
|
||||
these three eyes
|
||||
n
|
||||
"""
|
||||
Then we should see the message "Encrypted journal 'new_encrypted' created"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Feature: Zapped bugs should stay dead.
|
||||
|
||||
Scenario: Writing an entry does not print the entire journal
|
||||
# https://github.com/maebert/jrnl/issues/87
|
||||
# https://github.com/jrnl-org/jrnl/issues/87
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl 23 july 2013: A cold and stormy day. I ate crisps on the sofa."
|
||||
Then we should see the message "Entry added"
|
||||
|
@ -9,21 +9,14 @@ Feature: Zapped bugs should stay dead.
|
|||
Then the output should not contain "Life is good"
|
||||
|
||||
Scenario: Date with time should be parsed correctly
|
||||
# https://github.com/maebert/jrnl/issues/117
|
||||
# https://github.com/jrnl-org/jrnl/issues/117
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl 2013-11-30 15:42: Project Started."
|
||||
Then we should see the message "Entry added"
|
||||
and the journal should contain "[2013-11-30 15:42] Project Started."
|
||||
|
||||
Scenario: Date in the future should be parsed correctly
|
||||
# https://github.com/maebert/jrnl/issues/185
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl 26/06/2019: Planet? Earth. Year? 2019."
|
||||
Then we should see the message "Entry added"
|
||||
and the journal should contain "[2019-06-26 09:00] Planet?"
|
||||
|
||||
Scenario: Loading entry with ambiguous time stamp
|
||||
#https://github.com/maebert/jrnl/issues/153
|
||||
#https://github.com/jrnl-org/jrnl/issues/153
|
||||
Given we use the config "bug153.yaml"
|
||||
When we run "jrnl -1"
|
||||
Then we should get no error
|
||||
|
@ -32,6 +25,19 @@ Feature: Zapped bugs should stay dead.
|
|||
2013-10-27 03:27 Some text.
|
||||
"""
|
||||
|
||||
Scenario: Date in the future should be parsed correctly
|
||||
# https://github.com/jrnl-org/jrnl/issues/185
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl 26/06/2019: Planet? Earth. Year? 2019."
|
||||
Then we should see the message "Entry added"
|
||||
and the journal should contain "[2019-06-26 09:00] Planet?"
|
||||
|
||||
Scenario: Empty DayOne entry bodies should not error
|
||||
# https://github.com/jrnl-org/jrnl/issues/780
|
||||
Given we use the config "bug780.yaml"
|
||||
When we run "jrnl --short"
|
||||
Then we should get no error
|
||||
|
||||
Scenario: Title with an embedded period.
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl 04-24-2014: Created a new website - empty.com. Hope to get a lot of traffic."
|
||||
|
|
|
@ -3,10 +3,12 @@ from unittest.mock import patch
|
|||
from behave import given, when, then
|
||||
from jrnl import cli, install, Journal, util, plugins
|
||||
from jrnl import __version__
|
||||
from dateutil import parser as date_parser
|
||||
from collections import defaultdict
|
||||
try: import parsedatetime.parsedatetime_consts as pdt
|
||||
except ImportError: import parsedatetime as pdt
|
||||
|
||||
try:
|
||||
import parsedatetime.parsedatetime_consts as pdt
|
||||
except ImportError:
|
||||
import parsedatetime as pdt
|
||||
import time
|
||||
import os
|
||||
import json
|
||||
|
@ -17,7 +19,7 @@ import shlex
|
|||
import sys
|
||||
|
||||
consts = pdt.Constants(usePyICU=False)
|
||||
consts.DOWParseStyle = -1 # Prefers past weekdays
|
||||
consts.DOWParseStyle = -1 # Prefers past weekdays
|
||||
CALENDAR = pdt.Calendar(consts)
|
||||
|
||||
|
||||
|
@ -33,7 +35,7 @@ class TestKeyring(keyring.backend.KeyringBackend):
|
|||
def get_password(self, servicename, username):
|
||||
return self.keys[servicename].get(username)
|
||||
|
||||
def delete_password(self, servicename, username, password):
|
||||
def delete_password(self, servicename, username):
|
||||
self.keys[servicename][username] = None
|
||||
|
||||
|
||||
|
@ -44,23 +46,27 @@ keyring.set_keyring(TestKeyring())
|
|||
def ushlex(command):
|
||||
if sys.version_info[0] == 3:
|
||||
return shlex.split(command)
|
||||
return map(lambda s: s.decode('UTF8'), shlex.split(command.encode('utf8')))
|
||||
return map(lambda s: s.decode("UTF8"), shlex.split(command.encode("utf8")))
|
||||
|
||||
|
||||
def read_journal(journal_name="default"):
|
||||
config = util.load_config(install.CONFIG_FILE_PATH)
|
||||
with open(config['journals'][journal_name]) as journal_file:
|
||||
with open(config["journals"][journal_name]) as journal_file:
|
||||
journal = journal_file.read()
|
||||
return journal
|
||||
|
||||
|
||||
def open_journal(journal_name="default"):
|
||||
config = util.load_config(install.CONFIG_FILE_PATH)
|
||||
journal_conf = config['journals'][journal_name]
|
||||
if type(journal_conf) is dict: # We can override the default config on a by-journal basis
|
||||
journal_conf = config["journals"][journal_name]
|
||||
|
||||
if type(journal_conf) is dict:
|
||||
# We can override the default config on a by-journal basis
|
||||
config.update(journal_conf)
|
||||
else: # But also just give them a string to point to the journal file
|
||||
config['journal'] = journal_conf
|
||||
else:
|
||||
# But also just give them a string to point to the journal file
|
||||
config["journal"] = journal_conf
|
||||
|
||||
return Journal.open_journal(journal_name, config)
|
||||
|
||||
|
||||
|
@ -70,14 +76,15 @@ def set_config(context, config_file):
|
|||
install.CONFIG_FILE_PATH = os.path.abspath(full_path)
|
||||
if config_file.endswith("yaml"):
|
||||
# Add jrnl version to file for 2.x journals
|
||||
with open(install.CONFIG_FILE_PATH, 'a') as cf:
|
||||
with open(install.CONFIG_FILE_PATH, "a") as cf:
|
||||
cf.write("version: {}".format(__version__))
|
||||
|
||||
|
||||
@when('we open the editor and enter ""')
|
||||
@when('we open the editor and enter "{text}"')
|
||||
def open_editor_and_enter(context, text=""):
|
||||
text = (text or context.text)
|
||||
text = text or context.text
|
||||
|
||||
def _mock_editor_function(command):
|
||||
tmpfile = command[-1]
|
||||
with open(tmpfile, "w+") as f:
|
||||
|
@ -88,7 +95,7 @@ def open_editor_and_enter(context, text=""):
|
|||
|
||||
return tmpfile
|
||||
|
||||
with patch('subprocess.call', side_effect=_mock_editor_function):
|
||||
with patch("subprocess.call", side_effect=_mock_editor_function):
|
||||
run(context, "jrnl")
|
||||
|
||||
|
||||
|
@ -96,6 +103,7 @@ def _mock_getpass(inputs):
|
|||
def prompt_return(prompt="Password: "):
|
||||
print(prompt)
|
||||
return next(inputs)
|
||||
|
||||
return prompt_return
|
||||
|
||||
|
||||
|
@ -104,35 +112,44 @@ def _mock_input(inputs):
|
|||
val = next(inputs)
|
||||
print(prompt, val)
|
||||
return val
|
||||
|
||||
return prompt_return
|
||||
|
||||
|
||||
@when('we run "{command}" and enter')
|
||||
@when('we run "{command}" and enter ""')
|
||||
@when('we run "{command}" and enter "{inputs1}"')
|
||||
@when('we run "{command}" and enter "{inputs1}" and "{inputs2}"')
|
||||
def run_with_input(context, command, inputs1="", inputs2=""):
|
||||
@when('we run "{command}" and enter "{inputs}"')
|
||||
def run_with_input(context, command, inputs=""):
|
||||
# create an iterator through all inputs. These inputs will be fed one by one
|
||||
# to the mocked calls for 'input()', 'util.getpass()' and 'sys.stdin.read()'
|
||||
if inputs1:
|
||||
text = iter((inputs1, inputs2))
|
||||
elif context.text:
|
||||
if context.text:
|
||||
text = iter(context.text.split("\n"))
|
||||
else:
|
||||
text = iter(("", ""))
|
||||
text = iter([inputs])
|
||||
|
||||
args = ushlex(command)[1:]
|
||||
with patch("builtins.input", side_effect=_mock_input(text)) as mock_input:
|
||||
with patch("jrnl.util.getpass", side_effect=_mock_getpass(text)) as mock_getpass:
|
||||
with patch("sys.stdin.read", side_effect=text) as mock_read:
|
||||
try:
|
||||
cli.run(args or [])
|
||||
context.exit_status = 0
|
||||
except SystemExit as e:
|
||||
context.exit_status = e.code
|
||||
|
||||
# assert at least one of the mocked input methods got called
|
||||
assert mock_input.called or mock_getpass.called or mock_read.called
|
||||
# fmt: off
|
||||
# see: https://github.com/psf/black/issues/557
|
||||
with patch("builtins.input", side_effect=_mock_input(text)) as mock_input, \
|
||||
patch("getpass.getpass", side_effect=_mock_getpass(text)) as mock_getpass, \
|
||||
patch("sys.stdin.read", side_effect=text) as mock_read:
|
||||
|
||||
try:
|
||||
cli.run(args or [])
|
||||
context.exit_status = 0
|
||||
except SystemExit as e:
|
||||
context.exit_status = e.code
|
||||
|
||||
# at least one of the mocked input methods got called
|
||||
assert mock_input.called or mock_getpass.called or mock_read.called
|
||||
# all inputs were used
|
||||
try:
|
||||
next(text)
|
||||
assert False, "Not all inputs were consumed"
|
||||
except StopIteration:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
@when('we run "{command}"')
|
||||
|
@ -154,74 +171,32 @@ def load_template(context, filename):
|
|||
|
||||
@when('we set the keychain password of "{journal}" to "{password}"')
|
||||
def set_keychain(context, journal, password):
|
||||
keyring.set_password('jrnl', journal, password)
|
||||
keyring.set_password("jrnl", journal, password)
|
||||
|
||||
|
||||
@then('we should get an error')
|
||||
@then("we should get an error")
|
||||
def has_error(context):
|
||||
assert context.exit_status != 0, context.exit_status
|
||||
|
||||
|
||||
@then('we should get no error')
|
||||
@then("we should get no error")
|
||||
def no_error(context):
|
||||
assert context.exit_status is 0, context.exit_status
|
||||
assert context.exit_status == 0, context.exit_status
|
||||
|
||||
|
||||
@then('the output should be parsable as json')
|
||||
def check_output_json(context):
|
||||
out = context.stdout_capture.getvalue()
|
||||
assert json.loads(out), out
|
||||
|
||||
|
||||
@then('"{field}" in the json output should have {number:d} elements')
|
||||
@then('"{field}" in the json output should have 1 element')
|
||||
def check_output_field(context, field, number=1):
|
||||
out = context.stdout_capture.getvalue()
|
||||
out_json = json.loads(out)
|
||||
assert field in out_json, [field, out_json]
|
||||
assert len(out_json[field]) == number, len(out_json[field])
|
||||
|
||||
|
||||
@then('"{field}" in the json output should not contain "{key}"')
|
||||
def check_output_field_not_key(context, field, key):
|
||||
out = context.stdout_capture.getvalue()
|
||||
out_json = json.loads(out)
|
||||
assert field in out_json
|
||||
assert key not in out_json[field]
|
||||
|
||||
|
||||
@then('"{field}" in the json output should contain "{key}"')
|
||||
def check_output_field_key(context, field, key):
|
||||
out = context.stdout_capture.getvalue()
|
||||
out_json = json.loads(out)
|
||||
assert field in out_json
|
||||
assert key in out_json[field]
|
||||
|
||||
|
||||
@then('the json output should contain {path} = "{value}"')
|
||||
def check_json_output_path(context, path, value):
|
||||
""" E.g.
|
||||
the json output should contain entries.0.title = "hello"
|
||||
"""
|
||||
out = context.stdout_capture.getvalue()
|
||||
struct = json.loads(out)
|
||||
|
||||
for node in path.split('.'):
|
||||
try:
|
||||
struct = struct[int(node)]
|
||||
except ValueError:
|
||||
struct = struct[node]
|
||||
assert struct == value, struct
|
||||
|
||||
|
||||
@then('the output should be')
|
||||
@then("the output should be")
|
||||
@then('the output should be "{text}"')
|
||||
def check_output(context, text=None):
|
||||
text = (text or context.text).strip().splitlines()
|
||||
out = context.stdout_capture.getvalue().strip().splitlines()
|
||||
assert len(text) == len(out), "Output has {} lines (expected: {})".format(len(out), len(text))
|
||||
assert len(text) == len(out), "Output has {} lines (expected: {})".format(
|
||||
len(out), len(text)
|
||||
)
|
||||
for line_text, line_out in zip(text, out):
|
||||
assert line_text.strip() == line_out.strip(), [line_text.strip(), line_out.strip()]
|
||||
assert line_text.strip() == line_out.strip(), [
|
||||
line_text.strip(),
|
||||
line_out.strip(),
|
||||
]
|
||||
|
||||
|
||||
@then('the output should contain "{text}" in the local time')
|
||||
|
@ -229,11 +204,11 @@ def check_output_time_inline(context, text):
|
|||
out = context.stdout_capture.getvalue()
|
||||
local_tz = tzlocal.get_localzone()
|
||||
date, flag = CALENDAR.parse(text)
|
||||
output_date = time.strftime("%Y-%m-%d %H:%M",date)
|
||||
output_date = time.strftime("%Y-%m-%d %H:%M", date)
|
||||
assert output_date in out, output_date
|
||||
|
||||
|
||||
@then('the output should contain')
|
||||
@then("the output should contain")
|
||||
@then('the output should contain "{text}"')
|
||||
def check_output_inline(context, text=None):
|
||||
text = text or context.text
|
||||
|
@ -270,7 +245,7 @@ def check_journal_content(context, text, journal_name="default"):
|
|||
def journal_doesnt_exist(context, journal_name="default"):
|
||||
with open(install.CONFIG_FILE_PATH) as config_file:
|
||||
config = yaml.load(config_file, Loader=yaml.FullLoader)
|
||||
journal_path = config['journals'][journal_name]
|
||||
journal_path = config["journals"][journal_name]
|
||||
assert not os.path.exists(journal_path)
|
||||
|
||||
|
||||
|
@ -278,11 +253,7 @@ def journal_doesnt_exist(context, journal_name="default"):
|
|||
@then('the config for journal "{journal}" should have "{key}" set to "{value}"')
|
||||
def config_var(context, key, value, journal=None):
|
||||
t, value = value.split(":")
|
||||
value = {
|
||||
"bool": lambda v: v.lower() == "true",
|
||||
"int": int,
|
||||
"str": str
|
||||
}[t](value)
|
||||
value = {"bool": lambda v: v.lower() == "true", "int": int, "str": str}[t](value)
|
||||
config = util.load_config(install.CONFIG_FILE_PATH)
|
||||
if journal:
|
||||
config = config["journals"][journal]
|
||||
|
@ -290,8 +261,8 @@ def config_var(context, key, value, journal=None):
|
|||
assert config[key] == value
|
||||
|
||||
|
||||
@then('the journal should have {number:d} entries')
|
||||
@then('the journal should have {number:d} entry')
|
||||
@then("the journal should have {number:d} entries")
|
||||
@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_entries(context, number, journal_name="default"):
|
||||
|
@ -299,6 +270,6 @@ def check_journal_entries(context, number, journal_name="default"):
|
|||
assert len(journal.entries) == number
|
||||
|
||||
|
||||
@then('fail')
|
||||
@then("fail")
|
||||
def debug_fail(context):
|
||||
assert False
|
||||
|
|
124
features/steps/export_steps.py
Normal file
124
features/steps/export_steps.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
import json
|
||||
import os
|
||||
import shutil
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from behave import then, given
|
||||
|
||||
|
||||
@then("the output should be parsable as json")
|
||||
def check_output_json(context):
|
||||
out = context.stdout_capture.getvalue()
|
||||
assert json.loads(out), out
|
||||
|
||||
|
||||
@then('"{field}" in the json output should have {number:d} elements')
|
||||
@then('"{field}" in the json output should have 1 element')
|
||||
def check_output_field(context, field, number=1):
|
||||
out = context.stdout_capture.getvalue()
|
||||
out_json = json.loads(out)
|
||||
assert field in out_json, [field, out_json]
|
||||
assert len(out_json[field]) == number, len(out_json[field])
|
||||
|
||||
|
||||
@then('"{field}" in the json output should not contain "{key}"')
|
||||
def check_output_field_not_key(context, field, key):
|
||||
out = context.stdout_capture.getvalue()
|
||||
out_json = json.loads(out)
|
||||
assert field in out_json
|
||||
assert key not in out_json[field]
|
||||
|
||||
|
||||
@then('"{field}" in the json output should contain "{key}"')
|
||||
def check_output_field_key(context, field, key):
|
||||
out = context.stdout_capture.getvalue()
|
||||
out_json = json.loads(out)
|
||||
assert field in out_json
|
||||
assert key in out_json[field]
|
||||
|
||||
|
||||
@then('the json output should contain {path} = "{value}"')
|
||||
def check_json_output_path(context, path, value):
|
||||
""" E.g.
|
||||
the json output should contain entries.0.title = "hello"
|
||||
"""
|
||||
out = context.stdout_capture.getvalue()
|
||||
struct = json.loads(out)
|
||||
|
||||
for node in path.split("."):
|
||||
try:
|
||||
struct = struct[int(node)]
|
||||
except ValueError:
|
||||
struct = struct[node]
|
||||
assert struct == value, struct
|
||||
|
||||
|
||||
@then("the output should be a valid XML string")
|
||||
def assert_valid_xml_string(context):
|
||||
output = context.stdout_capture.getvalue()
|
||||
xml_tree = ElementTree.fromstring(output)
|
||||
assert xml_tree, output
|
||||
|
||||
|
||||
@then('"entries" node in the xml output should have {number:d} elements')
|
||||
def assert_xml_output_entries_count(context, number):
|
||||
output = context.stdout_capture.getvalue()
|
||||
xml_tree = ElementTree.fromstring(output)
|
||||
|
||||
xml_tags = (node.tag for node in xml_tree)
|
||||
assert "entries" in xml_tags, str(list(xml_tags))
|
||||
|
||||
actual_entry_count = len(xml_tree.find("entries"))
|
||||
assert actual_entry_count == number, actual_entry_count
|
||||
|
||||
|
||||
@then('"tags" in the xml output should contain {expected_tags_json_list}')
|
||||
def assert_xml_output_tags(context, expected_tags_json_list):
|
||||
output = context.stdout_capture.getvalue()
|
||||
xml_tree = ElementTree.fromstring(output)
|
||||
|
||||
xml_tags = (node.tag for node in xml_tree)
|
||||
assert "tags" in xml_tags, str(list(xml_tags))
|
||||
|
||||
expected_tags = json.loads(expected_tags_json_list)
|
||||
actual_tags = set(t.attrib["name"] for t in xml_tree.find("tags"))
|
||||
assert actual_tags == set(expected_tags), [actual_tags, set(expected_tags)]
|
||||
|
||||
|
||||
@given('we created a directory named "{dir_name}"')
|
||||
def create_directory(context, dir_name):
|
||||
if os.path.exists(dir_name):
|
||||
shutil.rmtree(dir_name)
|
||||
os.mkdir(dir_name)
|
||||
|
||||
|
||||
@then('"{dir_name}" should contain the files {expected_files_json_list}')
|
||||
def assert_dir_contains_files(context, dir_name, expected_files_json_list):
|
||||
actual_files = os.listdir(dir_name)
|
||||
expected_files = json.loads(expected_files_json_list)
|
||||
assert actual_files == expected_files, [actual_files, expected_files]
|
||||
|
||||
|
||||
@then('the content of exported yaml "{file_path}" should be')
|
||||
def assert_exported_yaml_file_content(context, file_path):
|
||||
expected_content = context.text.strip().splitlines()
|
||||
|
||||
with open(file_path, "r") as f:
|
||||
actual_content = f.read().strip().splitlines()
|
||||
|
||||
for actual_line, expected_line in zip(actual_content, expected_content):
|
||||
if actual_line.startswith("tags: ") and expected_line.startswith("tags: "):
|
||||
assert_equal_tags_ignoring_order(actual_line, expected_line)
|
||||
else:
|
||||
assert actual_line.strip() == expected_line.strip(), [
|
||||
actual_line.strip(),
|
||||
expected_line.strip(),
|
||||
]
|
||||
|
||||
|
||||
def assert_equal_tags_ignoring_order(actual_line, expected_line):
|
||||
actual_tags = set(tag.strip() for tag in actual_line[len("tags: ") :].split(","))
|
||||
expected_tags = set(
|
||||
tag.strip() for tag in expected_line[len("tags: ") :].split(",")
|
||||
)
|
||||
assert actual_tags == expected_tags, [actual_tags, expected_tags]
|
|
@ -19,7 +19,11 @@ class DayOne(Journal.Journal):
|
|||
"""A special Journal handling DayOne files"""
|
||||
|
||||
# InvalidFileException was added to plistlib in Python3.4
|
||||
PLIST_EXCEPTIONS = (ExpatError, plistlib.InvalidFileException) if hasattr(plistlib, "InvalidFileException") else ExpatError
|
||||
PLIST_EXCEPTIONS = (
|
||||
(ExpatError, plistlib.InvalidFileException)
|
||||
if hasattr(plistlib, "InvalidFileException")
|
||||
else ExpatError
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.entries = []
|
||||
|
@ -27,28 +31,43 @@ class DayOne(Journal.Journal):
|
|||
super().__init__(**kwargs)
|
||||
|
||||
def open(self):
|
||||
filenames = [os.path.join(self.config['journal'], "entries", f) for f in os.listdir(os.path.join(self.config['journal'], "entries"))]
|
||||
filenames = [
|
||||
os.path.join(self.config["journal"], "entries", f)
|
||||
for f in os.listdir(os.path.join(self.config["journal"], "entries"))
|
||||
]
|
||||
filenames = []
|
||||
for root, dirnames, f in os.walk(self.config['journal']):
|
||||
for filename in fnmatch.filter(f, '*.doentry'):
|
||||
for root, dirnames, f in os.walk(self.config["journal"]):
|
||||
for filename in fnmatch.filter(f, "*.doentry"):
|
||||
filenames.append(os.path.join(root, filename))
|
||||
self.entries = []
|
||||
for filename in filenames:
|
||||
with open(filename, 'rb') as plist_entry:
|
||||
with open(filename, "rb") as plist_entry:
|
||||
try:
|
||||
dict_entry = plistlib.readPlist(plist_entry)
|
||||
except self.PLIST_EXCEPTIONS:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
timezone = pytz.timezone(dict_entry['Time Zone'])
|
||||
timezone = pytz.timezone(dict_entry["Time Zone"])
|
||||
except (KeyError, pytz.exceptions.UnknownTimeZoneError):
|
||||
timezone = tzlocal.get_localzone()
|
||||
date = dict_entry['Creation Date']
|
||||
date = date + timezone.utcoffset(date, is_dst=False)
|
||||
entry = Entry.Entry(self, date, text=dict_entry['Entry Text'], starred=dict_entry["Starred"])
|
||||
date = dict_entry["Creation Date"]
|
||||
# convert the date to UTC rather than keep messing with
|
||||
# timezones
|
||||
if timezone.zone != "UTC":
|
||||
date = date + timezone.utcoffset(date, is_dst=False)
|
||||
|
||||
entry = Entry.Entry(
|
||||
self,
|
||||
date,
|
||||
text=dict_entry["Entry Text"],
|
||||
starred=dict_entry["Starred"],
|
||||
)
|
||||
entry.uuid = dict_entry["UUID"]
|
||||
entry._tags = [self.config['tagsymbols'][0] + tag.lower() for tag in dict_entry.get("Tags", [])]
|
||||
entry._tags = [
|
||||
self.config["tagsymbols"][0] + tag.lower()
|
||||
for tag in dict_entry.get("Tags", [])
|
||||
]
|
||||
|
||||
self.entries.append(entry)
|
||||
self.sort()
|
||||
|
@ -58,24 +77,33 @@ class DayOne(Journal.Journal):
|
|||
"""Writes only the entries that have been modified into plist files."""
|
||||
for entry in self.entries:
|
||||
if entry.modified:
|
||||
utc_time = datetime.utcfromtimestamp(time.mktime(entry.date.timetuple()))
|
||||
utc_time = datetime.utcfromtimestamp(
|
||||
time.mktime(entry.date.timetuple())
|
||||
)
|
||||
|
||||
if not hasattr(entry, "uuid"):
|
||||
entry.uuid = uuid.uuid1().hex
|
||||
|
||||
filename = os.path.join(self.config['journal'], "entries", entry.uuid.upper() + ".doentry")
|
||||
filename = os.path.join(
|
||||
self.config["journal"], "entries", entry.uuid.upper() + ".doentry"
|
||||
)
|
||||
|
||||
entry_plist = {
|
||||
'Creation Date': utc_time,
|
||||
'Starred': entry.starred if hasattr(entry, 'starred') else False,
|
||||
'Entry Text': entry.title + "\n" + entry.body,
|
||||
'Time Zone': str(tzlocal.get_localzone()),
|
||||
'UUID': entry.uuid.upper(),
|
||||
'Tags': [tag.strip(self.config['tagsymbols']).replace("_", " ") for tag in entry.tags]
|
||||
"Creation Date": utc_time,
|
||||
"Starred": entry.starred if hasattr(entry, "starred") else False,
|
||||
"Entry Text": entry.title + "\n" + entry.body,
|
||||
"Time Zone": str(tzlocal.get_localzone()),
|
||||
"UUID": entry.uuid.upper(),
|
||||
"Tags": [
|
||||
tag.strip(self.config["tagsymbols"]).replace("_", " ")
|
||||
for tag in entry.tags
|
||||
],
|
||||
}
|
||||
plistlib.writePlist(entry_plist, filename)
|
||||
for entry in self._deleted_entries:
|
||||
filename = os.path.join(self.config['journal'], "entries", entry.uuid + ".doentry")
|
||||
filename = os.path.join(
|
||||
self.config["journal"], "entries", entry.uuid + ".doentry"
|
||||
)
|
||||
os.remove(filename)
|
||||
|
||||
def editable_str(self):
|
||||
|
@ -113,7 +141,7 @@ class DayOne(Journal.Journal):
|
|||
if line.endswith("*"):
|
||||
current_entry.starred = True
|
||||
line = line[:-1]
|
||||
current_entry.title = line[len(date_blob) - 1:]
|
||||
current_entry.title = line[len(date_blob) - 1 :]
|
||||
current_entry.date = new_date
|
||||
elif current_entry:
|
||||
current_entry.body += line + "\n"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from . import Journal, util
|
||||
from . import util
|
||||
from .Journal import Journal, LegacyJournal
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
from cryptography.hazmat.primitives import hashes, padding
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
|
@ -9,6 +10,8 @@ import sys
|
|||
import os
|
||||
import base64
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
|
@ -19,107 +22,111 @@ def make_key(password):
|
|||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
# Salt is hard-coded
|
||||
salt=b'\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8',
|
||||
salt=b"\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8",
|
||||
iterations=100000,
|
||||
backend=default_backend()
|
||||
backend=default_backend(),
|
||||
)
|
||||
key = kdf.derive(password)
|
||||
return base64.urlsafe_b64encode(key)
|
||||
|
||||
|
||||
class EncryptedJournal(Journal.Journal):
|
||||
def __init__(self, name='default', **kwargs):
|
||||
class EncryptedJournal(Journal):
|
||||
def __init__(self, name="default", **kwargs):
|
||||
super().__init__(name, **kwargs)
|
||||
self.config['encrypt'] = True
|
||||
self.config["encrypt"] = True
|
||||
self.password = None
|
||||
|
||||
def open(self, filename=None):
|
||||
"""Opens the journal file defined in the config and parses it into a list of Entries.
|
||||
Entries have the form (date, title, body)."""
|
||||
filename = filename or self.config['journal']
|
||||
filename = filename or self.config["journal"]
|
||||
|
||||
if not os.path.exists(filename):
|
||||
password = util.getpass("Enter password for new journal: ")
|
||||
if password:
|
||||
if util.yesno("Do you want to store the password in your keychain?", default=True):
|
||||
util.set_keychain(self.name, password)
|
||||
else:
|
||||
util.set_keychain(self.name, None)
|
||||
self.config['password'] = password
|
||||
text = ""
|
||||
self._store(filename, text)
|
||||
print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr)
|
||||
else:
|
||||
print("No password supplied for encrypted journal", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
text = self._load(filename)
|
||||
self.create_file(filename)
|
||||
self.password = util.create_password(self.name)
|
||||
print(
|
||||
f"Encrypted journal '{self.name}' created at {filename}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
text = self._load(filename)
|
||||
self.entries = self._parse(text)
|
||||
self.sort()
|
||||
log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
|
||||
return self
|
||||
|
||||
def _load(self, filename, password=None):
|
||||
def _load(self, filename):
|
||||
"""Loads an encrypted journal from a file and tries to decrypt it.
|
||||
If password is not provided, will look for password in the keychain
|
||||
and otherwise ask the user to enter a password up to three times.
|
||||
If the password is provided but wrong (or corrupt), this will simply
|
||||
return None."""
|
||||
with open(filename, 'rb') as f:
|
||||
with open(filename, "rb") as f:
|
||||
journal_encrypted = f.read()
|
||||
|
||||
def validate_password(password):
|
||||
def decrypt_journal(password):
|
||||
key = make_key(password)
|
||||
try:
|
||||
plain = Fernet(key).decrypt(journal_encrypted).decode('utf-8')
|
||||
self.config['password'] = password
|
||||
plain = Fernet(key).decrypt(journal_encrypted).decode("utf-8")
|
||||
self.password = password
|
||||
return plain
|
||||
except (InvalidToken, IndexError):
|
||||
return None
|
||||
if password:
|
||||
return validate_password(password)
|
||||
return util.get_password(keychain=self.name, validator=validate_password)
|
||||
|
||||
if self.password:
|
||||
return decrypt_journal(self.password)
|
||||
|
||||
return util.decrypt_content(keychain=self.name, decrypt_func=decrypt_journal)
|
||||
|
||||
def _store(self, filename, text):
|
||||
key = make_key(self.config['password'])
|
||||
journal = Fernet(key).encrypt(text.encode('utf-8'))
|
||||
with open(filename, 'wb') as f:
|
||||
key = make_key(self.password)
|
||||
journal = Fernet(key).encrypt(text.encode("utf-8"))
|
||||
with open(filename, "wb") as f:
|
||||
f.write(journal)
|
||||
|
||||
@classmethod
|
||||
def _create(cls, filename, password):
|
||||
key = make_key(password)
|
||||
dummy = Fernet(key).encrypt(b"")
|
||||
with open(filename, 'wb') as f:
|
||||
f.write(dummy)
|
||||
def from_journal(cls, other: Journal):
|
||||
new_journal = super().from_journal(other)
|
||||
new_journal.password = (
|
||||
other.password
|
||||
if hasattr(other, "password")
|
||||
else util.create_password(other.name)
|
||||
)
|
||||
return new_journal
|
||||
|
||||
|
||||
class LegacyEncryptedJournal(Journal.LegacyJournal):
|
||||
class LegacyEncryptedJournal(LegacyJournal):
|
||||
"""Legacy class to support opening journals encrypted with the jrnl 1.x
|
||||
standard. You'll not be able to save these journals anymore."""
|
||||
def __init__(self, name='default', **kwargs):
|
||||
super().__init__(name, **kwargs)
|
||||
self.config['encrypt'] = True
|
||||
|
||||
def _load(self, filename, password=None):
|
||||
with open(filename, 'rb') as f:
|
||||
def __init__(self, name="default", **kwargs):
|
||||
super().__init__(name, **kwargs)
|
||||
self.config["encrypt"] = True
|
||||
self.password = None
|
||||
|
||||
def _load(self, filename):
|
||||
with open(filename, "rb") as f:
|
||||
journal_encrypted = f.read()
|
||||
iv, cipher = journal_encrypted[:16], journal_encrypted[16:]
|
||||
|
||||
def validate_password(password):
|
||||
decryption_key = hashlib.sha256(password.encode('utf-8')).digest()
|
||||
decryptor = Cipher(algorithms.AES(decryption_key), modes.CBC(iv), default_backend()).decryptor()
|
||||
def decrypt_journal(password):
|
||||
decryption_key = hashlib.sha256(password.encode("utf-8")).digest()
|
||||
decryptor = Cipher(
|
||||
algorithms.AES(decryption_key), modes.CBC(iv), default_backend()
|
||||
).decryptor()
|
||||
try:
|
||||
plain_padded = decryptor.update(cipher) + decryptor.finalize()
|
||||
self.config['password'] = password
|
||||
self.password = password
|
||||
if plain_padded[-1] in (" ", 32):
|
||||
# Ancient versions of jrnl. Do not judge me.
|
||||
return plain_padded.decode('utf-8').rstrip(" ")
|
||||
return plain_padded.decode("utf-8").rstrip(" ")
|
||||
else:
|
||||
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
|
||||
plain = unpadder.update(plain_padded) + unpadder.finalize()
|
||||
return plain.decode('utf-8')
|
||||
return plain.decode("utf-8")
|
||||
except ValueError:
|
||||
return None
|
||||
if password:
|
||||
return validate_password(password)
|
||||
return util.get_password(keychain=self.name, validator=validate_password)
|
||||
|
||||
if self.password:
|
||||
return decrypt_journal(self.password)
|
||||
return util.decrypt_content(keychain=self.name, decrypt_func=decrypt_journal)
|
||||
|
|
|
@ -22,7 +22,7 @@ class Entry:
|
|||
def _parse_text(self):
|
||||
raw_text = self.text
|
||||
lines = raw_text.splitlines()
|
||||
if lines[0].strip().endswith("*"):
|
||||
if lines and lines[0].strip().endswith("*"):
|
||||
self.starred = True
|
||||
raw_text = lines[0].strip("\n *") + "\n" + "\n".join(lines[1:])
|
||||
self._title, self._body = split_title(raw_text)
|
||||
|
@ -49,72 +49,84 @@ class Entry:
|
|||
|
||||
@staticmethod
|
||||
def tag_regex(tagsymbols):
|
||||
pattern = fr'(?u)(?:^|\s)([{tagsymbols}][-+*#/\w]+)'
|
||||
pattern = fr"(?u)(?:^|\s)([{tagsymbols}][-+*#/\w]+)"
|
||||
return re.compile(pattern)
|
||||
|
||||
def _parse_tags(self):
|
||||
tagsymbols = self.journal.config['tagsymbols']
|
||||
return {tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text)}
|
||||
tagsymbols = self.journal.config["tagsymbols"]
|
||||
return {
|
||||
tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text)
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
"""Returns a string representation of the entry to be written into a journal file."""
|
||||
date_str = self.date.strftime(self.journal.config['timeformat'])
|
||||
date_str = self.date.strftime(self.journal.config["timeformat"])
|
||||
title = "[{}] {}".format(date_str, self.title.rstrip("\n "))
|
||||
if self.starred:
|
||||
title += " *"
|
||||
return "{title}{sep}{body}\n".format(
|
||||
title=title,
|
||||
sep="\n" if self.body.rstrip("\n ") else "",
|
||||
body=self.body.rstrip("\n ")
|
||||
body=self.body.rstrip("\n "),
|
||||
)
|
||||
|
||||
def pprint(self, short=False):
|
||||
"""Returns a pretty-printed version of the entry.
|
||||
If short is true, only print the title."""
|
||||
date_str = self.date.strftime(self.journal.config['timeformat'])
|
||||
if self.journal.config['indent_character']:
|
||||
indent = self.journal.config['indent_character'].rstrip() + " "
|
||||
date_str = self.date.strftime(self.journal.config["timeformat"])
|
||||
if self.journal.config["indent_character"]:
|
||||
indent = self.journal.config["indent_character"].rstrip() + " "
|
||||
else:
|
||||
indent = ""
|
||||
if not short and self.journal.config['linewrap']:
|
||||
title = textwrap.fill(date_str + " " + self.title, self.journal.config['linewrap'])
|
||||
body = "\n".join([
|
||||
textwrap.fill(
|
||||
line,
|
||||
self.journal.config['linewrap'],
|
||||
initial_indent=indent,
|
||||
subsequent_indent=indent,
|
||||
drop_whitespace=True) or indent
|
||||
for line in self.body.rstrip(" \n").splitlines()
|
||||
])
|
||||
if not short and self.journal.config["linewrap"]:
|
||||
title = textwrap.fill(
|
||||
date_str + " " + self.title, self.journal.config["linewrap"]
|
||||
)
|
||||
body = "\n".join(
|
||||
[
|
||||
textwrap.fill(
|
||||
line,
|
||||
self.journal.config["linewrap"],
|
||||
initial_indent=indent,
|
||||
subsequent_indent=indent,
|
||||
drop_whitespace=True,
|
||||
)
|
||||
or indent
|
||||
for line in self.body.rstrip(" \n").splitlines()
|
||||
]
|
||||
)
|
||||
else:
|
||||
title = date_str + " " + self.title.rstrip("\n ")
|
||||
body = self.body.rstrip("\n ")
|
||||
|
||||
# Suppress bodies that are just blanks and new lines.
|
||||
has_body = len(self.body) > 20 or not all(char in (" ", "\n") for char in self.body)
|
||||
has_body = len(self.body) > 20 or not all(
|
||||
char in (" ", "\n") for char in self.body
|
||||
)
|
||||
|
||||
if short:
|
||||
return title
|
||||
else:
|
||||
return "{title}{sep}{body}\n".format(
|
||||
title=title,
|
||||
sep="\n" if has_body else "",
|
||||
body=body if has_body else "",
|
||||
title=title, sep="\n" if has_body else "", body=body if has_body else ""
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Entry '{}' on {}>".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M"))
|
||||
return "<Entry '{}' on {}>".format(
|
||||
self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M")
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__repr__())
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Entry) \
|
||||
or self.title.strip() != other.title.strip() \
|
||||
or self.body.rstrip() != other.body.rstrip() \
|
||||
or self.date != other.date \
|
||||
or self.starred != other.starred:
|
||||
if (
|
||||
not isinstance(other, Entry)
|
||||
or self.title.strip() != other.title.strip()
|
||||
or self.body.rstrip() != other.body.rstrip()
|
||||
or self.date != other.date
|
||||
or self.starred != other.starred
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
|
131
jrnl/Journal.py
131
jrnl/Journal.py
|
@ -25,17 +25,17 @@ class Tag:
|
|||
|
||||
|
||||
class Journal:
|
||||
def __init__(self, name='default', **kwargs):
|
||||
def __init__(self, name="default", **kwargs):
|
||||
self.config = {
|
||||
'journal': "journal.txt",
|
||||
'encrypt': False,
|
||||
'default_hour': 9,
|
||||
'default_minute': 0,
|
||||
'timeformat': "%Y-%m-%d %H:%M",
|
||||
'tagsymbols': '@',
|
||||
'highlight': True,
|
||||
'linewrap': 80,
|
||||
'indent_character': '|',
|
||||
"journal": "journal.txt",
|
||||
"encrypt": False,
|
||||
"default_hour": 9,
|
||||
"default_minute": 0,
|
||||
"timeformat": "%Y-%m-%d %H:%M",
|
||||
"tagsymbols": "@",
|
||||
"highlight": True,
|
||||
"linewrap": 80,
|
||||
"indent_character": "|",
|
||||
}
|
||||
self.config.update(kwargs)
|
||||
# Set up date parser
|
||||
|
@ -57,21 +57,28 @@ class Journal:
|
|||
another journal object"""
|
||||
new_journal = cls(other.name, **other.config)
|
||||
new_journal.entries = other.entries
|
||||
log.debug("Imported %d entries from %s to %s", len(new_journal), other.__class__.__name__, cls.__name__)
|
||||
log.debug(
|
||||
"Imported %d entries from %s to %s",
|
||||
len(new_journal),
|
||||
other.__class__.__name__,
|
||||
cls.__name__,
|
||||
)
|
||||
return new_journal
|
||||
|
||||
def import_(self, other_journal_txt):
|
||||
self.entries = list(frozenset(self.entries) | frozenset(self._parse(other_journal_txt)))
|
||||
self.entries = list(
|
||||
frozenset(self.entries) | frozenset(self._parse(other_journal_txt))
|
||||
)
|
||||
self.sort()
|
||||
|
||||
def open(self, filename=None):
|
||||
"""Opens the journal file defined in the config and parses it into a list of Entries.
|
||||
Entries have the form (date, title, body)."""
|
||||
filename = filename or self.config['journal']
|
||||
filename = filename or self.config["journal"]
|
||||
|
||||
if not os.path.exists(filename):
|
||||
self.create_file(filename)
|
||||
print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr)
|
||||
self._create(filename)
|
||||
|
||||
text = self._load(filename)
|
||||
self.entries = self._parse(text)
|
||||
|
@ -81,7 +88,7 @@ class Journal:
|
|||
|
||||
def write(self, filename=None):
|
||||
"""Dumps the journal into the config file, overwriting it"""
|
||||
filename = filename or self.config['journal']
|
||||
filename = filename or self.config["journal"]
|
||||
text = self._to_text()
|
||||
self._store(filename, text)
|
||||
|
||||
|
@ -93,6 +100,11 @@ class Journal:
|
|||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def create_file(filename):
|
||||
with open(filename, "w"):
|
||||
pass
|
||||
|
||||
def _to_text(self):
|
||||
return "\n".join([str(e) for e in self.entries])
|
||||
|
||||
|
@ -102,10 +114,6 @@ class Journal:
|
|||
def _store(self, filename, text):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def _create(cls, filename):
|
||||
raise NotImplementedError
|
||||
|
||||
def _parse(self, journal_txt):
|
||||
"""Parses a journal that's stored in a string and returns a list of entries"""
|
||||
|
||||
|
@ -128,7 +136,7 @@ class Journal:
|
|||
|
||||
if new_date:
|
||||
if entries:
|
||||
entries[-1].text = journal_txt[last_entry_pos:match.start()]
|
||||
entries[-1].text = journal_txt[last_entry_pos : match.start()]
|
||||
last_entry_pos = match.end()
|
||||
entries.append(Entry.Entry(self, date=new_date))
|
||||
|
||||
|
@ -147,18 +155,16 @@ class Journal:
|
|||
"""Prettyprints the journal's entries"""
|
||||
sep = "\n"
|
||||
pp = sep.join([e.pprint(short=short) for e in self.entries])
|
||||
if self.config['highlight']: # highlight tags
|
||||
if self.config["highlight"]: # highlight tags
|
||||
if self.search_tags:
|
||||
for tag in self.search_tags:
|
||||
tagre = re.compile(re.escape(tag), re.IGNORECASE)
|
||||
pp = re.sub(tagre,
|
||||
lambda match: util.colorize(match.group(0)),
|
||||
pp)
|
||||
pp = re.sub(tagre, lambda match: util.colorize(match.group(0)), pp)
|
||||
else:
|
||||
pp = re.sub(
|
||||
Entry.Entry.tag_regex(self.config['tagsymbols']),
|
||||
Entry.Entry.tag_regex(self.config["tagsymbols"]),
|
||||
lambda match: util.colorize(match.group(0)),
|
||||
pp
|
||||
pp,
|
||||
)
|
||||
return pp
|
||||
|
||||
|
@ -182,14 +188,22 @@ class Journal:
|
|||
"""Returns a set of tuples (count, tag) for all tags present in the journal."""
|
||||
# Astute reader: should the following line leave you as puzzled as me the first time
|
||||
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
|
||||
tags = [tag
|
||||
for entry in self.entries
|
||||
for tag in set(entry.tags)]
|
||||
tags = [tag for entry in self.entries for tag in set(entry.tags)]
|
||||
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
|
||||
tag_counts = {(tags.count(tag), tag) for tag in tags}
|
||||
return [Tag(tag, count=count) for count, tag in sorted(tag_counts)]
|
||||
|
||||
def filter(self, tags=[], start_date=None, end_date=None, starred=False, strict=False, short=False, exclude=[]):
|
||||
def filter(
|
||||
self,
|
||||
tags=[],
|
||||
start_date=None,
|
||||
end_date=None,
|
||||
starred=False,
|
||||
strict=False,
|
||||
short=False,
|
||||
contains=None,
|
||||
exclude=[],
|
||||
):
|
||||
"""Removes all entries from the journal that don't match the filter.
|
||||
|
||||
tags is a list of tags, each being a string that starts with one of the
|
||||
|
@ -211,13 +225,24 @@ class Journal:
|
|||
# If strict mode is on, all tags have to be present in entry
|
||||
tagged = self.search_tags.issubset if strict else self.search_tags.intersection
|
||||
excluded = lambda tags: len([tag for tag in tags if tag in excluded_tags]) > 0
|
||||
if contains:
|
||||
contains_lower = contains.casefold()
|
||||
|
||||
result = [
|
||||
entry for entry in self.entries
|
||||
entry
|
||||
for entry in self.entries
|
||||
if (not tags or tagged(entry.tags))
|
||||
and (not starred or entry.starred)
|
||||
and (not start_date or entry.date >= start_date)
|
||||
and (not end_date or entry.date <= end_date)
|
||||
and (not exclude or not excluded(entry.tags))
|
||||
and (
|
||||
not contains
|
||||
or (
|
||||
contains_lower in entry.title.casefold()
|
||||
or contains_lower in entry.body.casefold()
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
self.entries = result
|
||||
|
@ -240,7 +265,7 @@ class Journal:
|
|||
raw = raw.replace('\\n ', '\n').replace('\\n', '\n')
|
||||
# Split raw text into title and body
|
||||
sep = re.search(r"\n|[?!.]+ +\n?", raw)
|
||||
first_line = raw[:sep.end()].strip() if sep else raw
|
||||
first_line = raw[: sep.end()].strip() if sep else raw
|
||||
starred = False
|
||||
|
||||
if not date:
|
||||
|
@ -248,12 +273,12 @@ class Journal:
|
|||
if colon_pos > 0:
|
||||
date = time.parse(
|
||||
raw[:colon_pos],
|
||||
default_hour=self.config['default_hour'],
|
||||
default_minute=self.config['default_minute']
|
||||
default_hour=self.config["default_hour"],
|
||||
default_minute=self.config["default_minute"],
|
||||
)
|
||||
if date: # Parsed successfully, strip that from the raw text
|
||||
starred = raw[:colon_pos].strip().endswith("*")
|
||||
raw = raw[colon_pos + 1:].strip()
|
||||
raw = raw[colon_pos + 1 :].strip()
|
||||
starred = starred or first_line.startswith("*") or first_line.endswith("*")
|
||||
if not date: # Still nothing? Meh, just live in the moment.
|
||||
date = time.parse("now")
|
||||
|
@ -281,17 +306,12 @@ class Journal:
|
|||
|
||||
|
||||
class PlainJournal(Journal):
|
||||
@classmethod
|
||||
def _create(cls, filename):
|
||||
with open(filename, "a", encoding="utf-8"):
|
||||
pass
|
||||
|
||||
def _load(self, filename):
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def _store(self, filename, text):
|
||||
with open(filename, 'w', encoding="utf-8") as f:
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
|
@ -299,6 +319,7 @@ class LegacyJournal(Journal):
|
|||
"""Legacy class to support opening journals formatted with the jrnl 1.x
|
||||
standard. Main difference here is that in 1.x, timestamps were not cuddled
|
||||
by square brackets. You'll not be able to save these journals anymore."""
|
||||
|
||||
def _load(self, filename):
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
@ -307,17 +328,19 @@ class LegacyJournal(Journal):
|
|||
"""Parses a journal that's stored in a string and returns a list of entries"""
|
||||
# Entries start with a line that looks like 'date title' - let's figure out how
|
||||
# long the date will be by constructing one
|
||||
date_length = len(datetime.today().strftime(self.config['timeformat']))
|
||||
date_length = len(datetime.today().strftime(self.config["timeformat"]))
|
||||
|
||||
# Initialise our current entry
|
||||
entries = []
|
||||
current_entry = None
|
||||
new_date_format_regex = re.compile(r'(^\[[^\]]+\].*?$)')
|
||||
new_date_format_regex = re.compile(r"(^\[[^\]]+\].*?$)")
|
||||
for line in journal_txt.splitlines():
|
||||
line = line.rstrip()
|
||||
try:
|
||||
# try to parse line as date => new entry begins
|
||||
new_date = datetime.strptime(line[:date_length], self.config['timeformat'])
|
||||
new_date = datetime.strptime(
|
||||
line[:date_length], self.config["timeformat"]
|
||||
)
|
||||
|
||||
# parsing successful => save old entry and create new one
|
||||
if new_date and current_entry:
|
||||
|
@ -329,12 +352,14 @@ class LegacyJournal(Journal):
|
|||
else:
|
||||
starred = False
|
||||
|
||||
current_entry = Entry.Entry(self, date=new_date, text=line[date_length + 1:], starred=starred)
|
||||
current_entry = Entry.Entry(
|
||||
self, date=new_date, text=line[date_length + 1 :], starred=starred
|
||||
)
|
||||
except ValueError:
|
||||
# Happens when we can't parse the start of the line as an date.
|
||||
# In this case, just append line to our body (after some
|
||||
# escaping for the new format).
|
||||
line = new_date_format_regex.sub(r' \1', line)
|
||||
line = new_date_format_regex.sub(r" \1", line)
|
||||
if current_entry:
|
||||
current_entry.text += line + "\n"
|
||||
|
||||
|
@ -353,26 +378,30 @@ def open_journal(name, config, legacy=False):
|
|||
backwards compatibility with jrnl 1.x
|
||||
"""
|
||||
config = config.copy()
|
||||
config['journal'] = os.path.expanduser(os.path.expandvars(config['journal']))
|
||||
config["journal"] = os.path.expanduser(os.path.expandvars(config["journal"]))
|
||||
|
||||
if os.path.isdir(config['journal']):
|
||||
if config['journal'].strip("/").endswith(".dayone") or "entries" in os.listdir(config['journal']):
|
||||
if os.path.isdir(config["journal"]):
|
||||
if config["journal"].strip("/").endswith(".dayone") or "entries" in os.listdir(
|
||||
config["journal"]
|
||||
):
|
||||
from . import DayOneJournal
|
||||
|
||||
return DayOneJournal.DayOne(**config).open()
|
||||
else:
|
||||
print(
|
||||
f"[Error: {config['journal']} is a directory, but doesn't seem to be a DayOne journal either.",
|
||||
file=sys.stderr
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
if not config['encrypt']:
|
||||
if not config["encrypt"]:
|
||||
if legacy:
|
||||
return LegacyJournal(name, **config).open()
|
||||
return PlainJournal(name, **config).open()
|
||||
else:
|
||||
from . import EncryptedJournal
|
||||
|
||||
if legacy:
|
||||
return EncryptedJournal.LegacyEncryptedJournal(name, **config).open()
|
||||
return EncryptedJournal.EncryptedJournal(name, **config).open()
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import pkg_resources
|
||||
|
||||
dist = pkg_resources.get_distribution('jrnl')
|
||||
__title__ = dist.project_name
|
||||
__version__ = dist.version
|
||||
import os
|
||||
|
||||
try:
|
||||
from .__version__ import __version__
|
||||
except ImportError:
|
||||
__version__ = "source"
|
||||
__title__ = "jrnl"
|
||||
|
|
1
jrnl/__version__.py
Normal file
1
jrnl/__version__.py
Normal file
|
@ -0,0 +1 @@
|
|||
__version__ = "v2.2-beta"
|
317
jrnl/cli.py
317
jrnl/cli.py
|
@ -6,7 +6,8 @@
|
|||
license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from . import Journal
|
||||
from .Journal import PlainJournal, open_journal
|
||||
from .EncryptedJournal import EncryptedJournal
|
||||
from . import util
|
||||
from . import install
|
||||
from . import plugins
|
||||
|
@ -22,34 +23,157 @@ logging.getLogger("keyring.backend").setLevel(logging.ERROR)
|
|||
|
||||
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')
|
||||
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="*")
|
||||
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="*")
|
||||
|
||||
reading = parser.add_argument_group('Reading', 'Specifying either of these parameters will display posts of your journal')
|
||||
reading.add_argument('-from', dest='start_date', metavar="DATE", help='View entries after this date')
|
||||
reading.add_argument('-until', '-to', dest='end_date', metavar="DATE", help='View entries before this date')
|
||||
reading.add_argument('-on', dest='on_date', metavar="DATE", help='View entries on this date')
|
||||
reading.add_argument('-and', dest='strict', action="store_true", help='Filter by tags using AND (default: OR)')
|
||||
reading.add_argument('-starred', dest='starred', action="store_true", help='Show only starred entries')
|
||||
reading.add_argument('-n', dest='limit', default=None, metavar="N", help="Shows the last n entries matching the filter. '-n 3' and '-3' have the same effect.", nargs="?", type=int)
|
||||
reading.add_argument('-not', dest='excluded', nargs='+', default=[], metavar="E", help="Exclude entries with these tags")
|
||||
reading = parser.add_argument_group(
|
||||
"Reading",
|
||||
"Specifying either of these parameters will display posts of your journal",
|
||||
)
|
||||
reading.add_argument(
|
||||
"-from", dest="start_date", metavar="DATE", help="View entries after this date"
|
||||
)
|
||||
reading.add_argument(
|
||||
"-until",
|
||||
"-to",
|
||||
dest="end_date",
|
||||
metavar="DATE",
|
||||
help="View entries before this date",
|
||||
)
|
||||
reading.add_argument(
|
||||
"-contains", dest="contains", help="View entries containing a specific string"
|
||||
)
|
||||
reading.add_argument(
|
||||
"-on", dest="on_date", metavar="DATE", help="View entries on this date"
|
||||
)
|
||||
reading.add_argument(
|
||||
"-and",
|
||||
dest="strict",
|
||||
action="store_true",
|
||||
help="Filter by tags using AND (default: OR)",
|
||||
)
|
||||
reading.add_argument(
|
||||
"-starred",
|
||||
dest="starred",
|
||||
action="store_true",
|
||||
help="Show only starred entries",
|
||||
)
|
||||
reading.add_argument(
|
||||
"-n",
|
||||
dest="limit",
|
||||
default=None,
|
||||
metavar="N",
|
||||
help="Shows the last n entries matching the filter. '-n 3' and '-3' have the same effect.",
|
||||
nargs="?",
|
||||
type=int,
|
||||
)
|
||||
reading.add_argument(
|
||||
"-not",
|
||||
dest="excluded",
|
||||
nargs="+",
|
||||
default=[],
|
||||
metavar="E",
|
||||
help="Exclude entries with these tags",
|
||||
)
|
||||
|
||||
exporting = parser.add_argument_group(
|
||||
"Export / Import", "Options for transmogrifying your journal"
|
||||
)
|
||||
exporting.add_argument(
|
||||
"-s",
|
||||
"--short",
|
||||
dest="short",
|
||||
action="store_true",
|
||||
help="Show only titles or line containing the search tags",
|
||||
)
|
||||
exporting.add_argument(
|
||||
"--tags",
|
||||
dest="tags",
|
||||
action="store_true",
|
||||
help="Returns a list of all tags and number of occurences",
|
||||
)
|
||||
exporting.add_argument(
|
||||
"--export",
|
||||
metavar="TYPE",
|
||||
dest="export",
|
||||
choices=plugins.EXPORT_FORMATS,
|
||||
help="Export your journal. TYPE can be {}.".format(
|
||||
plugins.util.oxford_list(plugins.EXPORT_FORMATS)
|
||||
),
|
||||
default=False,
|
||||
const=None,
|
||||
)
|
||||
exporting.add_argument(
|
||||
"-o",
|
||||
metavar="OUTPUT",
|
||||
dest="output",
|
||||
help="Optionally specifies output file when using --export. If OUTPUT is a directory, exports each entry into an individual file instead.",
|
||||
default=False,
|
||||
const=None,
|
||||
)
|
||||
exporting.add_argument(
|
||||
"--import",
|
||||
metavar="TYPE",
|
||||
dest="import_",
|
||||
choices=plugins.IMPORT_FORMATS,
|
||||
help="Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.".format(
|
||||
plugins.util.oxford_list(plugins.IMPORT_FORMATS)
|
||||
),
|
||||
default=False,
|
||||
const="jrnl",
|
||||
nargs="?",
|
||||
)
|
||||
exporting.add_argument(
|
||||
"-i",
|
||||
metavar="INPUT",
|
||||
dest="input",
|
||||
help="Optionally specifies input file when using --import.",
|
||||
default=False,
|
||||
const=None,
|
||||
)
|
||||
exporting.add_argument(
|
||||
"--encrypt",
|
||||
metavar="FILENAME",
|
||||
dest="encrypt",
|
||||
help="Encrypts your existing journal with a new password",
|
||||
nargs="?",
|
||||
default=False,
|
||||
const=None,
|
||||
)
|
||||
exporting.add_argument(
|
||||
"--decrypt",
|
||||
metavar="FILENAME",
|
||||
dest="decrypt",
|
||||
help="Decrypts your journal and stores it in plain text",
|
||||
nargs="?",
|
||||
default=False,
|
||||
const=None,
|
||||
)
|
||||
exporting.add_argument(
|
||||
"--edit",
|
||||
dest="edit",
|
||||
help="Opens your editor to edit the selected entries.",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
exporting = parser.add_argument_group('Export / Import', 'Options for transmogrifying your journal')
|
||||
exporting.add_argument('-s', '--short', dest='short', action="store_true", help='Show only titles or line containing the search tags')
|
||||
exporting.add_argument('--tags', dest='tags', action="store_true", help='Returns a list of all tags and number of occurences')
|
||||
exporting.add_argument('--export', metavar='TYPE', dest='export', choices=plugins.EXPORT_FORMATS, help='Export your journal. TYPE can be {}.'.format(plugins.util.oxford_list(plugins.EXPORT_FORMATS)), default=False, const=None)
|
||||
exporting.add_argument('-o', metavar='OUTPUT', dest='output', help='Optionally specifies output file when using --export. If OUTPUT is a directory, exports each entry into an individual file instead.', default=False, const=None)
|
||||
exporting.add_argument('--import', metavar='TYPE', dest='import_', choices=plugins.IMPORT_FORMATS, help='Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.'.format(plugins.util.oxford_list(plugins.IMPORT_FORMATS)), default=False, const='jrnl', nargs='?')
|
||||
exporting.add_argument('-i', metavar='INPUT', dest='input', help='Optionally specifies input file when using --import.', default=False, const=None)
|
||||
exporting.add_argument('--encrypt', metavar='FILENAME', dest='encrypt', help='Encrypts your existing journal with a new password', nargs='?', default=False, const=None)
|
||||
exporting.add_argument('--decrypt', metavar='FILENAME', dest='decrypt', help='Decrypts your journal and stores it in plain text', nargs='?', default=False, const=None)
|
||||
exporting.add_argument('--edit', dest='edit', help='Opens your editor to edit the selected entries.', action="store_true")
|
||||
exporting.add_argument('--delete', dest='delete', action="store_true", help='Opens an interactive interface for deleting entries.')
|
||||
|
||||
return parser.parse_args(args)
|
||||
|
||||
|
||||
|
@ -62,13 +186,30 @@ def guess_mode(args, config):
|
|||
compose = False
|
||||
export = False
|
||||
import_ = True
|
||||
elif args.decrypt is not False or args.encrypt is not False or args.export is not False or any((args.short, args.tags, args.edit, args.delete)):
|
||||
elif (
|
||||
args.decrypt is not False
|
||||
or args.encrypt is not False
|
||||
or args.export is not False
|
||||
or any((args.short, args.tags, args.edit, args.delete))
|
||||
):
|
||||
compose = False
|
||||
export = True
|
||||
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,
|
||||
args.contains,
|
||||
)
|
||||
):
|
||||
# Any sign of displaying stuff?
|
||||
compose = False
|
||||
elif args.text and all(word[0] in config['tagsymbols'] for word in " ".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
|
||||
|
||||
|
@ -77,38 +218,37 @@ def guess_mode(args, config):
|
|||
|
||||
def encrypt(journal, filename=None):
|
||||
""" Encrypt into new file. If filename is not set, we encrypt the journal file itself. """
|
||||
from . import EncryptedJournal
|
||||
journal.config["encrypt"] = True
|
||||
|
||||
journal.config['password'] = util.getpass("Enter new password: ")
|
||||
journal.config['encrypt'] = True
|
||||
|
||||
new_journal = EncryptedJournal.EncryptedJournal(None, **journal.config)
|
||||
new_journal.entries = journal.entries
|
||||
new_journal = EncryptedJournal.from_journal(journal)
|
||||
new_journal.write(filename)
|
||||
|
||||
if util.yesno("Do you want to store the password in your keychain?", default=True):
|
||||
util.set_keychain(journal.name, journal.config['password'])
|
||||
|
||||
print("Journal encrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr)
|
||||
print(
|
||||
"Journal encrypted to {}.".format(filename or new_journal.config["journal"]),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def decrypt(journal, filename=None):
|
||||
""" Decrypts into new file. If filename is not set, we encrypt the journal file itself. """
|
||||
journal.config['encrypt'] = False
|
||||
journal.config['password'] = ""
|
||||
journal.config["encrypt"] = False
|
||||
|
||||
new_journal = Journal.PlainJournal(filename, **journal.config)
|
||||
new_journal.entries = journal.entries
|
||||
new_journal = PlainJournal.from_journal(journal)
|
||||
new_journal.write(filename)
|
||||
print("Journal decrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr)
|
||||
print(
|
||||
"Journal decrypted to {}.".format(filename or new_journal.config["journal"]),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def list_journals(config):
|
||||
"""List the journals specified in the configuration file"""
|
||||
result = f"Journals defined in {install.CONFIG_FILE_PATH}\n"
|
||||
ml = min(max(len(k) for k in config['journals']), 20)
|
||||
for journal, cfg in config['journals'].items():
|
||||
result += " * {:{}} -> {}\n".format(journal, ml, cfg['journal'] if isinstance(cfg, dict) else cfg)
|
||||
ml = min(max(len(k) for k in config["journals"]), 20)
|
||||
for journal, cfg in config["journals"].items():
|
||||
result += " * {:{}} -> {}\n".format(
|
||||
journal, ml, cfg["journal"] if isinstance(cfg, dict) else cfg
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
|
@ -116,11 +256,11 @@ def update_config(config, new_config, scope, force_local=False):
|
|||
"""Updates a config dict with new values - either global if scope is None
|
||||
or config['journals'][scope] is just a string pointing to a journal file,
|
||||
or within the scope"""
|
||||
if scope and type(config['journals'][scope]) is dict: # Update to journal specific
|
||||
config['journals'][scope].update(new_config)
|
||||
if scope and type(config["journals"][scope]) is dict: # Update to journal specific
|
||||
config["journals"][scope].update(new_config)
|
||||
elif scope and force_local: # Convert to dict
|
||||
config['journals'][scope] = {"journal": config['journals'][scope]}
|
||||
config['journals'][scope].update(new_config)
|
||||
config["journals"][scope] = {"journal": config["journals"][scope]}
|
||||
config["journals"][scope].update(new_config)
|
||||
else:
|
||||
config.update(new_config)
|
||||
|
||||
|
@ -128,9 +268,11 @@ def update_config(config, new_config, scope, force_local=False):
|
|||
def configure_logger(debug=False):
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG if debug else logging.INFO,
|
||||
format='%(levelname)-8s %(name)-12s %(message)s'
|
||||
format="%(levelname)-8s %(name)-12s %(message)s",
|
||||
)
|
||||
logging.getLogger('parsedatetime').setLevel(logging.INFO) # disable parsedatetime debug logging
|
||||
logging.getLogger("parsedatetime").setLevel(
|
||||
logging.INFO
|
||||
) # disable parsedatetime debug logging
|
||||
|
||||
|
||||
def run(manual_args=None):
|
||||
|
@ -156,11 +298,12 @@ def run(manual_args=None):
|
|||
|
||||
# If the first textual argument points to a journal file,
|
||||
# use this!
|
||||
journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default'
|
||||
|
||||
if journal_name != 'default':
|
||||
journal_name = install.DEFAULT_JOURNAL_KEY
|
||||
if args.text and args.text[0] in config["journals"]:
|
||||
journal_name = args.text[0]
|
||||
args.text = args.text[1:]
|
||||
elif "default" not in config['journals']:
|
||||
elif install.DEFAULT_JOURNAL_KEY not in config["journals"]:
|
||||
print("No default journal configured.", file=sys.stderr)
|
||||
print(list_journals(config), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
@ -188,18 +331,24 @@ def run(manual_args=None):
|
|||
if not sys.stdin.isatty():
|
||||
# Piping data into jrnl
|
||||
raw = sys.stdin.read()
|
||||
elif config['editor']:
|
||||
elif config["editor"]:
|
||||
template = ""
|
||||
if config['template']:
|
||||
if config["template"]:
|
||||
try:
|
||||
template = open(config['template']).read()
|
||||
template = open(config["template"]).read()
|
||||
except OSError:
|
||||
print(f"[Could not read template at '{config['template']}']", file=sys.stderr)
|
||||
print(
|
||||
f"[Could not read template at '{config['template']}']",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
raw = util.get_text_from_editor(config, template)
|
||||
else:
|
||||
try:
|
||||
print("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n", file=sys.stderr)
|
||||
print(
|
||||
"[Compose Entry; " + _exit_multiline_code + " to finish writing]\n",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raw = sys.stdin.read()
|
||||
except KeyboardInterrupt:
|
||||
print("[Entry NOT saved to journal.]", file=sys.stderr)
|
||||
|
@ -211,7 +360,7 @@ def run(manual_args=None):
|
|||
|
||||
# This is where we finally open the journal!
|
||||
try:
|
||||
journal = Journal.open_journal(journal_name, config)
|
||||
journal = open_journal(journal_name, config)
|
||||
except KeyboardInterrupt:
|
||||
print(f"[Interrupted while opening journal]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
@ -232,12 +381,16 @@ def run(manual_args=None):
|
|||
old_entries = journal.entries
|
||||
if args.on_date:
|
||||
args.start_date = args.end_date = args.on_date
|
||||
journal.filter(tags=args.text,
|
||||
start_date=args.start_date, end_date=args.end_date,
|
||||
strict=args.strict,
|
||||
short=args.short,
|
||||
starred=args.starred,
|
||||
exclude=args.excluded)
|
||||
journal.filter(
|
||||
tags=args.text,
|
||||
start_date=args.start_date,
|
||||
end_date=args.end_date,
|
||||
strict=args.strict,
|
||||
short=args.short,
|
||||
starred=args.starred,
|
||||
exclude=args.excluded,
|
||||
contains=args.contains,
|
||||
)
|
||||
journal.limit(args.limit)
|
||||
|
||||
# Reading mode
|
||||
|
@ -259,20 +412,28 @@ def run(manual_args=None):
|
|||
encrypt(journal, filename=args.encrypt)
|
||||
# Not encrypting to a separate file: update config!
|
||||
if not args.encrypt:
|
||||
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)
|
||||
|
||||
elif args.decrypt is not False:
|
||||
decrypt(journal, filename=args.decrypt)
|
||||
# Not decrypting to a separate file: update config!
|
||||
if not args.decrypt:
|
||||
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)
|
||||
|
||||
elif args.edit:
|
||||
if not config['editor']:
|
||||
print("[{1}ERROR{2}: You need to specify an editor in {0} to use the --edit function.]"
|
||||
.format(install.CONFIG_FILE_PATH, ERROR_COLOR, RESET_COLOR), file=sys.stderr)
|
||||
if not config["editor"]:
|
||||
print(
|
||||
"[{1}ERROR{2}: You need to specify an editor in {0} to use the --edit function.]".format(
|
||||
install.CONFIG_FILE_PATH, ERROR_COLOR, RESET_COLOR
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
other_entries = [e for e in old_entries if e not in journal.entries]
|
||||
# Edit
|
||||
|
@ -283,9 +444,17 @@ def run(manual_args=None):
|
|||
num_edited = len([e for e in journal.entries if e.modified])
|
||||
prompts = []
|
||||
if num_deleted:
|
||||
prompts.append("{} {} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries"))
|
||||
prompts.append(
|
||||
"{} {} deleted".format(
|
||||
num_deleted, "entry" if num_deleted == 1 else "entries"
|
||||
)
|
||||
)
|
||||
if num_edited:
|
||||
prompts.append("{} {} modified".format(num_edited, "entry" if num_deleted == 1 else "entries"))
|
||||
prompts.append(
|
||||
"{} {} modified".format(
|
||||
num_edited, "entry" if num_deleted == 1 else "entries"
|
||||
)
|
||||
)
|
||||
if prompts:
|
||||
print("[{}]".format(", ".join(prompts).capitalize()), file=sys.stderr)
|
||||
journal.entries += other_entries
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from .util import ERROR_COLOR, RESET_COLOR
|
||||
from .util import slugify
|
||||
from .plugins.template import Template
|
||||
import os
|
||||
|
||||
|
||||
class Exporter:
|
||||
"""This Exporter can convert entries and journals into text files."""
|
||||
def __init__(self, format):
|
||||
with open("jrnl/templates/" + format + ".template") as f:
|
||||
front_matter, body = f.read().strip("-\n").split("---", 2)
|
||||
self.template = Template(body)
|
||||
|
||||
def export_entry(self, entry):
|
||||
"""Returns a string representation of a single entry."""
|
||||
return str(entry)
|
||||
|
||||
def _get_vars(self, journal):
|
||||
return {
|
||||
'journal': journal,
|
||||
'entries': journal.entries,
|
||||
'tags': journal.tags
|
||||
}
|
||||
|
||||
def export_journal(self, journal):
|
||||
"""Returns a string representation of an entire journal."""
|
||||
return self.template.render_block("journal", **self._get_vars(journal))
|
||||
|
||||
def write_file(self, journal, path):
|
||||
"""Exports a journal into a single file."""
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(self.export_journal(journal))
|
||||
return f"[Journal exported to {path}]"
|
||||
except OSError as e:
|
||||
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
|
||||
|
||||
def make_filename(self, entry):
|
||||
return entry.date.strftime("%Y-%m-%d_{}.{}".format(slugify(entry.title), self.extension))
|
||||
|
||||
def write_files(self, journal, path):
|
||||
"""Exports a journal into individual files for each entry."""
|
||||
for entry in journal.entries:
|
||||
try:
|
||||
full_path = os.path.join(path, self.make_filename(entry))
|
||||
with open(full_path, "w", encoding="utf-8") as f:
|
||||
f.write(self.export_entry(entry))
|
||||
except OSError as e:
|
||||
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
|
||||
return f"[Journal exported to {path}]"
|
||||
|
||||
def export(self, journal, format="text", output=None):
|
||||
"""Exports to individual files if output is an existing path, or into
|
||||
a single file if output is a file name, or returns the exporter's
|
||||
representation as string if output is None."""
|
||||
if output and os.path.isdir(output): # multiple files
|
||||
return self.write_files(journal, output)
|
||||
elif output: # single file
|
||||
return self.write_file(journal, output)
|
||||
else:
|
||||
return self.export_journal(journal)
|
107
jrnl/install.py
107
jrnl/install.py
|
@ -13,15 +13,17 @@ from .util import UserAbort
|
|||
import yaml
|
||||
import logging
|
||||
import sys
|
||||
|
||||
if "win32" not in sys.platform:
|
||||
# readline is not included in Windows Active Python
|
||||
import readline
|
||||
|
||||
DEFAULT_CONFIG_NAME = 'jrnl.yaml'
|
||||
DEFAULT_JOURNAL_NAME = 'journal.txt'
|
||||
XDG_RESOURCE = 'jrnl'
|
||||
DEFAULT_CONFIG_NAME = "jrnl.yaml"
|
||||
DEFAULT_JOURNAL_NAME = "journal.txt"
|
||||
DEFAULT_JOURNAL_KEY = "default"
|
||||
XDG_RESOURCE = "jrnl"
|
||||
|
||||
USER_HOME = os.path.expanduser('~')
|
||||
USER_HOME = os.path.expanduser("~")
|
||||
|
||||
CONFIG_PATH = xdg.BaseDirectory.save_config_path(XDG_RESOURCE) or USER_HOME
|
||||
CONFIG_FILE_PATH = os.path.join(CONFIG_PATH, DEFAULT_CONFIG_NAME)
|
||||
|
@ -42,21 +44,20 @@ def module_exists(module_name):
|
|||
else:
|
||||
return True
|
||||
|
||||
|
||||
default_config = {
|
||||
'version': __version__,
|
||||
'journals': {
|
||||
"default": JOURNAL_FILE_PATH
|
||||
},
|
||||
'editor': os.getenv('VISUAL') or os.getenv('EDITOR') or "",
|
||||
'encrypt': False,
|
||||
'template': False,
|
||||
'default_hour': 9,
|
||||
'default_minute': 0,
|
||||
'timeformat': "%Y-%m-%d %H:%M",
|
||||
'tagsymbols': '@',
|
||||
'highlight': True,
|
||||
'linewrap': 79,
|
||||
'indent_character': '|',
|
||||
"version": __version__,
|
||||
"journals": {DEFAULT_JOURNAL_KEY: JOURNAL_FILE_PATH},
|
||||
"editor": os.getenv("VISUAL") or os.getenv("EDITOR") or "",
|
||||
"encrypt": False,
|
||||
"template": False,
|
||||
"default_hour": 9,
|
||||
"default_minute": 0,
|
||||
"timeformat": "%Y-%m-%d %H:%M",
|
||||
"tagsymbols": "@",
|
||||
"highlight": True,
|
||||
"linewrap": 79,
|
||||
"indent_character": "|",
|
||||
}
|
||||
|
||||
|
||||
|
@ -69,13 +70,18 @@ def upgrade_config(config):
|
|||
for key in missing_keys:
|
||||
config[key] = default_config[key]
|
||||
save_config(config)
|
||||
print(f"[Configuration updated to newest version at {CONFIG_FILE_PATH}]", file=sys.stderr)
|
||||
print(
|
||||
f"[Configuration updated to newest version at {CONFIG_FILE_PATH}]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def save_config(config):
|
||||
config['version'] = __version__
|
||||
with open(CONFIG_FILE_PATH, 'w') as f:
|
||||
yaml.safe_dump(config, f, encoding='utf-8', allow_unicode=True, default_flow_style=False)
|
||||
config["version"] = __version__
|
||||
with open(CONFIG_FILE_PATH, "w") as f:
|
||||
yaml.safe_dump(
|
||||
config, f, encoding="utf-8", allow_unicode=True, default_flow_style=False
|
||||
)
|
||||
|
||||
|
||||
def load_or_install_jrnl():
|
||||
|
@ -83,17 +89,27 @@ def load_or_install_jrnl():
|
|||
If jrnl is already installed, loads and returns a config object.
|
||||
Else, perform various prompts to install jrnl.
|
||||
"""
|
||||
config_path = CONFIG_FILE_PATH if os.path.exists(CONFIG_FILE_PATH) else CONFIG_FILE_PATH_FALLBACK
|
||||
config_path = (
|
||||
CONFIG_FILE_PATH
|
||||
if os.path.exists(CONFIG_FILE_PATH)
|
||||
else CONFIG_FILE_PATH_FALLBACK
|
||||
)
|
||||
if os.path.exists(config_path):
|
||||
log.debug('Reading configuration from file %s', config_path)
|
||||
log.debug("Reading configuration from file %s", config_path)
|
||||
config = util.load_config(config_path)
|
||||
|
||||
try:
|
||||
upgrade.upgrade_jrnl_if_necessary(config_path)
|
||||
except upgrade.UpgradeValidationException:
|
||||
print("Aborting upgrade.", file=sys.stderr)
|
||||
print("Please tell us about this problem at the following URL:", file=sys.stderr)
|
||||
print("https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException", file=sys.stderr)
|
||||
print(
|
||||
"Please tell us about this problem at the following URL:",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
"https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print("Exiting.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
@ -101,7 +117,7 @@ def load_or_install_jrnl():
|
|||
|
||||
return config
|
||||
else:
|
||||
log.debug('Configuration file not found, installing jrnl...')
|
||||
log.debug("Configuration file not found, installing jrnl...")
|
||||
try:
|
||||
config = install()
|
||||
except KeyboardInterrupt:
|
||||
|
@ -111,42 +127,39 @@ def load_or_install_jrnl():
|
|||
|
||||
def install():
|
||||
if "win32" not in sys.platform:
|
||||
readline.set_completer_delims(' \t\n;')
|
||||
readline.set_completer_delims(" \t\n;")
|
||||
readline.parse_and_bind("tab: complete")
|
||||
readline.set_completer(autocomplete)
|
||||
|
||||
# Where to create the journal?
|
||||
path_query = f'Path to your journal file (leave blank for {JOURNAL_FILE_PATH}): '
|
||||
path_query = f"Path to your journal file (leave blank for {JOURNAL_FILE_PATH}): "
|
||||
journal_path = input(path_query).strip() or JOURNAL_FILE_PATH
|
||||
default_config['journals']['default'] = os.path.expanduser(os.path.expandvars(journal_path))
|
||||
default_config["journals"][DEFAULT_JOURNAL_KEY] = os.path.expanduser(
|
||||
os.path.expandvars(journal_path)
|
||||
)
|
||||
|
||||
path = os.path.split(default_config['journals']['default'])[0] # If the folder doesn't exist, create it
|
||||
# If the folder doesn't exist, create it
|
||||
path = os.path.split(default_config["journals"][DEFAULT_JOURNAL_KEY])[0]
|
||||
try:
|
||||
os.makedirs(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Encrypt it?
|
||||
password = getpass.getpass("Enter password for journal (leave blank for no encryption): ")
|
||||
if password:
|
||||
default_config['encrypt'] = True
|
||||
if util.yesno("Do you want to store the password in your keychain?", default=True):
|
||||
util.set_keychain("default", password)
|
||||
else:
|
||||
util.set_keychain("default", None)
|
||||
EncryptedJournal._create(default_config['journals']['default'], password)
|
||||
encrypt = util.yesno(
|
||||
"Do you want to encrypt your journal? You can always change this later",
|
||||
default=False,
|
||||
)
|
||||
if encrypt:
|
||||
default_config["encrypt"] = True
|
||||
print("Journal will be encrypted.", file=sys.stderr)
|
||||
else:
|
||||
PlainJournal._create(default_config['journals']['default'])
|
||||
|
||||
config = default_config
|
||||
save_config(config)
|
||||
if password:
|
||||
config['password'] = password
|
||||
return config
|
||||
save_config(default_config)
|
||||
return default_config
|
||||
|
||||
|
||||
def autocomplete(text, state):
|
||||
expansions = glob.glob(os.path.expanduser(os.path.expandvars(text)) + '*')
|
||||
expansions = glob.glob(os.path.expanduser(os.path.expandvars(text)) + "*")
|
||||
expansions = [e + "/" if os.path.isdir(e) else e for e in expansions]
|
||||
expansions.append(None)
|
||||
return expansions[state]
|
||||
|
|
|
@ -11,8 +11,16 @@ from .yaml_exporter import YAMLExporter
|
|||
from .template_exporter import __all__ as template_exporters
|
||||
from .fancy_exporter import FancyExporter
|
||||
|
||||
__exporters =[JSONExporter, MarkdownExporter, TagExporter, TextExporter, XMLExporter, YAMLExporter, FancyExporter] + template_exporters
|
||||
__importers =[JRNLImporter]
|
||||
__exporters = [
|
||||
JSONExporter,
|
||||
MarkdownExporter,
|
||||
TagExporter,
|
||||
TextExporter,
|
||||
XMLExporter,
|
||||
YAMLExporter,
|
||||
FancyExporter,
|
||||
] + template_exporters
|
||||
__importers = [JRNLImporter]
|
||||
|
||||
__exporter_types = {name: plugin for plugin in __exporters for name in plugin.names}
|
||||
__importer_types = {name: plugin for plugin in __importers for name in plugin.names}
|
||||
|
@ -20,6 +28,7 @@ __importer_types = {name: plugin for plugin in __importers for name in plugin.na
|
|||
EXPORT_FORMATS = sorted(__exporter_types.keys())
|
||||
IMPORT_FORMATS = sorted(__importer_types.keys())
|
||||
|
||||
|
||||
def get_exporter(format):
|
||||
for exporter in __exporters:
|
||||
if hasattr(exporter, "names") and format in exporter.names:
|
||||
|
|
|
@ -8,46 +8,64 @@ from textwrap import TextWrapper
|
|||
|
||||
class FancyExporter(TextExporter):
|
||||
"""This Exporter can convert entries and journals into text with unicode box drawing characters."""
|
||||
|
||||
names = ["fancy", "boxed"]
|
||||
extension = "txt"
|
||||
|
||||
border_a="┎"
|
||||
border_b="─"
|
||||
border_c="╮"
|
||||
border_d="╘"
|
||||
border_e="═"
|
||||
border_f="╕"
|
||||
border_g="┃"
|
||||
border_h="│"
|
||||
border_i="┠"
|
||||
border_j="╌"
|
||||
border_k="┤"
|
||||
border_l="┖"
|
||||
border_m="┘"
|
||||
border_a = "┎"
|
||||
border_b = "─"
|
||||
border_c = "╮"
|
||||
border_d = "╘"
|
||||
border_e = "═"
|
||||
border_f = "╕"
|
||||
border_g = "┃"
|
||||
border_h = "│"
|
||||
border_i = "┠"
|
||||
border_j = "╌"
|
||||
border_k = "┤"
|
||||
border_l = "┖"
|
||||
border_m = "┘"
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry):
|
||||
"""Returns a fancy unicode representation of a single entry."""
|
||||
date_str = entry.date.strftime(entry.journal.config['timeformat'])
|
||||
linewrap = entry.journal.config['linewrap'] or 78
|
||||
date_str = entry.date.strftime(entry.journal.config["timeformat"])
|
||||
linewrap = entry.journal.config["linewrap"] or 78
|
||||
initial_linewrap = linewrap - len(date_str) - 2
|
||||
body_linewrap = linewrap - 2
|
||||
card = [cls.border_a + cls.border_b*(initial_linewrap) + cls.border_c + date_str]
|
||||
w = TextWrapper(width=initial_linewrap, initial_indent=cls.border_g+' ', subsequent_indent=cls.border_g+' ')
|
||||
card = [
|
||||
cls.border_a + cls.border_b * (initial_linewrap) + cls.border_c + date_str
|
||||
]
|
||||
w = TextWrapper(
|
||||
width=initial_linewrap,
|
||||
initial_indent=cls.border_g + " ",
|
||||
subsequent_indent=cls.border_g + " ",
|
||||
)
|
||||
title_lines = w.wrap(entry.title)
|
||||
card.append(title_lines[0].ljust(initial_linewrap+1) + cls.border_d + cls.border_e*(len(date_str)-1) + cls.border_f)
|
||||
card.append(
|
||||
title_lines[0].ljust(initial_linewrap + 1)
|
||||
+ cls.border_d
|
||||
+ cls.border_e * (len(date_str) - 1)
|
||||
+ cls.border_f
|
||||
)
|
||||
w.width = body_linewrap
|
||||
if len(title_lines) > 1:
|
||||
for line in w.wrap(' '.join([title_line[len(w.subsequent_indent):]
|
||||
for title_line in title_lines[1:]])):
|
||||
card.append(line.ljust(body_linewrap+1) + cls.border_h)
|
||||
for line in w.wrap(
|
||||
" ".join(
|
||||
[
|
||||
title_line[len(w.subsequent_indent) :]
|
||||
for title_line in title_lines[1:]
|
||||
]
|
||||
)
|
||||
):
|
||||
card.append(line.ljust(body_linewrap + 1) + cls.border_h)
|
||||
if entry.body:
|
||||
card.append(cls.border_i + cls.border_j*body_linewrap + cls.border_k)
|
||||
card.append(cls.border_i + cls.border_j * body_linewrap + cls.border_k)
|
||||
for line in entry.body.splitlines():
|
||||
body_lines = w.wrap(line) or [cls.border_g]
|
||||
for body_line in body_lines:
|
||||
card.append(body_line.ljust(body_linewrap+1) + cls.border_h)
|
||||
card.append(cls.border_l + cls.border_b*body_linewrap + cls.border_m)
|
||||
card.append(body_line.ljust(body_linewrap + 1) + cls.border_h)
|
||||
card.append(cls.border_l + cls.border_b * body_linewrap + cls.border_m)
|
||||
return "\n".join(card)
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
import sys
|
||||
from .. import util
|
||||
|
||||
|
||||
class JRNLImporter:
|
||||
"""This plugin imports entries from other jrnl files."""
|
||||
|
||||
names = ["jrnl"]
|
||||
|
||||
@staticmethod
|
||||
|
@ -25,5 +27,8 @@ class JRNLImporter:
|
|||
sys.exit(0)
|
||||
journal.import_(other_journal_txt)
|
||||
new_cnt = len(journal.entries)
|
||||
print("[{} imported to {} journal]".format(new_cnt - old_cnt, journal.name), file=sys.stderr)
|
||||
print(
|
||||
"[{} imported to {} journal]".format(new_cnt - old_cnt, journal.name),
|
||||
file=sys.stderr,
|
||||
)
|
||||
journal.write()
|
||||
|
|
|
@ -8,20 +8,21 @@ from .util import get_tags_count
|
|||
|
||||
class JSONExporter(TextExporter):
|
||||
"""This Exporter can convert entries and journals into json."""
|
||||
|
||||
names = ["json"]
|
||||
extension = "json"
|
||||
|
||||
@classmethod
|
||||
def entry_to_dict(cls, entry):
|
||||
entry_dict = {
|
||||
'title': entry.title,
|
||||
'body': entry.body,
|
||||
'date': entry.date.strftime("%Y-%m-%d"),
|
||||
'time': entry.date.strftime("%H:%M"),
|
||||
'starred': entry.starred
|
||||
"title": entry.title,
|
||||
"body": entry.body,
|
||||
"date": entry.date.strftime("%Y-%m-%d"),
|
||||
"time": entry.date.strftime("%H:%M"),
|
||||
"starred": entry.starred,
|
||||
}
|
||||
if hasattr(entry, "uuid"):
|
||||
entry_dict['uuid'] = entry.uuid
|
||||
entry_dict["uuid"] = entry.uuid
|
||||
return entry_dict
|
||||
|
||||
@classmethod
|
||||
|
@ -35,6 +36,6 @@ class JSONExporter(TextExporter):
|
|||
tags = get_tags_count(journal)
|
||||
result = {
|
||||
"tags": {tag: count for count, tag in tags},
|
||||
"entries": [cls.entry_to_dict(e) for e in journal.entries]
|
||||
"entries": [cls.entry_to_dict(e) for e in journal.entries],
|
||||
}
|
||||
return json.dumps(result, indent=2)
|
||||
|
|
|
@ -10,24 +10,25 @@ from ..util import WARNING_COLOR, RESET_COLOR
|
|||
|
||||
class MarkdownExporter(TextExporter):
|
||||
"""This Exporter can convert entries and journals into Markdown."""
|
||||
|
||||
names = ["md", "markdown"]
|
||||
extension = "md"
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry, to_multifile=True):
|
||||
"""Returns a markdown representation of a single entry."""
|
||||
date_str = entry.date.strftime(entry.journal.config['timeformat'])
|
||||
date_str = entry.date.strftime(entry.journal.config["timeformat"])
|
||||
body_wrapper = "\n" if entry.body else ""
|
||||
body = body_wrapper + entry.body
|
||||
|
||||
if to_multifile is True:
|
||||
heading = '#'
|
||||
heading = "#"
|
||||
else:
|
||||
heading = '###'
|
||||
heading = "###"
|
||||
|
||||
'''Increase heading levels in body text'''
|
||||
newbody = ''
|
||||
previous_line = ''
|
||||
"""Increase heading levels in body text"""
|
||||
newbody = ""
|
||||
previous_line = ""
|
||||
warn_on_heading_level = False
|
||||
for line in body.splitlines(True):
|
||||
if re.match(r"^#+ ", line):
|
||||
|
@ -35,24 +36,30 @@ class MarkdownExporter(TextExporter):
|
|||
newbody = newbody + previous_line + heading + line
|
||||
if re.match(r"^#######+ ", heading + line):
|
||||
warn_on_heading_level = True
|
||||
line = ''
|
||||
elif re.match(r"^=+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
|
||||
line = ""
|
||||
elif re.match(r"^=+$", line.rstrip()) and not re.match(
|
||||
r"^$", previous_line.strip()
|
||||
):
|
||||
"""Setext style H1"""
|
||||
newbody = newbody + heading + "# " + previous_line
|
||||
line = ''
|
||||
elif re.match(r"^-+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
|
||||
line = ""
|
||||
elif re.match(r"^-+$", line.rstrip()) and not re.match(
|
||||
r"^$", previous_line.strip()
|
||||
):
|
||||
"""Setext style H2"""
|
||||
newbody = newbody + heading + "## " + previous_line
|
||||
line = ''
|
||||
line = ""
|
||||
else:
|
||||
newbody = newbody + previous_line
|
||||
previous_line = line
|
||||
newbody = newbody + previous_line # add very last line
|
||||
newbody = newbody + previous_line # add very last line
|
||||
|
||||
if warn_on_heading_level is True:
|
||||
print(f"{WARNING_COLOR}WARNING{RESET_COLOR}: "
|
||||
f"Headings increased past H6 on export - {date_str} {entry.title}",
|
||||
file=sys.stderr)
|
||||
print(
|
||||
f"{WARNING_COLOR}WARNING{RESET_COLOR}: "
|
||||
f"Headings increased past H6 on export - {date_str} {entry.title}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
return f"{heading} {date_str} {entry.title}\n{newbody} "
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ from .util import get_tags_count
|
|||
|
||||
class TagExporter(TextExporter):
|
||||
"""This Exporter can lists the tags for entries and journals, exported as a plain text file."""
|
||||
|
||||
names = ["tags"]
|
||||
extension = "tags"
|
||||
|
||||
|
@ -21,9 +22,11 @@ class TagExporter(TextExporter):
|
|||
tag_counts = get_tags_count(journal)
|
||||
result = ""
|
||||
if not tag_counts:
|
||||
return '[No tags found in journal.]'
|
||||
return "[No tags found in 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("{:20} : {}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True))
|
||||
result += "[Removed tags that appear only once.]\n"
|
||||
result += "\n".join(
|
||||
"{:20} : {}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True)
|
||||
)
|
||||
return result
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import re
|
||||
import asteval
|
||||
import yaml
|
||||
|
||||
VAR_RE = r"[_a-zA-Z][a-zA-Z0-9_]*"
|
||||
|
@ -7,7 +6,9 @@ EXPRESSION_RE = r"[\[\]():.a-zA-Z0-9_]*"
|
|||
PRINT_RE = r"{{ *(.+?) *}}"
|
||||
START_BLOCK_RE = r"{% *(if|for) +(.+?) *%}"
|
||||
END_BLOCK_RE = r"{% *end(for|if) *%}"
|
||||
FOR_RE = r"{{% *for +({varname}) +in +([^%]+) *%}}".format(varname=VAR_RE, expression=EXPRESSION_RE)
|
||||
FOR_RE = r"{{% *for +({varname}) +in +([^%]+) *%}}".format(
|
||||
varname=VAR_RE, expression=EXPRESSION_RE
|
||||
)
|
||||
IF_RE = r"{% *if +(.+?) *%}"
|
||||
BLOCK_RE = r"{% *block +(.+?) *%}((?:.|\n)+?){% *endblock *%}"
|
||||
INCLUDE_RE = r"{% *include +(.+?) *%}"
|
||||
|
@ -39,9 +40,11 @@ class Template:
|
|||
return self._expand(self.blocks[block], **vars)
|
||||
|
||||
def _eval_context(self, vars):
|
||||
import asteval
|
||||
|
||||
e = asteval.Interpreter(use_numpy=False, writer=None)
|
||||
e.symtable.update(vars)
|
||||
e.symtable['__last_iteration'] = vars.get("__last_iteration", False)
|
||||
e.symtable["__last_iteration"] = vars.get("__last_iteration", False)
|
||||
return e
|
||||
|
||||
def _get_blocks(self):
|
||||
|
@ -49,12 +52,19 @@ class Template:
|
|||
name, contents = match.groups()
|
||||
self.blocks[name] = self._strip_single_nl(contents)
|
||||
return ""
|
||||
|
||||
self.clean_template = re.sub(BLOCK_RE, s, self.template, flags=re.MULTILINE)
|
||||
|
||||
def _expand(self, template, **vars):
|
||||
stack = sorted(
|
||||
[(m.start(), 1, m.groups()[0]) for m in re.finditer(START_BLOCK_RE, template)] +
|
||||
[(m.end(), -1, m.groups()[0]) for m in re.finditer(END_BLOCK_RE, template)]
|
||||
[
|
||||
(m.start(), 1, m.groups()[0])
|
||||
for m in re.finditer(START_BLOCK_RE, template)
|
||||
]
|
||||
+ [
|
||||
(m.end(), -1, m.groups()[0])
|
||||
for m in re.finditer(END_BLOCK_RE, template)
|
||||
]
|
||||
)
|
||||
|
||||
last_nesting, nesting = 0, 0
|
||||
|
@ -80,19 +90,23 @@ class Template:
|
|||
start = pos
|
||||
last_nesting = nesting
|
||||
|
||||
result += self._expand_vars(template[stack[-1][0]:], **vars)
|
||||
result += self._expand_vars(template[stack[-1][0] :], **vars)
|
||||
return result
|
||||
|
||||
def _expand_vars(self, template, **vars):
|
||||
safe_eval = self._eval_context(vars)
|
||||
expanded = re.sub(INCLUDE_RE, lambda m: self.render_block(m.groups()[0], **vars), template)
|
||||
expanded = re.sub(
|
||||
INCLUDE_RE, lambda m: self.render_block(m.groups()[0], **vars), template
|
||||
)
|
||||
return re.sub(PRINT_RE, lambda m: str(safe_eval(m.groups()[0])), expanded)
|
||||
|
||||
def _expand_cond(self, template, **vars):
|
||||
start_block = re.search(IF_RE, template, re.M)
|
||||
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
|
||||
expression = start_block.groups()[0]
|
||||
sub_template = self._strip_single_nl(template[start_block.end():end_block.start()])
|
||||
sub_template = self._strip_single_nl(
|
||||
template[start_block.end() : end_block.start()]
|
||||
)
|
||||
|
||||
safe_eval = self._eval_context(vars)
|
||||
if safe_eval(expression):
|
||||
|
@ -110,15 +124,17 @@ class Template:
|
|||
start_block = re.search(FOR_RE, template, re.M)
|
||||
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
|
||||
var_name, iterator = start_block.groups()
|
||||
sub_template = self._strip_single_nl(template[start_block.end():end_block.start()], strip_r=False)
|
||||
sub_template = self._strip_single_nl(
|
||||
template[start_block.end() : end_block.start()], strip_r=False
|
||||
)
|
||||
|
||||
safe_eval = self._eval_context(vars)
|
||||
|
||||
result = ''
|
||||
result = ""
|
||||
items = safe_eval(iterator)
|
||||
for idx, var in enumerate(items):
|
||||
vars[var_name] = var
|
||||
vars['__last_iteration'] = idx == len(items) - 1
|
||||
vars["__last_iteration"] = idx == len(items) - 1
|
||||
result += self._expand(sub_template, **vars)
|
||||
del vars[var_name]
|
||||
return self._strip_single_nl(result)
|
||||
|
|
|
@ -13,20 +13,13 @@ class GenericTemplateExporter(TextExporter):
|
|||
@classmethod
|
||||
def export_entry(cls, entry):
|
||||
"""Returns a string representation of a single entry."""
|
||||
vars = {
|
||||
'entry': entry,
|
||||
'tags': entry.tags
|
||||
}
|
||||
vars = {"entry": entry, "tags": entry.tags}
|
||||
return cls.template.render_block("entry", **vars)
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
"""Returns a string representation of an entire journal."""
|
||||
vars = {
|
||||
'journal': journal,
|
||||
'entries': journal.entries,
|
||||
'tags': journal.tags
|
||||
}
|
||||
vars = {"journal": journal, "entries": journal.entries, "tags": journal.tags}
|
||||
return cls.template.render_block("journal", **vars)
|
||||
|
||||
|
||||
|
@ -34,11 +27,12 @@ def __exporter_from_file(template_file):
|
|||
"""Create a template class from a file"""
|
||||
name = os.path.basename(template_file).replace(".template", "")
|
||||
template = Template.from_file(template_file)
|
||||
return type(str(f"{name.title()}Exporter"), (GenericTemplateExporter, ), {
|
||||
"names": [name],
|
||||
"extension": template.extension,
|
||||
"template": template
|
||||
})
|
||||
return type(
|
||||
str(f"{name.title()}Exporter"),
|
||||
(GenericTemplateExporter,),
|
||||
{"names": [name], "extension": template.extension, "template": template},
|
||||
)
|
||||
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ from ..util import ERROR_COLOR, RESET_COLOR
|
|||
|
||||
class TextExporter:
|
||||
"""This Exporter can convert entries and journals into text files."""
|
||||
|
||||
names = ["text", "txt"]
|
||||
extension = "txt"
|
||||
|
||||
|
@ -33,7 +34,9 @@ class TextExporter:
|
|||
|
||||
@classmethod
|
||||
def make_filename(cls, entry):
|
||||
return entry.date.strftime("%Y-%m-%d_{}.{}".format(slugify(str(entry.title)), cls.extension))
|
||||
return entry.date.strftime(
|
||||
"%Y-%m-%d_{}.{}".format(slugify(str(entry.title)), cls.extension)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def write_files(cls, journal, path):
|
||||
|
@ -44,7 +47,9 @@ class TextExporter:
|
|||
with open(full_path, "w", encoding="utf-8") as f:
|
||||
f.write(cls.export_entry(entry))
|
||||
except IOError as e:
|
||||
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
|
||||
return "[{2}ERROR{3}: {0} {1}]".format(
|
||||
e.filename, e.strerror, ERROR_COLOR, RESET_COLOR
|
||||
)
|
||||
return "[Journal exported to {}]".format(path)
|
||||
|
||||
@classmethod
|
||||
|
@ -54,7 +59,7 @@ class TextExporter:
|
|||
representation as string if output is None."""
|
||||
if output and os.path.isdir(output): # multiple files
|
||||
return cls.write_files(journal, output)
|
||||
elif output: # single file
|
||||
elif output: # single file
|
||||
return cls.write_file(journal, output)
|
||||
else:
|
||||
return cls.export_journal(journal)
|
||||
|
|
|
@ -6,9 +6,7 @@ def get_tags_count(journal):
|
|||
"""Returns a set of tuples (count, tag) for all tags present in the journal."""
|
||||
# Astute reader: should the following line leave you as puzzled as me the first time
|
||||
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
|
||||
tags = [tag
|
||||
for entry in journal.entries
|
||||
for tag in set(entry.tags)]
|
||||
tags = [tag for entry in journal.entries for tag in set(entry.tags)]
|
||||
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
|
||||
tag_counts = {(tags.count(tag), tag) for tag in tags}
|
||||
return tag_counts
|
||||
|
@ -24,4 +22,4 @@ def oxford_list(lst):
|
|||
elif len(lst) == 2:
|
||||
return lst[0] + " or " + lst[1]
|
||||
else:
|
||||
return ', '.join(lst[:-1]) + ", or " + lst[-1]
|
||||
return ", ".join(lst[:-1]) + ", or " + lst[-1]
|
||||
|
|
|
@ -8,6 +8,7 @@ from xml.dom import minidom
|
|||
|
||||
class XMLExporter(JSONExporter):
|
||||
"""This Exporter can convert entries and journals into XML."""
|
||||
|
||||
names = ["xml"]
|
||||
extension = "xml"
|
||||
|
||||
|
@ -15,7 +16,7 @@ class XMLExporter(JSONExporter):
|
|||
def export_entry(cls, entry, doc=None):
|
||||
"""Returns an XML representation of a single entry."""
|
||||
doc_el = doc or minidom.Document()
|
||||
entry_el = doc_el.createElement('entry')
|
||||
entry_el = doc_el.createElement("entry")
|
||||
for key, value in cls.entry_to_dict(entry).items():
|
||||
elem = doc_el.createElement(key)
|
||||
elem.appendChild(doc_el.createTextNode(value))
|
||||
|
@ -28,11 +29,11 @@ class XMLExporter(JSONExporter):
|
|||
|
||||
@classmethod
|
||||
def entry_to_xml(cls, entry, doc):
|
||||
entry_el = doc.createElement('entry')
|
||||
entry_el.setAttribute('date', entry.date.isoformat())
|
||||
entry_el = doc.createElement("entry")
|
||||
entry_el.setAttribute("date", entry.date.isoformat())
|
||||
if hasattr(entry, "uuid"):
|
||||
entry_el.setAttribute('uuid', entry.uuid)
|
||||
entry_el.setAttribute('starred', entry.starred)
|
||||
entry_el.setAttribute("uuid", entry.uuid)
|
||||
entry_el.setAttribute("starred", entry.starred)
|
||||
entry_el.appendChild(doc.createTextNode(entry.fulltext))
|
||||
return entry_el
|
||||
|
||||
|
@ -41,12 +42,12 @@ class XMLExporter(JSONExporter):
|
|||
"""Returns an XML representation of an entire journal."""
|
||||
tags = get_tags_count(journal)
|
||||
doc = minidom.Document()
|
||||
xml = doc.createElement('journal')
|
||||
tags_el = doc.createElement('tags')
|
||||
entries_el = doc.createElement('entries')
|
||||
xml = doc.createElement("journal")
|
||||
tags_el = doc.createElement("tags")
|
||||
entries_el = doc.createElement("entries")
|
||||
for count, tag in tags:
|
||||
tag_el = doc.createElement('tag')
|
||||
tag_el.setAttribute('name', tag)
|
||||
tag_el = doc.createElement("tag")
|
||||
tag_el.setAttribute("name", tag)
|
||||
count_node = doc.createTextNode(str(count))
|
||||
tag_el.appendChild(count_node)
|
||||
tags_el.appendChild(tag_el)
|
||||
|
|
|
@ -10,6 +10,7 @@ from ..util import WARNING_COLOR, ERROR_COLOR, RESET_COLOR
|
|||
|
||||
class YAMLExporter(TextExporter):
|
||||
"""This Exporter can convert entries and journals into Markdown formatted text with YAML front matter."""
|
||||
|
||||
names = ["yaml"]
|
||||
extension = "md"
|
||||
|
||||
|
@ -17,68 +18,109 @@ class YAMLExporter(TextExporter):
|
|||
def export_entry(cls, entry, to_multifile=True):
|
||||
"""Returns a markdown representation of a single entry, with YAML front matter."""
|
||||
if to_multifile is False:
|
||||
print("{}ERROR{}: YAML export must be to individual files. "
|
||||
"Please specify a directory to export to.".format("\033[31m", "\033[0m"), file=sys.stderr)
|
||||
print(
|
||||
"{}ERROR{}: YAML export must be to individual files. Please \
|
||||
specify a directory to export to.".format(
|
||||
ERROR_COLOR, RESET_COLOR, file=sys.stderr
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
date_str = entry.date.strftime(entry.journal.config['timeformat'])
|
||||
date_str = entry.date.strftime(entry.journal.config["timeformat"])
|
||||
body_wrapper = "\n" if entry.body else ""
|
||||
body = body_wrapper + entry.body
|
||||
|
||||
tagsymbols = entry.journal.config['tagsymbols']
|
||||
tagsymbols = entry.journal.config["tagsymbols"]
|
||||
# see also Entry.Entry.rag_regex
|
||||
multi_tag_regex = re.compile(r'(?u)^\s*([{tags}][-+*#/\w]+\s*)+$'.format(tags=tagsymbols))
|
||||
multi_tag_regex = re.compile(fr"(?u)^\s*([{tagsymbols}][-+*#/\w]+\s*)+$")
|
||||
|
||||
'''Increase heading levels in body text'''
|
||||
newbody = ''
|
||||
heading = '#'
|
||||
previous_line = ''
|
||||
"""Increase heading levels in body text"""
|
||||
newbody = ""
|
||||
heading = "#"
|
||||
previous_line = ""
|
||||
warn_on_heading_level = False
|
||||
for line in entry.body.splitlines(True):
|
||||
for line in body.splitlines(True):
|
||||
if re.match(r"^#+ ", line):
|
||||
"""ATX style headings"""
|
||||
newbody = newbody + previous_line + heading + line
|
||||
if re.match(r"^#######+ ", heading + line):
|
||||
warn_on_heading_level = True
|
||||
line = ''
|
||||
elif re.match(r"^=+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
|
||||
line = ""
|
||||
elif re.match(r"^=+$", line.rstrip()) and not re.match(
|
||||
r"^$", previous_line.strip()
|
||||
):
|
||||
"""Setext style H1"""
|
||||
newbody = newbody + heading + "# " + previous_line
|
||||
line = ''
|
||||
elif re.match(r"^-+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
|
||||
line = ""
|
||||
elif re.match(r"^-+$", line.rstrip()) and not re.match(
|
||||
r"^$", previous_line.strip()
|
||||
):
|
||||
"""Setext style H2"""
|
||||
newbody = newbody + heading + "## " + previous_line
|
||||
line = ''
|
||||
line = ""
|
||||
elif multi_tag_regex.match(line):
|
||||
"""Tag only lines"""
|
||||
line = ''
|
||||
line = ""
|
||||
else:
|
||||
newbody = newbody + previous_line
|
||||
previous_line = line
|
||||
newbody = newbody + previous_line # add very last line
|
||||
newbody = newbody + previous_line # add very last line
|
||||
|
||||
if warn_on_heading_level is True:
|
||||
print("{}WARNING{}: Headings increased past H6 on export - {} {}".format(WARNING_COLOR, RESET_COLOR, date_str, entry.title), file=sys.stderr)
|
||||
print(
|
||||
"{}WARNING{}: Headings increased past H6 on export - {} {}".format(
|
||||
WARNING_COLOR, RESET_COLOR, date_str, entry.title
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
dayone_attributes = ''
|
||||
dayone_attributes = ""
|
||||
if hasattr(entry, "uuid"):
|
||||
dayone_attributes += 'uuid: ' + entry.uuid + '\n'
|
||||
# TODO: copy over pictures, if present
|
||||
# source directory is entry.journal.config['journal']
|
||||
# output directory is...?
|
||||
dayone_attributes += "uuid: " + entry.uuid + "\n"
|
||||
if (
|
||||
hasattr(entry, "creator_device_agent")
|
||||
or hasattr(entry, "creator_generation_date")
|
||||
or hasattr(entry, "creator_host_name")
|
||||
or hasattr(entry, "creator_os_agent")
|
||||
or hasattr(entry, "creator_software_agent")
|
||||
):
|
||||
dayone_attributes += "creator:\n"
|
||||
if hasattr(entry, "creator_device_agent"):
|
||||
dayone_attributes += f" device agent: {entry.creator_device_agent}\n"
|
||||
if hasattr(entry, "creator_generation_date"):
|
||||
dayone_attributes += " generation date: {}\n".format(
|
||||
str(entry.creator_generation_date)
|
||||
)
|
||||
if hasattr(entry, "creator_host_name"):
|
||||
dayone_attributes += f" host name: {entry.creator_host_name}\n"
|
||||
if hasattr(entry, "creator_os_agent"):
|
||||
dayone_attributes += f" os agent: {entry.creator_os_agent}\n"
|
||||
if hasattr(entry, "creator_software_agent"):
|
||||
dayone_attributes += (
|
||||
f" software agent: {entry.creator_software_agent}\n"
|
||||
)
|
||||
|
||||
# TODO: copy over pictures, if present
|
||||
# source directory is entry.journal.config['journal']
|
||||
# output directory is...?
|
||||
|
||||
return "title: {title}\ndate: {date}\nstared: {stared}\ntags: {tags}\n{dayone} {body} {space}".format(
|
||||
date = date_str,
|
||||
title = entry.title,
|
||||
stared = entry.starred,
|
||||
tags = ', '.join([tag[1:] for tag in entry.tags]),
|
||||
dayone = dayone_attributes,
|
||||
body = newbody,
|
||||
space=""
|
||||
date=date_str,
|
||||
title=entry.title,
|
||||
stared=entry.starred,
|
||||
tags=", ".join([tag[1:] for tag in entry.tags]),
|
||||
dayone=dayone_attributes,
|
||||
body=newbody,
|
||||
space="",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
"""Returns an error, as YAML export requires a directory as a target."""
|
||||
print("{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format(ERROR_COLOR, RESET_COLOR), file=sys.stderr)
|
||||
print(
|
||||
"{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format(
|
||||
ERROR_COLOR, RESET_COLOR
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
return
|
||||
|
|
23
jrnl/time.py
23
jrnl/time.py
|
@ -1,7 +1,10 @@
|
|||
from datetime import datetime
|
||||
from dateutil.parser import parse as dateparse
|
||||
try: import parsedatetime.parsedatetime_consts as pdt
|
||||
except ImportError: import parsedatetime as pdt
|
||||
|
||||
try:
|
||||
import parsedatetime.parsedatetime_consts as pdt
|
||||
except ImportError:
|
||||
import parsedatetime as pdt
|
||||
|
||||
FAKE_YEAR = 9999
|
||||
DEFAULT_FUTURE = datetime(FAKE_YEAR, 12, 31, 23, 59, 59)
|
||||
|
@ -12,7 +15,9 @@ consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
|
|||
CALENDAR = pdt.Calendar(consts)
|
||||
|
||||
|
||||
def parse(date_str, inclusive=False, default_hour=None, default_minute=None, bracketed=False):
|
||||
def parse(
|
||||
date_str, inclusive=False, default_hour=None, default_minute=None, bracketed=False
|
||||
):
|
||||
"""Parses a string containing a fuzzy date and returns a datetime.datetime object"""
|
||||
if not date_str:
|
||||
return None
|
||||
|
@ -37,7 +42,7 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None, bra
|
|||
flag = 1 if date.hour == date.minute == 0 else 2
|
||||
date = date.timetuple()
|
||||
except Exception as e:
|
||||
if e.args[0] == 'day is out of range for month':
|
||||
if e.args[0] == "day is out of range for month":
|
||||
y, m, d, H, M, S = default_date.timetuple()[:6]
|
||||
default_date = datetime(y, m, d - 1, H, M, S)
|
||||
else:
|
||||
|
@ -53,10 +58,12 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None, bra
|
|||
return None
|
||||
|
||||
if flag is 1: # Date found, but no time. Use the default time.
|
||||
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)
|
||||
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])
|
||||
|
||||
|
|
|
@ -11,9 +11,9 @@ import os
|
|||
def backup(filename, binary=False):
|
||||
print(f" Created a backup at {filename}.backup", file=sys.stderr)
|
||||
filename = os.path.expanduser(os.path.expandvars(filename))
|
||||
with open(filename, 'rb' if binary else 'r') as original:
|
||||
with open(filename, "rb" if binary else "r") as original:
|
||||
contents = original.read()
|
||||
with open(filename + ".backup", 'wb' if binary else 'w') as backup:
|
||||
with open(filename + ".backup", "wb" if binary else "w") as backup:
|
||||
backup.write(contents)
|
||||
|
||||
|
||||
|
@ -25,7 +25,8 @@ def upgrade_jrnl_if_necessary(config_path):
|
|||
|
||||
config = util.load_config(config_path)
|
||||
|
||||
print("""Welcome to jrnl {}.
|
||||
print(
|
||||
f"""Welcome to jrnl {__version__}.
|
||||
|
||||
It looks like you've been using an older version of jrnl until now. That's
|
||||
okay - jrnl will now upgrade your configuration and journal files. Afterwards
|
||||
|
@ -39,18 +40,20 @@ you can enjoy all of the great new features that come with jrnl 2:
|
|||
Please note that jrnl 1.x is NOT forward compatible with this version of jrnl.
|
||||
If you choose to proceed, you will not be able to use your journals with
|
||||
older versions of jrnl anymore.
|
||||
""".format(__version__))
|
||||
"""
|
||||
)
|
||||
|
||||
encrypted_journals = {}
|
||||
plain_journals = {}
|
||||
other_journals = {}
|
||||
all_journals = []
|
||||
|
||||
for journal_name, journal_conf in config['journals'].items():
|
||||
for journal_name, journal_conf in config["journals"].items():
|
||||
if isinstance(journal_conf, dict):
|
||||
path = journal_conf.get("journal")
|
||||
encrypt = journal_conf.get("encrypt")
|
||||
else:
|
||||
encrypt = config.get('encrypt')
|
||||
encrypt = config.get("encrypt")
|
||||
path = journal_conf
|
||||
|
||||
path = os.path.expanduser(path)
|
||||
|
@ -62,21 +65,36 @@ older versions of jrnl anymore.
|
|||
else:
|
||||
plain_journals[journal_name] = path
|
||||
|
||||
longest_journal_name = max([len(journal) for journal in config['journals']])
|
||||
longest_journal_name = max([len(journal) for journal in config["journals"]])
|
||||
if encrypted_journals:
|
||||
print(f"\nFollowing encrypted journals will be upgraded to jrnl {__version__}:", file=sys.stderr)
|
||||
print(
|
||||
f"\nFollowing encrypted journals will be upgraded to jrnl {__version__}:",
|
||||
file=sys.stderr,
|
||||
)
|
||||
for journal, path in encrypted_journals.items():
|
||||
print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr)
|
||||
print(
|
||||
" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if plain_journals:
|
||||
print(f"\nFollowing plain text journals will upgraded to jrnl {__version__}:", file=sys.stderr)
|
||||
print(
|
||||
f"\nFollowing plain text journals will upgraded to jrnl {__version__}:",
|
||||
file=sys.stderr,
|
||||
)
|
||||
for journal, path in plain_journals.items():
|
||||
print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr)
|
||||
print(
|
||||
" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if other_journals:
|
||||
print("\nFollowing journals will be not be touched:", file=sys.stderr)
|
||||
for journal, path in other_journals.items():
|
||||
print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr)
|
||||
print(
|
||||
" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
try:
|
||||
cont = util.yesno("\nContinue upgrading jrnl?", default=False)
|
||||
|
@ -86,24 +104,37 @@ older versions of jrnl anymore.
|
|||
raise UserAbort("jrnl NOT upgraded, exiting.")
|
||||
|
||||
for journal_name, path in encrypted_journals.items():
|
||||
print(f"\nUpgrading encrypted '{journal_name}' journal stored in {path}...", file=sys.stderr)
|
||||
print(
|
||||
f"\nUpgrading encrypted '{journal_name}' journal stored in {path}...",
|
||||
file=sys.stderr,
|
||||
)
|
||||
backup(path, binary=True)
|
||||
old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True)
|
||||
old_journal = Journal.open_journal(
|
||||
journal_name, util.scope_config(config, journal_name), legacy=True
|
||||
)
|
||||
all_journals.append(EncryptedJournal.from_journal(old_journal))
|
||||
|
||||
for journal_name, path in plain_journals.items():
|
||||
print(f"\nUpgrading plain text '{journal_name}' journal stored in {path}...", file=sys.stderr)
|
||||
print(
|
||||
f"\nUpgrading plain text '{journal_name}' journal stored in {path}...",
|
||||
file=sys.stderr,
|
||||
)
|
||||
backup(path)
|
||||
old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True)
|
||||
old_journal = Journal.open_journal(
|
||||
journal_name, util.scope_config(config, journal_name), legacy=True
|
||||
)
|
||||
all_journals.append(Journal.PlainJournal.from_journal(old_journal))
|
||||
|
||||
# loop through lists to validate
|
||||
failed_journals = [j for j in all_journals if not j.validate_parsing()]
|
||||
|
||||
if len(failed_journals) > 0:
|
||||
print("\nThe following journal{} failed to upgrade:\n{}".format(
|
||||
's' if len(failed_journals) > 1 else '', "\n".join(j.name for j in failed_journals)),
|
||||
file=sys.stderr
|
||||
print(
|
||||
"\nThe following journal{} failed to upgrade:\n{}".format(
|
||||
"s" if len(failed_journals) > 1 else "",
|
||||
"\n".join(j.name for j in failed_journals),
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
raise UpgradeValidationException
|
||||
|
@ -120,4 +151,5 @@ older versions of jrnl anymore.
|
|||
|
||||
class UpgradeValidationException(Exception):
|
||||
"""Raised when the contents of an upgraded journal do not match the old journal"""
|
||||
|
||||
pass
|
||||
|
|
93
jrnl/util.py
93
jrnl/util.py
|
@ -4,8 +4,10 @@ import sys
|
|||
import os
|
||||
import getpass as gp
|
||||
import yaml
|
||||
|
||||
if "win32" in sys.platform:
|
||||
import colorama
|
||||
|
||||
colorama.init()
|
||||
import re
|
||||
import tempfile
|
||||
|
@ -13,6 +15,7 @@ import subprocess
|
|||
import unicodedata
|
||||
import shlex
|
||||
import logging
|
||||
from typing import Optional, Callable
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -22,7 +25,8 @@ RESET_COLOR = "\033[0m"
|
|||
|
||||
# Based on Segtok by Florian Leitner
|
||||
# https://github.com/fnl/segtok
|
||||
SENTENCE_SPLITTER = re.compile(r"""
|
||||
SENTENCE_SPLITTER = re.compile(
|
||||
r"""
|
||||
( # A sentence ends at one of two sequences:
|
||||
[.!?\u203C\u203D\u2047\u2048\u2049\u3002\uFE52\uFE57\uFF01\uFF0E\uFF1F\uFF61] # Either, a sequence starting with a sentence terminal,
|
||||
[\'\u2019\"\u201D]? # an optional right quote,
|
||||
|
@ -30,20 +34,44 @@ SENTENCE_SPLITTER = re.compile(r"""
|
|||
\s+ # a sequence of required spaces.
|
||||
| # Otherwise,
|
||||
\n # a sentence also terminates newlines.
|
||||
)""", re.VERBOSE)
|
||||
)""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
|
||||
class UserAbort(Exception):
|
||||
pass
|
||||
|
||||
|
||||
getpass = gp.getpass
|
||||
def create_password(
|
||||
journal_name: str, prompt: str = "Enter password for new journal: "
|
||||
) -> str:
|
||||
while True:
|
||||
pw = gp.getpass(prompt)
|
||||
if not pw:
|
||||
print("Password can't be an empty string!", file=sys.stderr)
|
||||
continue
|
||||
elif pw == gp.getpass("Enter password again: "):
|
||||
break
|
||||
|
||||
print("Passwords did not match, please try again", file=sys.stderr)
|
||||
|
||||
if yesno("Do you want to store the password in your keychain?", default=True):
|
||||
set_keychain(journal_name, pw)
|
||||
else:
|
||||
set_keychain(journal_name, None)
|
||||
|
||||
return pw
|
||||
|
||||
|
||||
def get_password(validator, keychain=None, max_attempts=3):
|
||||
def decrypt_content(
|
||||
decrypt_func: Callable[[str], Optional[str]],
|
||||
keychain: str = None,
|
||||
max_attempts: int = 3,
|
||||
) -> str:
|
||||
pwd_from_keychain = keychain and get_keychain(keychain)
|
||||
password = pwd_from_keychain or getpass()
|
||||
result = validator(password)
|
||||
password = pwd_from_keychain or gp.getpass()
|
||||
result = decrypt_func(password)
|
||||
# Password is bad:
|
||||
if result is None and pwd_from_keychain:
|
||||
set_keychain(keychain, None)
|
||||
|
@ -51,7 +79,7 @@ def get_password(validator, keychain=None, max_attempts=3):
|
|||
while result is None and attempt < max_attempts:
|
||||
print("Wrong password, try again.", file=sys.stderr)
|
||||
password = gp.getpass()
|
||||
result = validator(password)
|
||||
result = decrypt_func(password)
|
||||
attempt += 1
|
||||
if result is not None:
|
||||
return result
|
||||
|
@ -62,21 +90,23 @@ def get_password(validator, keychain=None, max_attempts=3):
|
|||
|
||||
def get_keychain(journal_name):
|
||||
import keyring
|
||||
|
||||
try:
|
||||
return keyring.get_password('jrnl', journal_name)
|
||||
return keyring.get_password("jrnl", journal_name)
|
||||
except RuntimeError:
|
||||
return ""
|
||||
|
||||
|
||||
def set_keychain(journal_name, password):
|
||||
import keyring
|
||||
|
||||
if password is None:
|
||||
try:
|
||||
keyring.delete_password('jrnl', journal_name)
|
||||
except RuntimeError:
|
||||
keyring.delete_password("jrnl", journal_name)
|
||||
except keyring.errors.PasswordDeleteError:
|
||||
pass
|
||||
else:
|
||||
keyring.set_password('jrnl', journal_name, password)
|
||||
keyring.set_password("jrnl", journal_name, password)
|
||||
|
||||
|
||||
def yesno(prompt, default=True):
|
||||
|
@ -93,34 +123,45 @@ def load_config(config_path):
|
|||
|
||||
|
||||
def scope_config(config, journal_name):
|
||||
if journal_name not in config['journals']:
|
||||
if journal_name not in config["journals"]:
|
||||
return config
|
||||
config = config.copy()
|
||||
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 journal overrides %s', journal_conf)
|
||||
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 journal 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
|
||||
config.pop('journals')
|
||||
config["journal"] = journal_conf
|
||||
config.pop("journals")
|
||||
return config
|
||||
|
||||
|
||||
def get_text_from_editor(config, template=""):
|
||||
filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt")
|
||||
with open(tmpfile, 'w', encoding="utf-8") as f:
|
||||
os.close(filehandle)
|
||||
|
||||
with open(tmpfile, "w", encoding="utf-8") as f:
|
||||
if template:
|
||||
f.write(template)
|
||||
|
||||
try:
|
||||
subprocess.call(shlex.split(config['editor'], posix="win" not in sys.platform) + [tmpfile])
|
||||
subprocess.call(
|
||||
shlex.split(config["editor"], posix="win" not in sys.platform) + [tmpfile]
|
||||
)
|
||||
except AttributeError:
|
||||
subprocess.call(config['editor'] + [tmpfile])
|
||||
subprocess.call(config["editor"] + [tmpfile])
|
||||
|
||||
with open(tmpfile, "r", encoding="utf-8") as f:
|
||||
raw = f.read()
|
||||
os.close(filehandle)
|
||||
os.remove(tmpfile)
|
||||
|
||||
if not raw:
|
||||
print('[Nothing saved to file]', file=sys.stderr)
|
||||
print("[Nothing saved to file]", file=sys.stderr)
|
||||
|
||||
return raw
|
||||
|
||||
|
||||
|
@ -133,9 +174,9 @@ def slugify(string):
|
|||
"""Slugifies a string.
|
||||
Based on public domain code from https://github.com/zacharyvoase/slugify
|
||||
"""
|
||||
normalized_string = str(unicodedata.normalize('NFKD', string))
|
||||
no_punctuation = re.sub(r'[^\w\s-]', '', normalized_string).strip().lower()
|
||||
slug = re.sub(r'[-\s]+', '-', no_punctuation)
|
||||
normalized_string = str(unicodedata.normalize("NFKD", string))
|
||||
no_punctuation = re.sub(r"[^\w\s-]", "", normalized_string).strip().lower()
|
||||
slug = re.sub(r"[-\s]+", "-", no_punctuation)
|
||||
return slug
|
||||
|
||||
|
||||
|
@ -144,4 +185,4 @@ def split_title(text):
|
|||
punkt = SENTENCE_SPLITTER.search(text)
|
||||
if not punkt:
|
||||
return text, ""
|
||||
return text[:punkt.end()].strip(), text[punkt.end():].strip()
|
||||
return text[: punkt.end()].strip(), text[punkt.end() :].strip()
|
||||
|
|
|
@ -12,7 +12,7 @@ markdown_extensions:
|
|||
- admonition
|
||||
repo_url: https://github.com/jrnl-org/jrnl/
|
||||
site_author: Manuel Ebert
|
||||
site_description: Never Worry about Money Again.
|
||||
site_description: Collect your thoughts and notes without leaving the command line.
|
||||
nav:
|
||||
- Overview: overview.md
|
||||
- Quickstart: installation.md
|
||||
|
|
180
poetry.lock
generated
180
poetry.lock
generated
|
@ -6,25 +6,13 @@ optional = false
|
|||
python-versions = "*"
|
||||
version = "1.4.3"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP"
|
||||
name = "asn1crypto"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.24.0"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Safe, minimalistic evaluator of python expression using ast module"
|
||||
name = "asteval"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "0.9.14"
|
||||
|
||||
[package.dependencies]
|
||||
numpy = "*"
|
||||
six = "*"
|
||||
python-versions = ">=3.5"
|
||||
version = "0.9.18"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
|
@ -32,7 +20,7 @@ description = "Classes Without Boilerplate"
|
|||
name = "attrs"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "19.1.0"
|
||||
version = "19.3.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
|
@ -53,13 +41,16 @@ description = "The uncompromising code formatter."
|
|||
name = "black"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
version = "18.9b0"
|
||||
version = "19.10b0"
|
||||
|
||||
[package.dependencies]
|
||||
appdirs = "*"
|
||||
attrs = ">=17.4.0"
|
||||
attrs = ">=18.1.0"
|
||||
click = ">=6.5"
|
||||
pathspec = ">=0.6,<1"
|
||||
regex = "*"
|
||||
toml = ">=0.9.4"
|
||||
typed-ast = ">=1.4.0"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
|
@ -67,7 +58,7 @@ description = "Foreign Function Interface for Python calling C code."
|
|||
name = "cffi"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.12.3"
|
||||
version = "1.13.2"
|
||||
|
||||
[package.dependencies]
|
||||
pycparser = "*"
|
||||
|
@ -86,8 +77,8 @@ description = "Cross-platform colored terminal text."
|
|||
marker = "sys_platform == \"win32\""
|
||||
name = "colorama"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "0.4.1"
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "0.4.3"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
|
@ -95,15 +86,14 @@ description = "cryptography is a package which provides cryptographic recipes an
|
|||
name = "cryptography"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
|
||||
version = "2.7"
|
||||
version = "2.8"
|
||||
|
||||
[package.dependencies]
|
||||
asn1crypto = ">=0.21.0"
|
||||
cffi = ">=1.8,<1.11.3 || >1.11.3"
|
||||
six = ">=1.4.1"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
category = "dev"
|
||||
description = "Discover and load entry points from installed packages."
|
||||
name = "entrypoints"
|
||||
optional = false
|
||||
|
@ -116,7 +106,7 @@ description = "the modular source code checker: pep8, pyflakes and co"
|
|||
name = "flake8"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "3.7.8"
|
||||
version = "3.7.9"
|
||||
|
||||
[package.dependencies]
|
||||
entrypoints = ">=0.3.0,<0.4.0"
|
||||
|
@ -126,11 +116,15 @@ pyflakes = ">=2.1.0,<2.2.0"
|
|||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Clean single-source support for Python 3 and 2"
|
||||
name = "future"
|
||||
description = "Read metadata from Python packages"
|
||||
marker = "python_version < \"3.8\""
|
||||
name = "importlib-metadata"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
version = "0.17.1"
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
version = "1.3.0"
|
||||
|
||||
[package.dependencies]
|
||||
zipp = ">=0.5"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
|
@ -139,15 +133,15 @@ marker = "sys_platform == \"linux\""
|
|||
name = "jeepney"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "0.4"
|
||||
version = "0.4.2"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "A small but fast and easy to use stand-alone template engine written in pure python."
|
||||
description = "A very fast and expressive template engine."
|
||||
name = "jinja2"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2.10.1"
|
||||
version = "2.10.3"
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=0.23"
|
||||
|
@ -158,13 +152,16 @@ description = "Store and access your passwords safely."
|
|||
name = "keyring"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "19.0.2"
|
||||
version = "19.3.0"
|
||||
|
||||
[package.dependencies]
|
||||
entrypoints = "*"
|
||||
pywin32-ctypes = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1"
|
||||
secretstorage = "*"
|
||||
|
||||
[package.dependencies.importlib-metadata]
|
||||
python = "<3.8"
|
||||
version = "*"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Python LiveReload is an awesome tool for web developers"
|
||||
|
@ -222,11 +219,12 @@ tornado = ">=5.0"
|
|||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "NumPy is the fundamental package for array computing with Python."
|
||||
name = "numpy"
|
||||
description = "More routines for operating on iterables, beyond itertools"
|
||||
marker = "python_version < \"3.8\""
|
||||
name = "more-itertools"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
|
||||
version = "1.16.4"
|
||||
python-versions = ">=3.5"
|
||||
version = "8.0.2"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
|
@ -234,7 +232,7 @@ description = "parse() is the opposite of format()"
|
|||
name = "parse"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.12.0"
|
||||
version = "1.14.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
|
@ -242,10 +240,10 @@ description = "Simplifies to build parse types based on the parse module"
|
|||
name = "parse-type"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*"
|
||||
version = "0.4.2"
|
||||
version = "0.5.2"
|
||||
|
||||
[package.dependencies]
|
||||
parse = ">=1.8"
|
||||
parse = ">=1.8.4"
|
||||
six = ">=1.11"
|
||||
|
||||
[[package]]
|
||||
|
@ -254,10 +252,7 @@ description = "Parse human-readable date/time text."
|
|||
name = "parsedatetime"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2.4"
|
||||
|
||||
[package.dependencies]
|
||||
future = "*"
|
||||
version = "2.5"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
|
@ -265,7 +260,15 @@ description = "comprehensive password hashing framework supporting over 30 schem
|
|||
name = "passlib"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.7.1"
|
||||
version = "1.7.2"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
name = "pathspec"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "0.7.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
|
@ -296,8 +299,8 @@ category = "main"
|
|||
description = "Extensions to the standard Python datetime module"
|
||||
name = "python-dateutil"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
version = "2.8.0"
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
version = "2.8.1"
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.5"
|
||||
|
@ -308,7 +311,7 @@ description = "World timezone definitions, modern and historical"
|
|||
name = "pytz"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2019.1"
|
||||
version = "2019.3"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
|
@ -333,7 +336,15 @@ description = "YAML parser and emitter for Python"
|
|||
name = "pyyaml"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "5.1.2"
|
||||
version = "5.2"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Alternative regular expression module, to replace re."
|
||||
name = "regex"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2019.12.20"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
|
@ -354,7 +365,7 @@ description = "Python 2 and 3 compatibility utilities"
|
|||
name = "six"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*"
|
||||
version = "1.12.0"
|
||||
version = "1.13.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
|
@ -372,6 +383,14 @@ optional = false
|
|||
python-versions = ">= 3.5"
|
||||
version = "6.0.3"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
||||
name = "typed-ast"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.4.0"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "tzinfo object for the local timezone"
|
||||
|
@ -383,47 +402,62 @@ version = "1.5.1"
|
|||
[package.dependencies]
|
||||
pytz = "*"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
marker = "python_version < \"3.8\""
|
||||
name = "zipp"
|
||||
optional = false
|
||||
python-versions = ">=2.7"
|
||||
version = "0.6.0"
|
||||
|
||||
[package.dependencies]
|
||||
more-itertools = "*"
|
||||
|
||||
[metadata]
|
||||
content-hash = "9896cf59c7552b6ad95219ee5555c7445a3fab39c2e4f4c6f3d991a36635e44b"
|
||||
python-versions = ">=3.6.0, <3.8.0"
|
||||
content-hash = "98e23837423d5d8621f14cbe592d209ef98e1926b7a3f94e0f88bb6be908aae8"
|
||||
python-versions = ">=3.6.0, <3.9.0"
|
||||
|
||||
[metadata.hashes]
|
||||
appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"]
|
||||
asn1crypto = ["2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", "9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"]
|
||||
asteval = ["7c81fee6707a7a28e8beae891b858535a7e61f9ce275a0a4cf5f428fbc934cb8"]
|
||||
attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"]
|
||||
asteval = ["5d64e18b8a72c2c7ae8f9b70d1f80b68bbcaa98c1c0d7047c35489d03209bc86"]
|
||||
attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"]
|
||||
behave = ["b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86", "ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c"]
|
||||
black = ["817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", "e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"]
|
||||
cffi = ["041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774", "046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d", "066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90", "066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b", "2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63", "300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45", "34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25", "46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3", "4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b", "4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647", "4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016", "50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4", "55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb", "5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753", "59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7", "73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9", "a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f", "a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8", "a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f", "a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc", "ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42", "b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3", "d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909", "d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45", "dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d", "e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512", "e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff", "ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201"]
|
||||
black = ["1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", "c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"]
|
||||
cffi = ["0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42", "0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04", "135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5", "19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54", "2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba", "291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57", "2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396", "2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12", "32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97", "3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43", "415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db", "42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3", "4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b", "4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579", "599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346", "5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159", "5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652", "62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e", "6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a", "6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506", "71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f", "74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d", "7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c", "7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20", "7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858", "8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc", "aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a", "ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3", "d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e", "d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410", "dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25", "e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b", "fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"]
|
||||
click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"]
|
||||
colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"]
|
||||
cryptography = ["24b61e5fcb506424d3ec4e18bca995833839bf13c59fc43e530e488f28d46b8c", "25dd1581a183e9e7a806fe0543f485103232f940fcfc301db65e630512cce643", "3452bba7c21c69f2df772762be0066c7ed5dc65df494a1d53a58b683a83e1216", "41a0be220dd1ed9e998f5891948306eb8c812b512dc398e5a01846d855050799", "5751d8a11b956fbfa314f6553d186b94aa70fdb03d8a4d4f1c82dcacf0cbe28a", "5f61c7d749048fa6e3322258b4263463bfccefecb0dd731b6561cb617a1d9bb9", "72e24c521fa2106f19623a3851e9f89ddfdeb9ac63871c7643790f872a305dfc", "7b97ae6ef5cba2e3bb14256625423413d5ce8d1abb91d4f29b6d1a081da765f8", "961e886d8a3590fd2c723cf07be14e2a91cf53c25f02435c04d39e90780e3b53", "96d8473848e984184b6728e2c9d391482008646276c3ff084a1bd89e15ff53a1", "ae536da50c7ad1e002c3eee101871d93abdc90d9c5f651818450a0d3af718609", "b0db0cecf396033abb4a93c95d1602f268b3a68bb0a9cc06a7cff587bb9a7292", "cfee9164954c186b191b91d4193989ca994703b2fff406f71cf454a2d3c7327e", "e6347742ac8f35ded4a46ff835c60e68c22a536a8ae5c4422966d06946b6d4c6", "f27d93f0139a3c056172ebb5d4f9056e770fdf0206c2f422ff2ebbad142e09ed", "f57b76e46a58b63d1c6375017f4564a28f19a5ca912691fd2e4261b3414b618d"]
|
||||
colorama = ["7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", "e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"]
|
||||
cryptography = ["02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c", "1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595", "369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad", "3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651", "44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2", "4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff", "58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d", "6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42", "7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d", "73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e", "7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912", "90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793", "971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13", "a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7", "b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0", "b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879", "d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f", "de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9", "df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2", "ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf", "fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"]
|
||||
entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"]
|
||||
flake8 = ["19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", "8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"]
|
||||
future = ["67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"]
|
||||
jeepney = ["6089412a5de162c04747f0220f6b2223b8ba660acd041e52a76426ca550e3c70", "f6f8b1428403b4afad04b6b82f9ab9fc426c253d7504c9031c41712a2c01dc74"]
|
||||
jinja2 = ["065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", "14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"]
|
||||
keyring = ["1b74595f7439e4581a11d4f9a12790ac34addce64ca389c86272ff465f5e0b90", "afbfe7bc9bdba69d25c551b0c738adde533d87e0b51ad6bbe332cbea19ad8476"]
|
||||
flake8 = ["45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", "49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"]
|
||||
importlib-metadata = ["073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45", "d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"]
|
||||
jeepney = ["0ba6d8c597e9bef1ebd18aaec595f942a264e25c1a48f164d46120eacaa2e9bb", "6f45dce1125cf6c58a1c88123d3831f36a789f9204fbad3172eac15f8ccd08d0"]
|
||||
jinja2 = ["74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", "9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"]
|
||||
keyring = ["9b80469783d3f6106bce1d389c6b8b20c8d4d739943b1b8cd0ddc2a45d065f9d", "ee3d35b7f1ac3cb69e9a1e4323534649d3ab2fea402738a77e4250c152970fed"]
|
||||
livereload = ["78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", "89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"]
|
||||
markdown = ["2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a", "56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c"]
|
||||
markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"]
|
||||
mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"]
|
||||
mkdocs = ["17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939", "8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"]
|
||||
numpy = ["0778076e764e146d3078b17c24c4d89e0ecd4ac5401beff8e1c87879043a0633", "141c7102f20abe6cf0d54c4ced8d565b86df4d3077ba2343b61a6db996cefec7", "14270a1ee8917d11e7753fb54fc7ffd1934f4d529235beec0b275e2ccf00333b", "27e11c7a8ec9d5838bc59f809bfa86efc8a4fd02e58960fa9c49d998e14332d5", "2a04dda79606f3d2f760384c38ccd3d5b9bb79d4c8126b67aff5eb09a253763e", "3c26010c1b51e1224a3ca6b8df807de6e95128b0908c7e34f190e7775455b0ca", "52c40f1a4262c896420c6ea1c6fda62cf67070e3947e3307f5562bd783a90336", "6e4f8d9e8aa79321657079b9ac03f3cf3fd067bf31c1cca4f56d49543f4356a5", "7242be12a58fec245ee9734e625964b97cf7e3f2f7d016603f9e56660ce479c7", "7dc253b542bfd4b4eb88d9dbae4ca079e7bf2e2afd819ee18891a43db66c60c7", "94f5bd885f67bbb25c82d80184abbf7ce4f6c3c3a41fbaa4182f034bba803e69", "a89e188daa119ffa0d03ce5123dee3f8ffd5115c896c2a9d4f0dbb3d8b95bfa3", "ad3399da9b0ca36e2f24de72f67ab2854a62e623274607e37e0ce5f5d5fa9166", "b0348be89275fd1d4c44ffa39530c41a21062f52299b1e3ee7d1c61f060044b8", "b5554368e4ede1856121b0dfa35ce71768102e4aa55e526cb8de7f374ff78722", "cbddc56b2502d3f87fda4f98d948eb5b11f36ff3902e17cb6cc44727f2200525", "d79f18f41751725c56eceab2a886f021d70fd70a6188fd386e29a045945ffc10", "dc2ca26a19ab32dc475dbad9dfe723d3a64c835f4c23f625c2b6566ca32b9f29", "dd9bcd4f294eb0633bb33d1a74febdd2b9018b8b8ed325f861fffcd2c7660bb8", "e8baab1bc7c9152715844f1faca6744f2416929de10d7639ed49555a85549f52", "ec31fe12668af687b99acf1567399632a7c47b0e17cfb9ae47c098644ef36797", "f12b4f7e2d8f9da3141564e6737d79016fe5336cc92de6814eba579744f65b0a", "f58ac38d5ca045a377b3b377c84df8175ab992c970a53332fa8ac2373df44ff7"]
|
||||
parse = ["1b68657434d371e5156048ca4a0c5aea5afc6ca59a2fea4dd1a575354f617142"]
|
||||
parse-type = ["6e906a66f340252e4c324914a60d417d33a4bea01292ea9bbf68b4fc123be8c9", "f596bdc75d3dd93036fbfe3d04127da9f6df0c26c36e01e76da85adef4336b3c"]
|
||||
parsedatetime = ["3d817c58fb9570d1eec1dd46fa9448cd644eeed4fb612684b02dfda3a79cb84b", "9ee3529454bf35c40a77115f5a596771e59e1aee8c53306f346c461b8e913094"]
|
||||
passlib = ["3d948f64138c25633613f303bcc471126eae67c04d5e3f6b7b8ce6242f8653e0", "43526aea08fa32c6b6dbbbe9963c4c767285b78147b7437597f992812f69d280"]
|
||||
more-itertools = ["b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d", "c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"]
|
||||
parse = ["95a4f4469e37c57b5e924629ac99926f28bee7da59515dc5b8078c4c3e779249"]
|
||||
parse-type = ["089a471b06327103865dfec2dd844230c3c658a4a1b5b4c8b6c16c8f77577f9e", "7f690b18d35048c15438d6d0571f9045cffbec5907e0b1ccf006f889e3a38c0b"]
|
||||
parsedatetime = ["3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1", "d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667"]
|
||||
passlib = ["68c35c98a7968850e17f1b6892720764cc7eed0ef2b7cb3116a89a28e43fe177", "8d666cef936198bc2ab47ee9b0410c94adf2ba798e5a84bf220be079ae7ab6a8"]
|
||||
pathspec = ["163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424", "562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"]
|
||||
pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"]
|
||||
pycparser = ["a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"]
|
||||
pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"]
|
||||
python-dateutil = ["7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"]
|
||||
pytz = ["303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", "d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"]
|
||||
python-dateutil = ["73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"]
|
||||
pytz = ["1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", "b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"]
|
||||
pywin32-ctypes = ["24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", "9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"]
|
||||
pyxdg = ["1948ff8e2db02156c0cccd2529b43c0cff56ebaa71f5f021bbd755bc1419190e", "fe2928d3f532ed32b39c32a482b54136fe766d19936afc96c8f00645f9da1a06"]
|
||||
pyyaml = ["0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", "01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", "5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", "5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", "7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", "7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", "87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", "9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", "a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", "b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", "b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", "bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", "f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"]
|
||||
pyyaml = ["0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", "2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", "35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", "38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", "483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", "4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", "7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", "8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", "c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", "e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", "ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"]
|
||||
regex = ["032fdcc03406e1a6485ec09b826eac78732943840c4b29e503b789716f051d8d", "0e6cf1e747f383f52a0964452658c04300a9a01e8a89c55ea22813931b580aa8", "106e25a841921d8259dcef2a42786caae35bc750fb996f830065b3dfaa67b77e", "1768cf42a78a11dae63152685e7a1d90af7a8d71d2d4f6d2387edea53a9e0588", "27d1bd20d334f50b7ef078eba0f0756a640fd25f5f1708d3b5bed18a5d6bced9", "29b20f66f2e044aafba86ecf10a84e611b4667643c42baa004247f5dfef4f90b", "4850c78b53acf664a6578bba0e9ebeaf2807bb476c14ec7e0f936f2015133cae", "57eacd38a5ec40ed7b19a968a9d01c0d977bda55664210be713e750dd7b33540", "724eb24b92fc5fdc1501a1b4df44a68b9c1dda171c8ef8736799e903fb100f63", "77ae8d926f38700432807ba293d768ba9e7652df0cbe76df2843b12f80f68885", "78b3712ec529b2a71731fbb10b907b54d9c53a17ca589b42a578bc1e9a2c82ea", "7bbbdbada3078dc360d4692a9b28479f569db7fc7f304b668787afc9feb38ec8", "8d9ef7f6c403e35e73b7fc3cde9f6decdc43b1cb2ff8d058c53b9084bfcb553e", "a83049eb717ae828ced9cf607845929efcb086a001fc8af93ff15c50012a5716", "adc35d38952e688535980ae2109cad3a109520033642e759f987cf47fe278aa1", "c29a77ad4463f71a506515d9ec3a899ed026b4b015bf43245c919ff36275444b", "cfd31b3300fefa5eecb2fe596c6dee1b91b3a05ece9d5cfd2631afebf6c6fadd", "d3ee0b035816e0520fac928de31b6572106f0d75597f6fa3206969a02baba06f", "d508875793efdf6bab3d47850df8f40d4040ae9928d9d80864c1768d6aeaf8e3", "ef0b828a7e22e58e06a1cceddba7b4665c6af8afeb22a0d8083001330572c147", "faad39fdbe2c2ccda9846cd21581063086330efafa47d87afea4073a08128656"]
|
||||
secretstorage = ["20c797ae48a4419f66f8d28fc221623f11fc45b6828f96bdb1ad9990acb59f92", "7a119fb52a88e398dbb22a4b3eb39b779bfbace7e4153b7bc6e5954d86282a8a"]
|
||||
six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"]
|
||||
six = ["1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", "30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"]
|
||||
toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"]
|
||||
tornado = ["349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", "398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", "4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", "559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", "abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", "c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", "c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"]
|
||||
typed-ast = ["1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", "838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"]
|
||||
tzlocal = ["4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e"]
|
||||
zipp = ["3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", "f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"]
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
[tool.poetry]
|
||||
name = "jrnl"
|
||||
version = "v2.1.1"
|
||||
version = "v2.2.1-beta2"
|
||||
description = "Collect your thoughts and notes without leaving the command line."
|
||||
authors = [
|
||||
"Manuel Ebert <manuel@1450.me>",
|
||||
"Jonathan Wren <jonathan@nowandwren.com>",
|
||||
"Micah Ellison <micahellison@gmail.com>"
|
||||
]
|
||||
maintainers = [
|
||||
"Jonathan Wren and Micah Ellison <jrnl-sh@googlegroups.com>",
|
||||
]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
homepage = "https://jrnl.sh"
|
||||
repository = "https://github.com/jrnl-org/jrnl"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.6.0, <3.8.0"
|
||||
python = ">=3.6.0, <3.9.0"
|
||||
pyxdg = "^0.26.0"
|
||||
cryptography = "^2.7"
|
||||
passlib = "^1.7"
|
||||
|
@ -30,7 +33,7 @@ pyyaml = "^5.1"
|
|||
behave = "^1.2"
|
||||
mkdocs = "^1.0"
|
||||
flake8 = "^3.7"
|
||||
black = {version = "^18.3-alpha.0",allows-prereleases = true}
|
||||
black = {version = "^19.10b0",allow-prereleases = true}
|
||||
|
||||
[tool.poetry.scripts]
|
||||
jrnl = 'jrnl.cli:run'
|
||||
|
|
Loading…
Add table
Reference in a new issue