Merge branch 'develop' into maebert-fast-import

This commit is contained in:
Jonathan Wren 2019-12-10 20:47:39 -08:00 committed by GitHub
commit 6a8f3edec1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 1102 additions and 699 deletions

View file

@ -1,35 +0,0 @@
Hello! Thank you for reporting an issue!
If you would fill out the below points, that would make our process a whole lot easier!
## **I'm submitting a ...**
- [ ] bug report
- [ ] feature request
- [ ] support request
## Regarding bug reports:
* **Please tell us about your environment:**
- Jrnl version: (run `jrnl -v`)
- How you installed Jrnl
- Operating system [MacOS, Linux, Windows?]
* **What is the current behavior?**
* **Please provide the steps to reproduce and if possible a minimal demo of the problem**
* **What is the expected behavior?**
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc)
## Regarding Feature requests:
* **What is the motivation / use case for changing the behavior?**
* **Please provide examples of the usage**
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc)

View file

@ -1,20 +1,26 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ":new:, bug"
assignees: ''
---
## Bug report ## Bug report
Hello! Thank you for reporting an issue! Hello! Thank you for reporting an issue!
If you would fill out the below points, that would make our process a whole lot easier! If you would fill out the below points, that would make our process a whole lot easier!
* **Please tell us about your environment:**
* **Please tell us about your environment:**
- Jrnl version: (run `jrnl -v`) - Jrnl version: (run `jrnl -v`)
- How you installed Jrnl - How you installed Jrnl
- Operating system [MacOS, Linux, Windows?] - Operating system [MacOS, Linux, Windows?]
* **What is the current behavior?** * **What is the current behavior?**
* **Please provide the steps to reproduce and if possible a minimal demo of the problem** * **Please provide the steps to reproduce and if possible a minimal demo of the problem**
* **What is the expected behavior?** * **What is the expected behavior?**
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc) * **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc)

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1 @@
blank_issues_enabled: false

View file

@ -1,4 +1,14 @@
#Feature Request ---
name: Feature request
about: Suggest an idea for jrnl
title: ''
labels: ":new:, enhancement"
assignees: ''
---
## Feature Request
Hello! Thank you for reporting an issue! Hello! Thank you for reporting an issue!
If you would fill out the below points, that would make our process a whole lot easier! If you would fill out the below points, that would make our process a whole lot easier!

View file

@ -1,14 +1,22 @@
---
name: Support Request
about: Get help with jrnl
title: ''
labels: ":new:, support"
assignees: ''
---
## Support request ## Support request
Hello! Thank you for reporting an issue! Hello! Thank you for reporting an issue!
If you would fill out the below points, that would make our process a whole lot easier! If you would fill out the below points, that would make our process a whole lot easier!
* **Please tell us about your environment:** * **Please tell us about your environment:**
- Jrnl version: (run `jrnl -v`) - Jrnl version: (run `jrnl -v`)
- How you installed Jrnl - How you installed Jrnl
- Operating system [MacOS, Linux, Windows?] - Operating system [MacOS, Linux, Windows?]
* **What are you trying to do?** * **What are you trying to do?**
@ -16,4 +24,3 @@ If you would fill out the below points, that would make our process a whole lot
* **What have you tried?** * **What have you tried?**
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc) * **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc)

View file

@ -1,25 +1,127 @@
dist: xenial # required for Python >= 3.7 dist: xenial # required for Python >= 3.7
os: linux
language: python language: python
python: "3.7"
cache:
- pip
git:
depth: false
before_install: before_install:
- pip install poetry - date
install: install:
# we run `poetry version` here to appease poetry about '0.0.0-source' - pip install poetry~=0.12.17
- poetry version
- poetry install - poetry install
script:
- poetry run python --version - poetry run python --version
script:
- poetry run behave - poetry run behave
before_deploy:
- pip install poetry aliases:
- poetry config http-basic.pypi $PYPI_USER $PYPI_PASS test_mac: &test_mac
- echo __version__ = \"$TRAVIS_TAG\" > jrnl/__version__.py os: osx
- poetry version $TRAVIS_TAG language: shell
- poetry build osx_image: xcode11.2
deploy: cache:
- provider: script directories:
script: poetry publish - $HOME/.pyenv/versions
skip_cleanup: true before_install:
on: - eval "$(pyenv init -)"
branch: master - pyenv install -s $JRNL_PYTHON_VERSION
tags: true - 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
- os: windows
include:
# 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
# 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
# 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
# ... 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
- stage: Deploy
if: branch = master AND tag IS present
before_deploy:
- poetry config http-basic.pypi "$PYPI_USER" "$PYPI_PASS"
- poetry version "$TRAVIS_TAG"
- echo __version__ = \"$TRAVIS_TAG\" > jrnl/__version__.py
- poetry build
deploy:
- provider: script
script: poetry publish
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

View file

@ -3,7 +3,7 @@ Changelog
## 2.0 ## 2.0
* Cryptographical backend changed from PyCrypto to cryptography.io * Cryptographic backend changed from PyCrypto to cryptography.io
* Config now respects XDG conventions and may move accordingly * Config now respects XDG conventions and may move accordingly
* Config now saved as YAML * Config now saved as YAML
* Config name changed from `journals.jrnl_name.journal` to `journals.jrnl_name.path` * Config name changed from `journals.jrnl_name.journal` to `journals.jrnl_name.path`

View file

@ -1,35 +1,45 @@
Contributing # Contributing
============
If you use jrnl, you can totally make our day by just saying "thanks for the code." It's your chance to make a programmer happy today! If you have a moment, let us know what you use jrnl for and how; it'll help us to make it even better! If you use jrnl, you can totally make our day by just saying "thanks for the code." It's your chance to make a programmer happy today! If you have a moment, let us know what you use jrnl for and how; it'll help us to make it even better!
Docs & Typos # Table of Contents
------------ * [Docs and Typos](#docs-and-typos)
* [Bugs](#bugs)
* [Feature requests and ideas](#feature-requests-and-ideas)
* [New programmers and programmers new to python](#new-programmers-and-programmers-new-to-python)
* [Developing jrnl](#developing-jrnl)
## Docs and Typos
If you find a typo or a mistake in the docs, please fix it right away and send a pull request. The Right Way™ to fix the docs is to edit the `docs/*.md` files on the **master** branch. You can see the result if you run `make html` inside the project's root directory, which will open a browser that hot-reloads as you change the docs. This requires [mkdocs](https://www.mkdocs.org) to be installed. The `gh-pages` branch is automatically maintained and updates from `master`; you should never have to edit that. If you find a typo or a mistake in the docs, please fix it right away and send a pull request. The Right Way™ to fix the docs is to edit the `docs/*.md` files on the **master** branch. You can see the result if you run `make html` inside the project's root directory, which will open a browser that hot-reloads as you change the docs. This requires [mkdocs](https://www.mkdocs.org) to be installed. The `gh-pages` branch is automatically maintained and updates from `master`; you should never have to edit that.
Bugs ## Bugs
----
Unfortunately, bugs happen. If you found one, please [open a new issue](https://github.com/jrnl-org/jrnl/issues/new) and describe it as well as possible. If you're a programmer with some time, go ahead and send us a pull request! We'll review as quickly as we can. Unfortunately, bugs happen. If you found one, please [open a new issue](https://github.com/jrnl-org/jrnl/issues/new/choose) and describe it as well as possible. If you're a programmer with some time, go ahead and send us a pull request that references the issue! We'll review as quickly as we can.
## Feature requests and ideas
Feature requests and ideas So, you have an idea for a great feature? Awesome! We'd love to hear from you! Please [open a new issue](https://github.com/jrnl-org/jrnl/issues/new/choose) and describe the goal of the feature, and any relevant use cases. We'll discuss the issue with you, and decide if it's a good fit for the project.
--------------------------
So, you have an idea for a great feature? Awesome! We'd love to hear from you! Please [open a new issue](https://github.com/jrnl-org/jrnl/issues) and describe the goal of the feature, and any relvant use cases. We'll discuss the issue with you, and decide if it's a good fit for the project.
When discussing new features, please keep in mind our design goals. jrnl strives to do one thing well. To us, that means: When discussing new features, please keep in mind our design goals. jrnl strives to do one thing well. To us, that means:
* be _slim_ * be _slim_
* have a simple interface * have a simple interface
* avoid dupicating functionality * avoid duplicating functionality
## New programmers and programmers new to python
A short note for new programmers and programmers new to python Although jrnl has grown quite a bit since its inception, the overall complexity (for an end-user program) is fairly low, and we hope you'll find the code easy enough to understand.
--------------------------------------------------------------
Although jrnl has grown quite a bit since its inception. The overall complexity (for an end-user program) is fairly low, and we hope you'll find the code easy enough to understand.
If you have a question, please don't hesitate to ask! Python is known for its welcoming community and openness to novice programmers, so feel free to fork the code and play around with it! If you create something you want to share with us, please create a pull request. We never expect pull requests to be perfect, idiomatic, instantly mergeable code. We can work through it together! If you have a question, please don't hesitate to ask! Python is known for its welcoming community and openness to novice programmers, so feel free to fork the code and play around with it! If you create something you want to share with us, please create a pull request. We never expect pull requests to be perfect, idiomatic, instantly mergeable code. We can work through it together!
## Developing jrnl
The jrnl source uses [poetry](https://poetry.eustace.io/) for dependency management. You will need to install it to develop journal.
* To run tests: `make test` (or `poetry run behave` if on Windows)
* To run the source: `poetry install` then `poetry shell` then run `jrnl` with or without arguments as necessary
For testing, jrnl uses [behave](https://behave.readthedocs.io/).

View file

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2014 Manuel Ebert Copyright (c) 2014-2019 Manuel Ebert
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -3,15 +3,19 @@
## Configuration File ## Configuration File
You can configure the way jrnl behaves in a configuration file. By You can configure the way jrnl behaves in a configuration file. By
default, this is `~/.jrnl_config`. If you have the `XDG_CONFIG_HOME` default, this is `~/.config/jrnl/jrnl.yaml`. If you have the `XDG_CONFIG_HOME`
variable set, the configuration file will be saved as variable set, the configuration file will be saved as
`$XDG_CONFIG_HOME/jrnl/.jrnl_config`. `$XDG_CONFIG_HOME/jrnl/jrnl.yaml`.
!!! note !!! note
On Windows, The configuration file is typically found at `C:\Users\[Your Username]\.jrnl_config`. On Windows, the configuration file is typically found at `%USERPROFILE%\.config\jrnl\jrnl.yaml`.
The configuration file is a simple JSON file with the following options The configuration file is a YAML file with the following options
and can be edited with any plain text editor. and can be edited with a plain text editor.
!!! note
Backup your config file before editing. Changes to the config file
have destructive effects on your journal!
- `journals` - `journals`
paths to your journal files paths to your journal files
@ -51,46 +55,16 @@ and can be edited with any plain text editor.
Or use the built-in prompt or an external editor to compose your Or use the built-in prompt or an external editor to compose your
entries. entries.
## DayOne Integration
Using your DayOne journal instead of a flat text file is dead simple --
instead of pointing to a text file, change your `.jrnl_config` to point
to your DayOne journal. This is a folder named something like
`Journal_dayone` or `Journal.dayone`, and it's located at
- `~/Library/Application Support/Day One/` by default
- `~/Dropbox/Apps/Day One/` if you're syncing with Dropbox and
- `~/Library/Mobile
Documents/5U8NS4GX82~com~dayoneapp~dayone/Documents/` if you're
syncing with iCloud.
Instead of all entries being in a single file, each entry will live in a
separate `plist` file. So your `.jrnl_config` should look like this:
``` javascript
{
...
"journals": {
"default": "~/journal.txt",
"dayone": "~/Library/Mobile Documents/5U8NS4GX82~com~dayoneapp~dayone/Documents/Journal_dayone"
}
}
```
## Multiple journal files ## Multiple journal files
You can configure `jrnl`to use with multiple journals (eg. You can configure `jrnl`to use with multiple journals (eg.
`private` and `work`) by defining more journals in your `.jrnl_config`, `private` and `work`) by defining more journals in your `jrnl.yaml`,
for example: for example:
``` javascript ``` yaml
{ journals:
... default: ~\journal.txt
"journals": { work: ~\work.txt
"default": "~/journal.txt",
"work": "~/work.txt"
}
}
``` ```
The `default` journal gets created the first time you start `jrnl` The `default` journal gets created the first time you start `jrnl`
@ -106,26 +80,22 @@ will both use `~/work.txt`, while `jrnl -n 3` will display the last
three entries from `~/journal.txt` (and so does `jrnl default -n 3`). three entries from `~/journal.txt` (and so does `jrnl default -n 3`).
You can also override the default options for each individual journal. You can also override the default options for each individual journal.
If you `.jrnl_config` looks like this: If your `jrnl.yaml` looks like this:
``` javascript ``` yaml
{ encrypt: false
... journals:
"encrypt": false default: ~/journal.txt
"journals": { work:
"default": "~/journal.txt", journal: ~/work.txt
"work": { encrypt: true
"journal": "~/work.txt", food: ~/my_recipes.txt
"encrypt": true
},
"food": "~/my_recipes.txt",
}
``` ```
Your `default` and your `food` journals won't be encrypted, however your Your `default` and your `food` journals won't be encrypted, however your
`work` journal will! You can override all options that are present at `work` journal will! You can override all options that are present at
the top level of `.jrnl_config`, just make sure that at the very least the top level of `jrnl.yaml`, just make sure that at the very least
you specify a `"journal": ...` key that points to the journal file of you specify a `journal: ...` key that points to the journal file of
that journal. that journal.
!!! note !!! note

View file

@ -45,9 +45,17 @@ p {
/* No-one likes lines that are 400 characters long. */ /* No-one likes lines that are 400 characters long. */
div.rst-content {max-width: 54em;} 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; 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 { .wy-side-nav-search a.icon-home {
width: 100%; width: 100%;
max-width: 250px; max-width: 250px;

View file

@ -2,9 +2,9 @@
## Encrypting and decrypting ## Encrypting and decrypting
If you don't choose to encrypt your file when you run If you dont choose to encrypt your file when you run
`jrnl` for the first time, you can encrypt `jrnl` for the first time, you can encrypt
your existing journal file or change its password using your existing journal file or change its password using this:
``` sh ``` sh
jrnl --encrypt jrnl --encrypt
@ -18,44 +18,50 @@ replaced by the encrypted file. Conversely,
jrnl --decrypt jrnl --decrypt
``` ```
will replace your encrypted journal file by a Journal in plain text. You will replace your encrypted journal file with a journal in plain text. You
can also specify a filename, ie. `jrnl --decrypt plain_text_copy.txt`, can also specify a filename, i.e. `jrnl --decrypt plain_text_copy.txt`,
to leave your original file untouched. to leave your original file untouched.
## Storing passwords in your keychain ## Storing passwords in your keychain
Whenever you encrypt your journal, you are asked whether you want to Whenever you encrypt your journal, you are asked whether you want to
store the encryption password in your keychain. If you do this, you store the encryption password in your keychain. If you do this, you
won't have to enter your password every time you want to write or read wont have to enter your password every time you want to write or read
your journal. your journal.
If you don't initially store the password in the keychain but decide to If you dont initially store the password in the keychain but decide to
do so at a later point -- or maybe want to store it on one computer but do so at a later point or maybe want to store it on one computer but
not on another -- you can simply run `jrnl --encrypt` on an encrypted not on another you can simply run `jrnl --encrypt` on an encrypted
journal and use the same password again. journal and use the same password again.
## A note on security ## A note on security
While jrnl follows best practises, true security is an illusion. While jrnl follows best practises, true security is an illusion.
Specifically, jrnl will leave traces in your memory and your shell Specifically, jrnl will leave traces in your memory and your shell
history -- it's meant to keep journals secure in transit, for example history its meant to keep journals secure in transit, for example
when storing it on an when storing it on an
[untrusted](http://techcrunch.com/2014/04/09/condoleezza-rice-joins-dropboxs-board/) [untrusted](http://techcrunch.com/2014/04/09/condoleezza-rice-joins-dropboxs-board/)
services such as Dropbox. If you're concerned about security, disable services such as Dropbox. If youre concerned about security, disable
history logging for journal in your `.bashrc` history logging for journal in your `.bashrc`:
``` sh ``` sh
HISTIGNORE="$HISTIGNORE:jrnl *" HISTIGNORE="$HISTIGNORE:jrnl *"
``` ```
If you are using zsh instead of bash, you can get the same behaviour If you are using zsh instead of bash, you can get the same behaviour by
adding this to your `zshrc` adding this to your `zshrc`:
``` sh ``` sh
setopt HIST_IGNORE_SPACE setopt HIST_IGNORE_SPACE
alias jrnl=" jrnl" 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 fishs history, run
`history delete --prefix 'jrnl '`.
## Manual decryption ## Manual decryption
Should you ever want to decrypt your journal manually, you can do so Should you ever want to decrypt your journal manually, you can do so
@ -63,8 +69,8 @@ with any program that supports the AES algorithm in CBC. The key used
for encryption is the SHA-256-hash of your password, the IV for encryption is the SHA-256-hash of your password, the IV
(initialisation vector) is stored in the first 16 bytes of the encrypted (initialisation vector) is stored in the first 16 bytes of the encrypted
file. The plain text is encoded in UTF-8 and padded according to PKCS\#7 file. The plain text is encoded in UTF-8 and padded according to PKCS\#7
before being encrypted. Here's a Python script that you can use to before being encrypted. Heres a Python script that you can use to
decrypt your journal decrypt your journal:
``` python ``` python
#!/usr/bin/env python3 #!/usr/bin/env python3

View file

@ -15,27 +15,12 @@ On other platforms, install *jrnl* using pip
pip install jrnl pip install jrnl
``` ```
Or, if you want the option to encrypt your journal,
``` sh
pip install jrnl[encrypted]
```
to install the dependencies for encrypting journals as well.
!!! note
Installing the encryption library, `pycrypto`, requires a `gcc` compiler. For this reason, jrnl will
not install `pycrypto` unless explicitly told so like this. You can [install PyCrypto manually](https://www.dlitz.net/software/pycrypto/)
first or install it with `pip install pycrypto` if you have a `gcc` compiler.
Also note that when using zsh, the correct syntax is `pip install "jrnl[encrypted]"` (note the quotes).
The first time you run `jrnl` you will be asked where your journal file The first time you run `jrnl` you will be asked where your journal file
should be created and whether you wish to encrypt it. should be created and whether you wish to encrypt it.
## Quickstart ## Quickstart
to make a new entry, just type To make a new entry, just type
``` sh ``` sh
jrnl yesterday: Called in sick. Used the time to clean the house and spent 4h on writing my book. jrnl yesterday: Called in sick. Used the time to clean the house and spent 4h on writing my book.

View file

@ -8,10 +8,6 @@ files - you can put them into a Dropbox folder for instant syncing and
you can be assured that your journal will still be readable in 2050, you can be assured that your journal will still be readable in 2050,
when all your fancy iPad journal applications will long be forgotten. when all your fancy iPad journal applications will long be forgotten.
`jrnl` also plays nice with the fabulous
[DayOne](http://dayoneapp.com) and can read and write directly from and
to DayOne Journals.
Optionally, your journal can be encrypted using the [256-bit Optionally, your journal can be encrypted using the [256-bit
AES](http://en.wikipedia.org/wiki/Advanced_Encryption_Standard). AES](http://en.wikipedia.org/wiki/Advanced_Encryption_Standard).

View file

@ -7,7 +7,7 @@
If I want to find out how often I mentioned my flatmates Alberto and If I want to find out how often I mentioned my flatmates Alberto and
Melo in the same entry, I run Melo in the same entry, I run
``` sh ```sh
jrnl @alberto --tags | grep @melo jrnl @alberto --tags | grep @melo
``` ```
@ -22,7 +22,7 @@ each tag occurred in this filtered journal. Finally, we pipe this to
You can do things like You can do things like
``` sh ```sh
jrnl @fixed -starred -n 10 -until "jan 2013" --short jrnl @fixed -starred -n 10 -until "jan 2013" --short
``` ```
@ -33,14 +33,14 @@ January 1, 2013 that are tagged with `@fixed`.
How much did I write last year? How much did I write last year?
``` sh ```sh
jrnl -from "jan 1 2013" -until "dec 31 2013" | wc -w jrnl -from "jan 1 2013" -until "dec 31 2013" | wc -w
``` ```
Will give you the number of words you wrote in 2013. How long is my Will give you the number of words you wrote in 2013. How long is my
average entry? average entry?
``` sh ```sh
expr $(jrnl --export text | wc -w) / $(jrnl --short | wc -l) expr $(jrnl --export text | wc -w) / $(jrnl --short | wc -l)
``` ```
@ -50,11 +50,10 @@ print exactly one line per entry).
### Importing older files ### Importing older files
If you want to import a file as an entry to jrnl, you can just do `jrnl If you want to import a file as an entry to jrnl, you can just do `jrnl < entry.ext`. But what if you want the modification date of the file to
< entry.ext`. But what if you want the modification date of the file to
be the date of the entry in jrnl? Try this be the date of the entry in jrnl? Try this
``` sh ```sh
echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' entry.txt` `cat entry.txt` | jrnl echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' entry.txt` `cat entry.txt` | jrnl
``` ```
@ -63,7 +62,7 @@ then combine it with the contents of the file before piping it to jrnl.
If you do that often, consider creating a function in your `.bashrc` or If you do that often, consider creating a function in your `.bashrc` or
`.bash_profile` `.bash_profile`
``` sh ```sh
jrnlimport () { jrnlimport () {
echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' $1` `cat $1` | jrnl echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' $1` `cat $1` | jrnl
} }
@ -83,7 +82,7 @@ Another nice solution that allows you to define individual prompts comes
from [Jacobo de from [Jacobo de
Vera](https://github.com/maebert/jrnl/issues/194#issuecomment-47402869): Vera](https://github.com/maebert/jrnl/issues/194#issuecomment-47402869):
``` sh ```sh
function log_question() function log_question()
{ {
echo $1 echo $1
@ -94,22 +93,32 @@ log_question 'What did I achieve today?'
log_question 'What did I make progress with?' log_question 'What did I make progress with?'
``` ```
### Display random entry
You can use this to select one title at random and then display the whole
entry. The invocation of `cut` needs to match the format of the timestamp.
For timestamps that have a space between data and time components, select
fields 1 and 2 as shown. For timestamps that have no whitespace, select
only field 1.
```sh
jrnl -on "$(jrnl --short | shuf -n 1 | cut -d' ' -f1,2)"
```
## External editors ## External editors
To use external editors for writing and editing journal entries, set To use external editors for writing and editing journal entries, set
them up in your `.jrnl_config` (see `advanced usage <advanced>` for them up in your `jrnl.yaml` (see `advanced usage <advanced>` for
details). Generally, after writing an entry, you will have to save and details). Generally, after writing an entry, you will have to save and
close the file to save the changes to jrnl. close the file to save the changes to jrnl.
### Sublime Text ### Sublime Text
To use Sublime Text, install the command line tools for Sublime Text and To use Sublime Text, install the command line tools for Sublime Text and
configure your `.jrnl_config` like this: configure your `jrnl.yaml` like this:
``` json ```yaml
{ editor: "subl -w"
"editor": "subl -w"
}
``` ```
Note the `-w` flag to make sure jrnl waits for Sublime Text to close the Note the `-w` flag to make sure jrnl waits for Sublime Text to close the
@ -121,22 +130,18 @@ 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 the the process to wait until the file is closed before passing control
back to journal. In the case of MacVim, this is `-f`: back to journal. In the case of MacVim, this is `-f`:
``` json ```yaml
{ editor: "mvim -f"
"editor": "mvim -f"
}
``` ```
### iA Writer ### iA Writer
On OS X, you can use the fabulous [iA On OS X, you can use the fabulous [iA
Writer](http://www.iawriter.com/mac) to write entries. Configure your Writer](http://www.iawriter.com/mac) to write entries. Configure your
`.jrnl_config` like this: `jrnl.yaml` like this:
``` json ```yaml
{ editor: "open -b pro.writer.mac -Wn"
"editor": "open -b pro.writer.mac -Wn"
}
``` ```
What does this do? `open -b ...` opens a file using the application What does this do? `open -b ...` opens a file using the application
@ -148,19 +153,17 @@ If the `pro.writer.mac` bundle identifier is not found on your system,
you can find the right string to use by inspecting iA Writer's you can find the right string to use by inspecting iA Writer's
`Info.plist` file in your shell: `Info.plist` file in your shell:
``` sh ```sh
grep -A 1 CFBundleIdentifier /Applications/iA\ Writer.app/Contents/Info.plist grep -A 1 CFBundleIdentifier /Applications/iA\ Writer.app/Contents/Info.plist
``` ```
### Notepad++ on Windows ### Notepad++ on Windows
To set [Notepad++](http://notepad-plus-plus.org/) as your editor, edit To set [Notepad++](http://notepad-plus-plus.org/) as your editor, edit
the jrnl config file (`.jrnl_config`) like this: the jrnl config file (`jrnl.yaml`) like this:
``` json ```yaml
{ editor: "C:\\Program Files (x86)\\Notepad++\\notepad++.exe -multiInst -nosession"
"editor": "C:\\Program Files (x86)\\Notepad++\\notepad++.exe -multiInst -nosession",
}
``` ```
The double backslashes are needed so jrnl can read the file path The double backslashes are needed so jrnl can read the file path
@ -169,12 +172,10 @@ its own Notepad++ window.
### Visual Studio Code ### Visual Studio Code
To set [Visual Studo Code](https://code.visualstudio.com) as your editor on Linux, edit `.jrnl_config` like this: To set [Visual Studo Code](https://code.visualstudio.com) as your editor on Linux, edit `jrnl.yaml` like this:
```json ```yaml
{ editor: "/usr/bin/code --wait"
"editor": "/usr/bin/code --wait",
}
``` ```
The `--wait` argument tells VS Code to wait for files to be written out before handing back control to jrnl. The `--wait` argument tells VS Code to wait for files to be written out before handing back control to jrnl.
@ -184,14 +185,13 @@ On MacOS you will need to add VS Code to your PATH. You can do that by adding:
```sh ```sh
export PATH="\$PATH:/Applications/Visual Studio Code.app/Contents/Resources/app/bin" export PATH="\$PATH:/Applications/Visual Studio Code.app/Contents/Resources/app/bin"
``` ```
to your `.bash_profile`, or by running the **Install 'code' command in PATH** command from the command pallet in VS Code. to your `.bash_profile`, or by running the **Install 'code' command in PATH** command from the command pallet in VS Code.
Then you can add: Then you can add:
```javascript ```yaml
{ editor: "code --wait"
"editor": "code --wait",
}
``` ```
to ``.jrnl_config``. See also the [Visual Studio Code documentation](https://code.visualstudio.com/docs/setup/mac) to `jrnl.yaml`. See also the [Visual Studio Code documentation](https://code.visualstudio.com/docs/setup/mac)

View file

@ -82,11 +82,6 @@
<h3>Accessible anywhere.</h3> <h3>Accessible anywhere.</h3>
<p>Sync your journals with Dropbox and capture your thoughts where ever you are</p> <p>Sync your journals with Dropbox and capture your thoughts where ever you are</p>
</section> </section>
<section>
<i class="icon dayone"></i>
<h3>DayOne compatible.</h3>
<p>Read, write and search your DayOne journal from the command line.</p>
</section>
<section> <section>
<i class="icon github"></i> <i class="icon github"></i>
<h3>Free &amp; Open Source.</h3> <h3>Free &amp; Open Source.</h3>

View file

@ -1,15 +1,15 @@
# Basic Usage # Basic Usage
`jrnl` has two modes: **composing** and **viewing**. Basically, whenever `jrnl` has two modes: **composing** and **viewing**. Basically, whenever
you *don't* supply any arguments that start you _don't_ supply any arguments that start
with a dash or double-dash, you're in composing mode, meaning you can with a dash or double-dash, you're in composing mode, meaning you can
write your entry on the command line or an editor of your choice. write your entry on the command line or an editor of your choice.
We intentionally break a convention on command line arguments: all We intentionally break a convention on command line arguments: all
arguments starting with a *single dash* arguments starting with a _single dash_
will *filter* your journal before viewing will _filter_ your journal before viewing
it, and can be combined arbitrarily. Arguments with a it, and can be combined arbitrarily. Arguments with a
*double dash* will control how your journal _double dash_ will control how your journal
is displayed or exported and are mutually exclusive (ie. you can only is displayed or exported and are mutually exclusive (ie. you can only
specify one way to display or export your journal at a time). specify one way to display or export your journal at a time).
@ -17,7 +17,7 @@ specify one way to display or export your journal at a time).
You can list the journals accessible by jrnl You can list the journals accessible by jrnl
``` sh ```sh
jrnl -ls jrnl -ls
``` ```
@ -30,21 +30,21 @@ Composing mode is entered by either starting `jrnl` without any
arguments -- which will prompt you to write an entry or launch your arguments -- which will prompt you to write an entry or launch your
editor -- or by just writing an entry on the prompt, such as editor -- or by just writing an entry on the prompt, such as
``` sh ```sh
jrnl today at 3am: I just met Steve Buscemi in a bar! He looked funny. jrnl today at 3am: I just met Steve Buscemi in a bar! He looked funny.
``` ```
!!! note !!! note
Most shell contains a certain number of reserved characters, such as `#` Most shell contains a certain number of reserved characters, such as `#`
and `*`. Unbalanced quotes, parenthesis, and so on will also get into and `*`. Unbalanced quotes, parenthesis, and so on will also get into
the way of your editing. the way of your editing.
For writing longer entries, just enter `jrnl` For writing longer entries, just enter `jrnl`
and hit `return`. Only then enter the text of your journal entry. and hit `return`. Only then enter the text of your journal entry.
Alternatively, `use an external editor <advanced>`). Alternatively, `use an external editor <advanced>`).
You can also import an entry directly from a file You can also import an entry directly from a file
``` sh ```sh
jrnl < my_entry.txt jrnl < my_entry.txt
``` ```
@ -52,51 +52,51 @@ jrnl < my_entry.txt
Timestamps that work: Timestamps that work:
- at 6am - at 6am
- yesterday - yesterday
- last monday - last monday
- sunday at noon - sunday at noon
- 2 march 2012 - 2 march 2012
- 7 apr - 7 apr
- 5/20/1998 at 23:42 - 5/20/1998 at 23:42
### Starring entries ### Starring entries
To mark an entry as a favourite, simply "star" it To mark an entry as a favourite, simply "star" it
``` sh ```sh
jrnl last sunday *: Best day of my life. jrnl last sunday *: Best day of my life.
``` ```
If you don't want to add a date (ie. your entry will be dated as now), If you don't want to add a date (ie. your entry will be dated as now),
The following options are equivalent: The following options are equivalent:
- `jrnl *: Best day of my life.` - `jrnl *: Best day of my life.`
- `jrnl *Best day of my life.` - `jrnl *Best day of my life.`
- `jrnl Best day of my life.*` - `jrnl Best day of my life.*`
!!! note !!! note
Just make sure that the asterisk sign is **not** surrounded by Just make sure that the asterisk sign is **not** surrounded by
whitespaces, e.g. `jrnl Best day of my life! *` will **not** work (the 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). reason being that the `*` sign has a special meaning on most shells).
## Viewing ## Viewing
``` sh ```sh
jrnl -n 10 jrnl -n 10
``` ```
will list you the ten latest entries (if you're lazy, `jrnl -10` will do will list you the ten latest entries (if you're lazy, `jrnl -10` will do
the same), the same),
``` sh ```sh
jrnl -from "last year" -until march jrnl -from "last year" -until march
``` ```
everything that happened from the start of last year to the start of everything that happened from the start of last year to the start of
last march. To only see your favourite entries, use last march. To only see your favourite entries, use
``` sh ```sh
jrnl -starred jrnl -starred
``` ```
@ -105,20 +105,20 @@ jrnl -starred
Keep track of people, projects or locations, by tagging them with an `@` Keep track of people, projects or locations, by tagging them with an `@`
in your entries in your entries
``` sh ```sh
jrnl Had a wonderful day on the @beach with @Tom and @Anna. jrnl Had a wonderful day on the @beach with @Tom and @Anna.
``` ```
You can filter your journal entries just like this: You can filter your journal entries just like this:
``` sh ```sh
jrnl @pinkie @WorldDomination jrnl @pinkie @WorldDomination
``` ```
Will print all entries in which either `@pinkie` or `@WorldDomination` Will print all entries in which either `@pinkie` or `@WorldDomination`
occurred. occurred.
``` sh ```sh
jrnl -n 5 -and @pineapple @lubricant jrnl -n 5 -and @pineapple @lubricant
``` ```
@ -127,18 +127,18 @@ You can change which symbols you'd like to use for tagging in the
configuration. configuration.
!!! note !!! note
`jrnl @pinkie @WorldDomination` will switch to viewing mode because `jrnl @pinkie @WorldDomination` will switch to viewing mode because
although **no** command line arguments are given, all the input strings although **no** command line arguments are given, all the input strings
look like tags - *jrnl* will assume you want to filter by tag. look like tags - _jrnl_ will assume you want to filter by tag.
## Editing older entries ## Editing older entries
You can edit selected entries after you wrote them. This is particularly You can edit selected entries after you wrote them. This is particularly
useful when your journal file is encrypted or if you're using a DayOne useful when your journal file is encrypted. To use this feature, you need
journal. To use this feature, you need to have an editor configured in to have an editor configured in your journal configuration file (see
your journal configuration file (see `advanced usage <advanced>`) `advanced usage <advanced>`)
``` sh ```sh
jrnl -until 1950 @texas -and @history --edit jrnl -until 1950 @texas -and @history --edit
``` ```
@ -153,30 +153,8 @@ encrypt) your edited journal after you save and exit the editor.
You can also use this feature for deleting entries from your journal You can also use this feature for deleting entries from your journal
``` sh ```sh
jrnl @girlfriend -until 'june 2012' --edit jrnl @girlfriend -until 'june 2012' --edit
``` ```
Just select all text, press delete, and everything is gone... Just select all text, press delete, and everything is gone...
### Editing DayOne Journals
DayOne journals can be edited exactly the same way, however the output
looks a little bit different because of the way DayOne stores its
entries:
```md
# af8dbd0d43fb55458f11aad586ea2abf
2013-05-02 15:30 I told everyone I built my @robot wife for sex.
But late at night when we're alone we mostly play Battleship.
# 2391048fe24111e1983ed49a20be6f9e
2013-08-10 03:22 I had all kinds of plans in case of a @zombie attack.
I just figured I'd be on the other side.
```
The long strings starting with hash symbol are the so-called UUIDs,
unique identifiers for each entry. Don't touch them. If you do, then the
old entry would get deleted and a new one written, which means that you
could lose DayOne data that jrnl can't handle (such as as the entry's
geolocation).

29
features/contains.feature Normal file
View 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"

View file

@ -20,6 +20,19 @@ Feature: Basic reading and writing to a journal
When we run "jrnl -n 1" When we run "jrnl -n 1"
Then the output should contain "2013-07-23 09:00 A cold and stormy day." Then the output should contain "2013-07-23 09:00 A cold and stormy day."
Scenario: Writing an empty entry from the editor
Given we use the config "editor.yaml"
When we open the editor and enter ""
Then we should see the message "[Nothing saved to file]"
Scenario: Writing an empty entry from the command line
Given we use the config "basic.yaml"
When we run "jrnl" and enter ""
Then the output should be
"""
"""
Scenario: Filtering for dates Scenario: Filtering for dates
Given we use the config "basic.yaml" Given we use the config "basic.yaml"
When we run "jrnl -on 2013-06-10 --short" When we run "jrnl -on 2013-06-10 --short"

View file

@ -0,0 +1,35 @@
Feature: Reading and writing to journal with custom date formats
Scenario: Loading a sample journal
Given we use the config "little_endian_dates.yaml"
When we run "jrnl -n 2"
Then we should get no error
And the output should be
"""
09.06.2013 15:39 My first entry.
| Everything is alright
10.06.2013 15:40 Life is good.
| But I'm better.
"""
Scenario: Writing an entry from command line
Given we use the config "little_endian_dates.yaml"
When we run "jrnl 2013-07-12: A cold and stormy day. I ate crisps on the sofa."
Then we should see the message "Entry added"
When we run "jrnl -n 1"
Then the output should contain "12.07.2013 09:00 A cold and stormy day."
Scenario: Filtering for dates
Given we use the config "little_endian_dates.yaml"
When we run "jrnl -on 2013-06-10 --short"
Then the output should be "10.06.2013 15:40 Life is good."
When we run "jrnl -on 'june 6 2013' --short"
Then the output should be "10.06.2013 15:40 Life is good."
Scenario: Writing an entry at the prompt
Given we use the config "little_endian_dates.yaml"
When we run "jrnl" and enter "2013-05-10: I saw Elvis. He's alive."
Then we should get no error
And the journal should contain "[10.05.2013 09:00] I saw Elvis."
And the journal should contain "He's alive."

View file

@ -0,0 +1,12 @@
default_hour: 9
default_minute: 0
editor: "vim"
encrypt: false
highlight: true
journals:
default: features/journals/simple.journal
linewrap: 80
tagsymbols: "@"
template: false
timeformat: "%Y-%m-%d %H:%M"
indent_character: "|"

View file

@ -0,0 +1,12 @@
default_hour: 9
default_minute: 0
editor: ""
encrypt: false
highlight: true
journals:
default: features/journals/little_endian_dates.journal
linewrap: 80
tagsymbols: "@"
template: false
timeformat: "%d.%m.%Y %H:%M"
indent_character: "|"

View file

@ -0,0 +1,11 @@
{
"default_hour": 9,
"timeformat": "%d.%m.%Y %H:%M",
"linewrap": 80,
"encrypt": false,
"editor": "",
"default_minute": 0,
"highlight": true,
"journals": {"default": "features/journals/simple_jrnl-1-9-5_little_endian_dates.journal"},
"tagsymbols": "@"
}

View file

@ -0,0 +1,5 @@
[09.06.2013 15:39] My first entry.
Everything is alright
[10.06.2013 15:40] Life is good.
But I'm better.

View file

@ -0,0 +1,13 @@
10.06.2010 15:00 A life without chocolate is like a bad analogy.
10.06.2013 15:40 He said "[this] is the best time to be alive".
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent malesuada
quis est ac dignissim. Aliquam dignissim rutrum pretium. Phasellus pellentesque
augue et venenatis facilisis.
[03.08.2019 12:55] Some chat log or something
Suspendisse potenti. Sed dignissim sed nisl eu consequat. Aenean ante ex,
elementum ut interdum et, mattis eget lacus. In commodo nulla nec tellus
placerat, sed ultricies metus bibendum. Duis eget venenatis erat. In at dolor
dui.

76
features/dayone.feature Normal file
View file

@ -0,0 +1,76 @@
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'"
Then we should get no error
and the output should be
"""
2013-05-17 11:39 This entry has tags!
2013-06-17 20:38 This entry has a location.
2013-07-17 11:38 This entry is starred!
"""
# fails when system time is UTC (as on Travis-CI)
@skip
Scenario: Entries without timezone information will be interpreted as in the current timezone
Given we use the config "dayone.yaml"
When we run "jrnl -until 'feb 2013'"
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."
and we run "jrnl -until 1980"
Then the output should be
"""
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"
Then the output should be
"""
@work : 1
@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"
and we run "jrnl --tags"
Then the output should be
"""
@work : 2
@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"
Then the output should be
"""
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"
Then we should get no error
and the output should be parsable as json
and the json output should contain entries.0.uuid = "4BB1F46946AD439996C9B59DE7C4DDC1"

View file

@ -0,0 +1,31 @@
Feature: Zapped Dayone bugs stay dead!
# fails when system time is UTC (as on Travis-CI)
@skip
Scenario: DayOne tag searching should work with tags containing a mixture of upper and lower case.
# https://github.com/jrnl-org/jrnl/issues/354
Given we use the config "dayone.yaml"
When we run "jrnl @plAy"
Then the output should contain
"""
2013-05-17 11:39 This entry has tags!
"""
# 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: Opening an folder that's not a DayOne folder gives a nice error message
Given we use the config "empty_folder.yaml"
When we run "jrnl Herro"
Then we should get an error
Then we should see the message "is a directory, but doesn't seem to be a DayOne journal either"

View file

@ -2,30 +2,56 @@
Scenario: Loading an encrypted journal Scenario: Loading an encrypted journal
Given we use the config "encrypted.yaml" Given we use the config "encrypted.yaml"
When we run "jrnl -n 1" and enter "bad doggie no biscuit" When we run "jrnl -n 1" and enter "bad doggie no biscuit"
Then we should see the message "Password" 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 Scenario: Decrypting a journal
Given we use the config "encrypted.yaml" Given we use the config "encrypted.yaml"
When we run "jrnl --decrypt" and enter "bad doggie no biscuit" 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 the config for journal "default" should have "encrypt" set to "bool:False"
Then we should see the message "Journal decrypted" 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 Scenario: Encrypting a journal
Given we use the config "basic.yaml" Given we use the config "basic.yaml"
When we run "jrnl --encrypt" and enter "swordfish" When we run "jrnl --encrypt" and enter
"""
swordfish
swordfish
n
"""
Then we should see the message "Journal encrypted" 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" When we run "jrnl -n 1" and enter "swordfish"
Then we should see the message "Password" 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 Scenario: Storing a password in Keychain
Given we use the config "multiple.yaml" Given we use the config "multiple.yaml"
When we run "jrnl simple --encrypt" and enter "sabertooth" When we run "jrnl simple --encrypt" and enter
"""
sabertooth
sabertooth
y
"""
When we set the keychain password of "simple" to "sabertooth" When we set the keychain password of "simple" to "sabertooth"
Then the config for journal "simple" should have "encrypt" set to "bool:True" Then the config for journal "simple" should have "encrypt" set to "bool:True"
When we run "jrnl simple -n 1" When we run "jrnl simple -n 1"
Then we should not see the message "Password" Then 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"

View file

@ -1,25 +1,23 @@
from behave import *
import shutil import shutil
import os import os
import jrnl
try:
from io import StringIO def before_feature(context, feature):
except ImportError: # add "skip" tag
from cStringIO import StringIO # https://stackoverflow.com/a/42721605/4276230
if "skip" in feature.tags:
feature.skip("Marked with @skip")
return
def before_scenario(context, scenario): def before_scenario(context, scenario):
"""Before each scenario, backup all config and journal test data.""" """Before each scenario, backup all config and journal test data."""
context.messages = StringIO()
jrnl.util.STDERR = context.messages
jrnl.util.TEST = True
# Clean up in case something went wrong # Clean up in case something went wrong
for folder in ("configs", "journals"): for folder in ("configs", "journals"):
working_dir = os.path.join("features", folder) working_dir = os.path.join("features", folder)
if os.path.exists(working_dir): if os.path.exists(working_dir):
shutil.rmtree(working_dir) shutil.rmtree(working_dir)
for folder in ("configs", "journals"): for folder in ("configs", "journals"):
original = os.path.join("features", "data", folder) original = os.path.join("features", "data", folder)
working_dir = os.path.join("features", folder) working_dir = os.path.join("features", folder)
@ -32,10 +30,15 @@ def before_scenario(context, scenario):
else: else:
shutil.copy2(source, working_dir) shutil.copy2(source, working_dir)
# add "skip" tag
# https://stackoverflow.com/a/42721605/4276230
if "skip" in scenario.effective_tags:
scenario.skip("Marked with @skip")
return
def after_scenario(context, scenario): def after_scenario(context, scenario):
"""After each scenario, restore all test data and remove working_dirs.""" """After each scenario, restore all test data and remove working_dirs."""
context.messages.close()
context.messages = None
for folder in ("configs", "journals"): for folder in ("configs", "journals"):
working_dir = os.path.join("features", folder) working_dir = os.path.join("features", folder)
if os.path.exists(working_dir): if os.path.exists(working_dir):

View file

@ -42,5 +42,10 @@ Feature: Multiple journals
Scenario: Don't crash if no file exists for a configured encrypted journal Scenario: Don't crash if no file exists for a configured encrypted journal
Given we use the config "multiple.yaml" Given we use the config "multiple.yaml"
When we run "jrnl new_encrypted Adding first entry" and enter "these three eyes" When we run "jrnl new_encrypted Adding first entry" and enter
"""
these three eyes
these three eyes
n
"""
Then we should see the message "Journal 'new_encrypted' created" Then we should see the message "Journal 'new_encrypted' created"

View file

@ -62,3 +62,57 @@ Feature: Zapped bugs should stay dead.
Then the output should contain "I'm going to activate the machine." Then the output should contain "I'm going to activate the machine."
Then the output should contain "I've crossed so many timelines. Is there any going back?" Then the output should contain "I've crossed so many timelines. Is there any going back?"
Scenario: Viewing today's entries does not print the entire journal
# https://github.com/jrnl-org/jrnl/issues/741
Given we use the config "basic.yaml"
When we run "jrnl -on today"
Then the output should not contain "Life is good"
Then the output should not contain "But I'm better."
Scenario: Create entry using day of the week as entry date.
Given we use the config "basic.yaml"
When we run "jrnl monday: This is an entry on a Monday."
Then we should see the message "Entry added"
When we run "jrnl -1"
Then the output should contain "monday at 9am" in the local time
Then the output should contain "This is an entry on a Monday."
Scenario: Create entry using day of the week abbreviations as entry date.
Given we use the config "basic.yaml"
When we run "jrnl fri: This is an entry on a Friday."
Then we should see the message "Entry added"
When we run "jrnl -1"
Then the output should contain "friday at 9am" in the local time
Scenario: Displaying entries using -on today should display entries created today.
Given we use the config "basic.yaml"
When we run "jrnl today: Adding an entry right now."
Then we should see the message "Entry added"
When we run "jrnl -on today"
Then the output should contain "Adding an entry right now."
Scenario: Displaying entries using -from day should display correct entries
Given we use the config "basic.yaml"
When we run "jrnl yesterday: This thing happened yesterday"
Then we should see the message "Entry added"
When we run "jrnl today at 11:59pm: Adding an entry right now."
Then we should see the message "Entry added"
When we run "jrnl tomorrow: A future entry."
Then we should see the message "Entry added"
When we run "jrnl -from today"
Then the output should contain "Adding an entry right now."
Then the output should contain "A future entry."
Then the output should not contain "This thing happened yesterday"
Scenario: Displaying entries using -from and -to day should display correct entries
Given we use the config "basic.yaml"
When we run "jrnl yesterday: This thing happened yesterday"
Then we should see the message "Entry added"
When we run "jrnl today at 11:59pm: Adding an entry right now."
Then we should see the message "Entry added"
When we run "jrnl tomorrow: A future entry."
Then we should see the message "Entry added"
When we run "jrnl -from yesterday -to today"
Then the output should contain "This thing happened yesterday"
Then the output should contain "Adding an entry right now."
Then the output should not contain "A future entry."

View file

@ -1,19 +1,28 @@
from __future__ import unicode_literals from unittest.mock import patch
from __future__ import absolute_import
from behave import given, when, then from behave import given, when, then
from jrnl import cli, install, Journal, util, plugins from jrnl import cli, install, Journal, util, plugins
from jrnl import __version__ from jrnl import __version__
from dateutil import parser as date_parser from dateutil import parser as date_parser
from collections import defaultdict from collections import defaultdict
try: import parsedatetime.parsedatetime_consts as pdt
except ImportError: import parsedatetime as pdt
import time
import os import os
import json import json
import yaml import yaml
import keyring import keyring
import tzlocal
import shlex
import sys
consts = pdt.Constants(usePyICU=False)
consts.DOWParseStyle = -1 # Prefers past weekdays
CALENDAR = pdt.Calendar(consts)
class TestKeyring(keyring.backend.KeyringBackend): class TestKeyring(keyring.backend.KeyringBackend):
"""A test keyring that just stores its valies in a hash""" """A test keyring that just stores its values in a hash"""
priority = 1 priority = 1
keys = defaultdict(dict) keys = defaultdict(dict)
@ -24,22 +33,14 @@ class TestKeyring(keyring.backend.KeyringBackend):
def get_password(self, servicename, username): def get_password(self, servicename, username):
return self.keys[servicename].get(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 self.keys[servicename][username] = None
# set the keyring for keyring lib # set the keyring for keyring lib
keyring.set_keyring(TestKeyring()) keyring.set_keyring(TestKeyring())
try:
from io import StringIO
except ImportError:
from cStringIO import StringIO
import tzlocal
import shlex
import sys
def ushlex(command): def ushlex(command):
if sys.version_info[0] == 3: if sys.version_info[0] == 3:
return shlex.split(command) return shlex.split(command)
@ -73,18 +74,69 @@ def set_config(context, config_file):
cf.write("version: {}".format(__version__)) 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)
def _mock_editor_function(command):
tmpfile = command[-1]
with open(tmpfile, "w+") as f:
if text is not None:
f.write(text)
else:
f.write("")
return tmpfile
with patch('subprocess.call', side_effect=_mock_editor_function):
run(context, "jrnl")
def _mock_getpass(inputs):
def prompt_return(prompt="Password: "):
print(prompt)
return next(inputs)
return prompt_return
def _mock_input(inputs):
def prompt_return(prompt=""):
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 ""')
@when('we run "{command}" and enter "{inputs}"') @when('we run "{command}" and enter "{inputs}"')
def run_with_input(context, command, inputs=None): def run_with_input(context, command, inputs=""):
text = inputs or context.text # 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 context.text:
text = iter(context.text.split("\n"))
else:
text = iter([inputs])
args = ushlex(command)[1:] args = ushlex(command)[1:]
buffer = StringIO(text.strip()) with patch("builtins.input", side_effect=_mock_input(text)) as mock_input,\
util.STDIN = buffer patch("getpass.getpass", side_effect=_mock_getpass(text)) as mock_getpass,\
try: patch("sys.stdin.read", side_effect=text) as mock_read:
cli.run(args or []) try:
context.exit_status = 0 cli.run(args or [])
except SystemExit as e: context.exit_status = 0
context.exit_status = e.code 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
@when('we run "{command}"') @when('we run "{command}"')
@ -116,7 +168,7 @@ def has_error(context):
@then('we should get no error') @then('we should get no error')
def no_error(context): 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') @then('the output should be parsable as json')
@ -180,9 +232,9 @@ def check_output(context, text=None):
def check_output_time_inline(context, text): def check_output_time_inline(context, text):
out = context.stdout_capture.getvalue() out = context.stdout_capture.getvalue()
local_tz = tzlocal.get_localzone() local_tz = tzlocal.get_localzone()
utc_time = date_parser.parse(text) date, flag = CALENDAR.parse(text)
local_date = utc_time.astimezone(local_tz).strftime("%Y-%m-%d %H:%M") output_date = time.strftime("%Y-%m-%d %H:%M",date)
assert local_date in out, local_date assert output_date in out, output_date
@then('the output should contain') @then('the output should contain')
@ -190,28 +242,24 @@ def check_output_time_inline(context, text):
def check_output_inline(context, text=None): def check_output_inline(context, text=None):
text = text or context.text text = text or context.text
out = context.stdout_capture.getvalue() out = context.stdout_capture.getvalue()
if isinstance(out, bytes):
out = out.decode('utf-8')
assert text in out, text assert text in out, text
@then('the output should not contain "{text}"') @then('the output should not contain "{text}"')
def check_output_not_inline(context, text): def check_output_not_inline(context, text):
out = context.stdout_capture.getvalue() out = context.stdout_capture.getvalue()
if isinstance(out, bytes):
out = out.decode('utf-8')
assert text not in out assert text not in out
@then('we should see the message "{text}"') @then('we should see the message "{text}"')
def check_message(context, text): def check_message(context, text):
out = context.messages.getvalue() out = context.stderr_capture.getvalue()
assert text in out, [text, out] assert text in out, [text, out]
@then('we should not see the message "{text}"') @then('we should not see the message "{text}"')
def check_not_message(context, text): def check_not_message(context, text):
out = context.messages.getvalue() out = context.stderr_capture.getvalue()
assert text not in out, [text, out] assert text not in out, [text, out]

View file

@ -13,11 +13,22 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x
Scenario: Upgrading a journal encrypted with jrnl 1.x Scenario: Upgrading a journal encrypted with jrnl 1.x
Given we use the config "encrypted_old.json" Given we use the config "encrypted_old.json"
When we run "jrnl -n 1" and enter When we run "jrnl -n 1" and enter
""" """
Y Y
bad doggie no biscuit bad doggie no biscuit
bad doggie no biscuit bad doggie no biscuit
""" """
Then we should see the message "Password" 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: Upgrade and parse journals with little endian date format
Given we use the config "upgrade_from_195_little_endian_dates.json"
When we run "jrnl -9" and enter "Y"
Then the output should contain
"""
10.06.2010 15:00 A life without chocolate is like a bad analogy.
10.06.2013 15:40 He said "[this] is the best time to be alive".
"""
Then the journal should have 2 entries

View file

@ -1,7 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from . import Entry from . import Entry
from . import Journal from . import Journal
from . import time as jrnl_time from . import time as jrnl_time
@ -26,7 +24,7 @@ class DayOne(Journal.Journal):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.entries = [] self.entries = []
self._deleted_entries = [] self._deleted_entries = []
super(DayOne, self).__init__(**kwargs) super().__init__(**kwargs)
def open(self): 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"))]
@ -83,7 +81,7 @@ class DayOne(Journal.Journal):
def editable_str(self): def editable_str(self):
"""Turns the journal into a string of entries that can be edited """Turns the journal into a string of entries that can be edited
manually and later be parsed with eslf.parse_editable_str.""" manually and later be parsed with eslf.parse_editable_str."""
return "\n".join(["# {0}\n{1}".format(e.uuid, e.__unicode__()) for e in self.entries]) return "\n".join([f"# {e.uuid}\n{str(e)}" for e in self.entries])
def parse_editable_str(self, edited): def parse_editable_str(self, edited):
"""Parses the output of self.editable_str and updates its entries.""" """Parses the output of self.editable_str and updates its entries."""
@ -107,7 +105,7 @@ class DayOne(Journal.Journal):
current_entry.modified = False current_entry.modified = False
current_entry.uuid = m.group(1).lower() current_entry.uuid = m.group(1).lower()
else: else:
date_blob_re = re.compile("^\[[^\\]]+\] ") date_blob_re = re.compile("^\\[[^\\]]+\\] ")
date_blob = date_blob_re.findall(line) date_blob = date_blob_re.findall(line)
if date_blob: if date_blob:
date_blob = date_blob[0] date_blob = date_blob[0]

View file

@ -8,14 +8,13 @@ from cryptography.hazmat.backends import default_backend
import sys import sys
import os import os
import base64 import base64
import getpass
import logging import logging
log = logging.getLogger() log = logging.getLogger()
def make_key(password): def make_key(password):
password = util.bytes(password) password = password.encode("utf-8")
kdf = PBKDF2HMAC( kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(), algorithm=hashes.SHA256(),
length=32, length=32,
@ -30,7 +29,7 @@ def make_key(password):
class EncryptedJournal(Journal.Journal): class EncryptedJournal(Journal.Journal):
def __init__(self, name='default', **kwargs): def __init__(self, name='default', **kwargs):
super(EncryptedJournal, self).__init__(name, **kwargs) super().__init__(name, **kwargs)
self.config['encrypt'] = True self.config['encrypt'] = True
def open(self, filename=None): def open(self, filename=None):
@ -39,7 +38,7 @@ class EncryptedJournal(Journal.Journal):
filename = filename or self.config['journal'] filename = filename or self.config['journal']
if not os.path.exists(filename): if not os.path.exists(filename):
password = util.getpass("Enter password for new journal: ") password = util.create_password()
if password: if password:
if util.yesno("Do you want to store the password in your keychain?", default=True): if util.yesno("Do you want to store the password in your keychain?", default=True):
util.set_keychain(self.name, password) util.set_keychain(self.name, password)
@ -48,9 +47,9 @@ class EncryptedJournal(Journal.Journal):
self.config['password'] = password self.config['password'] = password
text = "" text = ""
self._store(filename, text) self._store(filename, text)
util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename)) print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr)
else: else:
util.prompt("No password supplied for encrypted journal") print("No password supplied for encrypted journal", file=sys.stderr)
sys.exit(1) sys.exit(1)
else: else:
text = self._load(filename) text = self._load(filename)
@ -59,7 +58,6 @@ class EncryptedJournal(Journal.Journal):
log.debug("opened %s with %d entries", self.__class__.__name__, len(self)) log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
return self return self
def _load(self, filename, password=None): def _load(self, filename, password=None):
"""Loads an encrypted journal from a file and tries to decrypt it. """Loads an encrypted journal from a file and tries to decrypt it.
If password is not provided, will look for password in the keychain If password is not provided, will look for password in the keychain
@ -99,7 +97,7 @@ class LegacyEncryptedJournal(Journal.LegacyJournal):
"""Legacy class to support opening journals encrypted with the jrnl 1.x """Legacy class to support opening journals encrypted with the jrnl 1.x
standard. You'll not be able to save these journals anymore.""" standard. You'll not be able to save these journals anymore."""
def __init__(self, name='default', **kwargs): def __init__(self, name='default', **kwargs):
super(LegacyEncryptedJournal, self).__init__(name, **kwargs) super().__init__(name, **kwargs)
self.config['encrypt'] = True self.config['encrypt'] = True
def _load(self, filename, password=None): def _load(self, filename, password=None):

View file

@ -1,7 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8
from __future__ import unicode_literals
import re import re
import textwrap import textwrap
from datetime import datetime from datetime import datetime
@ -51,14 +49,14 @@ class Entry:
@staticmethod @staticmethod
def tag_regex(tagsymbols): def tag_regex(tagsymbols):
pattern = r'(?u)(?:^|\s)([{tags}][-+*#/\w]+)'.format(tags=tagsymbols) pattern = fr'(?u)(?:^|\s)([{tagsymbols}][-+*#/\w]+)'
return re.compile(pattern, re.UNICODE) return re.compile(pattern)
def _parse_tags(self): def _parse_tags(self):
tagsymbols = self.journal.config['tagsymbols'] tagsymbols = self.journal.config['tagsymbols']
return set(tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text)) return {tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text)}
def __unicode__(self): def __str__(self):
"""Returns a string representation of the entry to be written into a journal file.""" """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 ")) title = "[{}] {}".format(date_str, self.title.rstrip("\n "))
@ -106,7 +104,7 @@ class Entry:
) )
def __repr__(self): def __repr__(self):
return "<Entry '{0}' on {1}>".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): def __hash__(self):
return hash(self.__repr__()) return hash(self.__repr__())

View file

@ -1,13 +1,10 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from . import Entry from . import Entry
from . import util from . import util
from . import time from . import time
import os import os
import sys import sys
import codecs
import re import re
from datetime import datetime from datetime import datetime
import logging import logging
@ -15,7 +12,7 @@ import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Tag(object): class Tag:
def __init__(self, name, count=0): def __init__(self, name, count=0):
self.name = name self.name = name
self.count = count self.count = count
@ -24,10 +21,10 @@ class Tag(object):
return self.name return self.name
def __repr__(self): def __repr__(self):
return "<Tag '{}'>".format(self.name) return f"<Tag '{self.name}'>"
class Journal(object): class Journal:
def __init__(self, name='default', **kwargs): def __init__(self, name='default', **kwargs):
self.config = { self.config = {
'journal': "journal.txt", 'journal': "journal.txt",
@ -72,7 +69,7 @@ class Journal(object):
filename = filename or self.config['journal'] filename = filename or self.config['journal']
if not os.path.exists(filename): if not os.path.exists(filename):
util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename)) print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr)
self._create(filename) self._create(filename)
text = self._load(filename) text = self._load(filename)
@ -96,7 +93,7 @@ class Journal(object):
return True return True
def _to_text(self): def _to_text(self):
return "\n".join([e.__unicode__() for e in self.entries]) return "\n".join([str(e) for e in self.entries])
def _load(self, filename): def _load(self, filename):
raise NotImplementedError raise NotImplementedError
@ -118,11 +115,16 @@ class Journal(object):
# Initialise our current entry # Initialise our current entry
entries = [] entries = []
date_blob_re = re.compile("(?:^|\n)\[([^\\]]+)\] ") date_blob_re = re.compile("(?:^|\n)\\[([^\\]]+)\\] ")
last_entry_pos = 0 last_entry_pos = 0
for match in date_blob_re.finditer(journal_txt): for match in date_blob_re.finditer(journal_txt):
date_blob = match.groups()[0] date_blob = match.groups()[0]
new_date = time.parse(date_blob) try:
new_date = datetime.strptime(date_blob, self.config["timeformat"])
except ValueError:
# Passing in a date that had brackets around it
new_date = time.parse(date_blob, bracketed=True)
if new_date: if new_date:
if entries: if entries:
entries[-1].text = journal_txt[last_entry_pos:match.start()] entries[-1].text = journal_txt[last_entry_pos:match.start()]
@ -140,9 +142,6 @@ class Journal(object):
entry._parse_text() entry._parse_text()
return entries return entries
def __unicode__(self):
return self.pprint()
def pprint(self, short=False): def pprint(self, short=False):
"""Prettyprints the journal's entries""" """Prettyprints the journal's entries"""
sep = "\n" sep = "\n"
@ -153,7 +152,7 @@ class Journal(object):
tagre = re.compile(re.escape(tag), re.IGNORECASE) tagre = re.compile(re.escape(tag), re.IGNORECASE)
pp = re.sub(tagre, pp = re.sub(tagre,
lambda match: util.colorize(match.group(0)), lambda match: util.colorize(match.group(0)),
pp, re.UNICODE) pp)
else: else:
pp = re.sub( pp = re.sub(
Entry.Entry.tag_regex(self.config['tagsymbols']), Entry.Entry.tag_regex(self.config['tagsymbols']),
@ -162,8 +161,11 @@ class Journal(object):
) )
return pp return pp
def __str__(self):
return self.pprint()
def __repr__(self): def __repr__(self):
return "<Journal with {0} entries>".format(len(self.entries)) return f"<Journal with {len(self.entries)} entries>"
def sort(self): def sort(self):
"""Sorts the Journal's entries by date""" """Sorts the Journal's entries by date"""
@ -183,10 +185,10 @@ class Journal(object):
for entry in self.entries for entry in self.entries
for tag in set(entry.tags)] for tag in set(entry.tags)]
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag] # To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
tag_counts = set([(tags.count(tag), tag) for tag in tags]) tag_counts = {(tags.count(tag), tag) for tag in tags}
return [Tag(tag, count=count) for count, tag in sorted(tag_counts)] 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. """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 tags is a list of tags, each being a string that starts with one of the
@ -200,14 +202,17 @@ class Journal(object):
exclude is a list of the tags which should not appear in the results. exclude is a list of the tags which should not appear in the results.
entry is kept if any tag is present, unless they appear in exclude.""" entry is kept if any tag is present, unless they appear in exclude."""
self.search_tags = set([tag.lower() for tag in tags]) self.search_tags = {tag.lower() for tag in tags}
excluded_tags = set([tag.lower() for tag in exclude]) excluded_tags = {tag.lower() for tag in exclude}
end_date = time.parse(end_date, inclusive=True) end_date = time.parse(end_date, inclusive=True)
start_date = time.parse(start_date) start_date = time.parse(start_date)
# If strict mode is on, all tags have to be present in entry # 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 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 excluded = lambda tags: len([tag for tag in tags if tag in excluded_tags]) > 0
if contains:
contains_lower = contains.casefold()
result = [ result = [
entry for entry in self.entries entry for entry in self.entries
if (not tags or tagged(entry.tags)) if (not tags or tagged(entry.tags))
@ -215,6 +220,7 @@ class Journal(object):
and (not start_date or entry.date >= start_date) and (not start_date or entry.date >= start_date)
and (not end_date or entry.date <= end_date) and (not end_date or entry.date <= end_date)
and (not exclude or not excluded(entry.tags)) 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 self.entries = result
@ -226,7 +232,7 @@ class Journal(object):
raw = raw.replace('\\n ', '\n').replace('\\n', '\n') raw = raw.replace('\\n ', '\n').replace('\\n', '\n')
starred = False starred = False
# Split raw text into title and body # Split raw text into title and body
sep = re.search("\n|[\?!.]+ +\n?", raw) 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 starred = False
@ -254,7 +260,7 @@ class Journal(object):
def editable_str(self): def editable_str(self):
"""Turns the journal into a string of entries that can be edited """Turns the journal into a string of entries that can be edited
manually and later be parsed with eslf.parse_editable_str.""" manually and later be parsed with eslf.parse_editable_str."""
return "\n".join([e.__unicode__() for e in self.entries]) return "\n".join([str(e) for e in self.entries])
def parse_editable_str(self, edited): def parse_editable_str(self, edited):
"""Parses the output of self.editable_str and updates it's entries.""" """Parses the output of self.editable_str and updates it's entries."""
@ -270,15 +276,15 @@ class Journal(object):
class PlainJournal(Journal): class PlainJournal(Journal):
@classmethod @classmethod
def _create(cls, filename): def _create(cls, filename):
with codecs.open(filename, "a", "utf-8"): with open(filename, "a", encoding="utf-8"):
pass pass
def _load(self, filename): def _load(self, filename):
with codecs.open(filename, "r", "utf-8") as f: with open(filename, "r", encoding="utf-8") as f:
return f.read() return f.read()
def _store(self, filename, text): def _store(self, filename, text):
with codecs.open(filename, 'w', "utf-8") as f: with open(filename, 'w', encoding="utf-8") as f:
f.write(text) f.write(text)
@ -287,7 +293,7 @@ class LegacyJournal(Journal):
standard. Main difference here is that in 1.x, timestamps were not cuddled 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.""" by square brackets. You'll not be able to save these journals anymore."""
def _load(self, filename): def _load(self, filename):
with codecs.open(filename, "r", "utf-8") as f: with open(filename, "r", encoding="utf-8") as f:
return f.read() return f.read()
def _parse(self, journal_txt): def _parse(self, journal_txt):
@ -323,7 +329,7 @@ class LegacyJournal(Journal):
# escaping for the new format). # 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: if current_entry:
current_entry.text += line + u"\n" current_entry.text += line + "\n"
# Append last entry # Append last entry
if current_entry: if current_entry:
@ -347,8 +353,9 @@ def open_journal(name, config, legacy=False):
from . import DayOneJournal from . import DayOneJournal
return DayOneJournal.DayOne(**config).open() return DayOneJournal.DayOne(**config).open()
else: else:
util.prompt( print(
u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal']) f"[Error: {config['journal']} is a directory, but doesn't seem to be a DayOne journal either.",
file=sys.stderr
) )
sys.exit(1) sys.exit(1)

View file

@ -1,5 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8
import os import os
try: try:

View file

@ -1,6 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from . import cli from . import cli

View file

@ -1,5 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8
""" """
jrnl jrnl
@ -7,8 +6,6 @@
license: MIT, see LICENSE for more details. license: MIT, see LICENSE for more details.
""" """
from __future__ import unicode_literals
from __future__ import absolute_import
from . import Journal from . import Journal
from . import util from . import util
from . import install from . import install
@ -35,6 +32,7 @@ def parse_args(args=None):
reading = parser.add_argument_group('Reading', 'Specifying either of these parameters will display posts of your journal') 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('-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('-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('-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('-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('-starred', dest='starred', action="store_true", help='Show only starred entries')
@ -67,7 +65,7 @@ def guess_mode(args, config):
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)): 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)):
compose = False compose = False
export = True 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? # Any sign of displaying stuff?
compose = False 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()):
@ -81,7 +79,7 @@ def encrypt(journal, filename=None):
""" Encrypt into new file. If filename is not set, we encrypt the journal file itself. """ """ Encrypt into new file. If filename is not set, we encrypt the journal file itself. """
from . import EncryptedJournal from . import EncryptedJournal
journal.config['password'] = util.getpass("Enter new password: ") journal.config['password'] = util.create_password()
journal.config['encrypt'] = True journal.config['encrypt'] = True
new_journal = EncryptedJournal.EncryptedJournal(None, **journal.config) new_journal = EncryptedJournal.EncryptedJournal(None, **journal.config)
@ -91,7 +89,7 @@ def encrypt(journal, filename=None):
if util.yesno("Do you want to store the password in your keychain?", default=True): if util.yesno("Do you want to store the password in your keychain?", default=True):
util.set_keychain(journal.name, journal.config['password']) util.set_keychain(journal.name, journal.config['password'])
util.prompt("Journal encrypted to {0}.".format(filename or new_journal.config['journal'])) print("Journal encrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr)
def decrypt(journal, filename=None): def decrypt(journal, filename=None):
@ -102,12 +100,12 @@ def decrypt(journal, filename=None):
new_journal = Journal.PlainJournal(filename, **journal.config) new_journal = Journal.PlainJournal(filename, **journal.config)
new_journal.entries = journal.entries new_journal.entries = journal.entries
new_journal.write(filename) new_journal.write(filename)
util.prompt("Journal decrypted to {0}.".format(filename or new_journal.config['journal'])) print("Journal decrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr)
def list_journals(config): def list_journals(config):
"""List the journals specified in the configuration file""" """List the journals specified in the configuration file"""
result = "Journals defined in {}\n".format(install.CONFIG_FILE_PATH) result = f"Journals defined in {install.CONFIG_FILE_PATH}\n"
ml = min(max(len(k) for k in config['journals']), 20) ml = min(max(len(k) for k in config['journals']), 20)
for journal, cfg in config['journals'].items(): for journal, cfg in config['journals'].items():
result += " * {:{}} -> {}\n".format(journal, ml, cfg['journal'] if isinstance(cfg, dict) else cfg) result += " * {:{}} -> {}\n".format(journal, ml, cfg['journal'] if isinstance(cfg, dict) else cfg)
@ -138,20 +136,19 @@ def configure_logger(debug=False):
def run(manual_args=None): def run(manual_args=None):
args = parse_args(manual_args) args = parse_args(manual_args)
configure_logger(args.debug) configure_logger(args.debug)
args.text = [p.decode('utf-8') if util.PY2 and not isinstance(p, unicode) else p for p in args.text]
if args.version: if args.version:
version_str = "{0} version {1}".format(jrnl.__title__, jrnl.__version__) version_str = f"{jrnl.__title__} version {jrnl.__version__}"
print(util.py2encode(version_str)) print(version_str)
sys.exit(0) sys.exit(0)
try: try:
config = install.load_or_install_jrnl() config = install.load_or_install_jrnl()
except UserAbort as err: except UserAbort as err:
util.prompt("\n{}".format(err)) print(f"\n{err}", file=sys.stderr)
sys.exit(1) sys.exit(1)
if args.ls: if args.ls:
util.prnt(list_journals(config)) print(list_journals(config))
sys.exit(0) sys.exit(0)
log.debug('Using configuration "%s"', config) log.debug('Using configuration "%s"', config)
@ -161,11 +158,11 @@ def run(manual_args=None):
# use this! # use this!
journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default' journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default'
if journal_name is not 'default': if journal_name != 'default':
args.text = args.text[1:] args.text = args.text[1:]
elif "default" not in config['journals']: elif "default" not in config['journals']:
util.prompt("No default journal configured.") print("No default journal configured.", file=sys.stderr)
util.prompt(list_journals(config)) print(list_journals(config), file=sys.stderr)
sys.exit(1) sys.exit(1)
config = util.scope_config(config, journal_name) config = util.scope_config(config, journal_name)
@ -175,7 +172,7 @@ def run(manual_args=None):
try: try:
args.limit = int(args.text[0].lstrip("-")) args.limit = int(args.text[0].lstrip("-"))
args.text = args.text[1:] args.text = args.text[1:]
except: except ValueError:
pass pass
log.debug('Using journal "%s"', journal_name) log.debug('Using journal "%s"', journal_name)
@ -190,32 +187,33 @@ def run(manual_args=None):
if mode_compose and not args.text: if mode_compose and not args.text:
if not sys.stdin.isatty(): if not sys.stdin.isatty():
# Piping data into jrnl # Piping data into jrnl
raw = util.py23_read() raw = sys.stdin.read()
elif config['editor']: elif config['editor']:
template = "" template = ""
if config['template']: if config['template']:
try: try:
template = open(config['template']).read() template = open(config['template']).read()
except: except OSError:
util.prompt("[Could not read template at '']".format(config['template'])) print(f"[Could not read template at '{config['template']}']", file=sys.stderr)
sys.exit(1) sys.exit(1)
raw = util.get_text_from_editor(config, template) raw = util.get_text_from_editor(config, template)
else: else:
try: try:
raw = util.py23_read("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n") print("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n", file=sys.stderr)
raw = sys.stdin.read()
except KeyboardInterrupt: except KeyboardInterrupt:
util.prompt("[Entry NOT saved to journal.]") print("[Entry NOT saved to journal.]", file=sys.stderr)
sys.exit(0) sys.exit(0)
if raw: if raw:
args.text = [raw] args.text = [raw]
else: else:
mode_compose = False sys.exit()
# This is where we finally open the journal! # This is where we finally open the journal!
try: try:
journal = Journal.open_journal(journal_name, config) journal = Journal.open_journal(journal_name, config)
except KeyboardInterrupt: except KeyboardInterrupt:
util.prompt("[Interrupted while opening journal]".format(journal_name)) print(f"[Interrupted while opening journal]", file=sys.stderr)
sys.exit(1) sys.exit(1)
# Import mode # Import mode
@ -225,11 +223,9 @@ def run(manual_args=None):
# Writing mode # Writing mode
elif mode_compose: elif mode_compose:
raw = " ".join(args.text).strip() raw = " ".join(args.text).strip()
if util.PY2 and type(raw) is not unicode:
raw = raw.decode(sys.getfilesystemencoding())
log.debug('Appending raw line "%s" to journal "%s"', raw, journal_name) log.debug('Appending raw line "%s" to journal "%s"', raw, journal_name)
journal.new_entry(raw) journal.new_entry(raw)
util.prompt("[Entry added to {0} journal]".format(journal_name)) print(f"[Entry added to {journal_name} journal]", file=sys.stderr)
journal.write() journal.write()
if not mode_compose: if not mode_compose:
@ -241,19 +237,20 @@ def run(manual_args=None):
strict=args.strict, strict=args.strict,
short=args.short, short=args.short,
starred=args.starred, starred=args.starred,
exclude=args.excluded) exclude=args.excluded,
contains=args.contains)
journal.limit(args.limit) journal.limit(args.limit)
# Reading mode # Reading mode
if not mode_compose and not mode_export and not mode_import: if not mode_compose and not mode_export and not mode_import:
print(util.py2encode(journal.pprint())) print(journal.pprint())
# Various export modes # Various export modes
elif args.short: elif args.short:
print(util.py2encode(journal.pprint(short=True))) print(journal.pprint(short=True))
elif args.tags: elif args.tags:
print(util.py2encode(plugins.get_exporter("tags").export(journal))) print(plugins.get_exporter("tags").export(journal))
elif args.export is not False: elif args.export is not False:
exporter = plugins.get_exporter(args.export) exporter = plugins.get_exporter(args.export)
@ -275,7 +272,8 @@ def run(manual_args=None):
elif args.edit: elif args.edit:
if not config['editor']: if not config['editor']:
util.prompt("[{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)) 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) sys.exit(1)
other_entries = [e for e in old_entries if e not in journal.entries] other_entries = [e for e in old_entries if e not in journal.entries]
# Edit # Edit
@ -286,11 +284,11 @@ def run(manual_args=None):
num_edited = len([e for e in journal.entries if e.modified]) num_edited = len([e for e in journal.entries if e.modified])
prompts = [] prompts = []
if num_deleted: if num_deleted:
prompts.append("{0} {1} 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: if num_edited:
prompts.append("{0} {1} 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: if prompts:
util.prompt("[{0}]".format(", ".join(prompts).capitalize())) print("[{}]".format(", ".join(prompts).capitalize()), file=sys.stderr)
journal.entries += other_entries journal.entries += other_entries
journal.sort() journal.sort()
journal.write() journal.write()

View file

@ -1,15 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .util import ERROR_COLOR, RESET_COLOR from .util import ERROR_COLOR, RESET_COLOR
from .util import slugify, u from .util import slugify
from .template import Template from .plugins.template import Template
import os import os
import codecs
class Exporter(object): class Exporter:
"""This Exporter can convert entries and journals into text files.""" """This Exporter can convert entries and journals into text files."""
def __init__(self, format): def __init__(self, format):
with open("jrnl/templates/" + format + ".template") as f: with open("jrnl/templates/" + format + ".template") as f:
@ -17,8 +14,8 @@ class Exporter(object):
self.template = Template(body) self.template = Template(body)
def export_entry(self, entry): def export_entry(self, entry):
"""Returns a unicode representation of a single entry.""" """Returns a string representation of a single entry."""
return entry.__unicode__() return str(entry)
def _get_vars(self, journal): def _get_vars(self, journal):
return { return {
@ -28,36 +25,36 @@ class Exporter(object):
} }
def export_journal(self, journal): def export_journal(self, journal):
"""Returns a unicode representation of an entire journal.""" """Returns a string representation of an entire journal."""
return self.template.render_block("journal", **self._get_vars(journal)) return self.template.render_block("journal", **self._get_vars(journal))
def write_file(self, journal, path): def write_file(self, journal, path):
"""Exports a journal into a single file.""" """Exports a journal into a single file."""
try: try:
with codecs.open(path, "w", "utf-8") as f: with open(path, "w", encoding="utf-8") as f:
f.write(self.export_journal(journal)) f.write(self.export_journal(journal))
return "[Journal exported to {0}]".format(path) return f"[Journal exported to {path}]"
except IOError as e: except OSError as e:
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR) return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
def make_filename(self, entry): def make_filename(self, entry):
return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), self.extension)) return entry.date.strftime("%Y-%m-%d_{}.{}".format(slugify(entry.title), self.extension))
def write_files(self, journal, path): def write_files(self, journal, path):
"""Exports a journal into individual files for each entry.""" """Exports a journal into individual files for each entry."""
for entry in journal.entries: for entry in journal.entries:
try: try:
full_path = os.path.join(path, self.make_filename(entry)) full_path = os.path.join(path, self.make_filename(entry))
with codecs.open(full_path, "w", "utf-8") as f: with open(full_path, "w", encoding="utf-8") as f:
f.write(self.export_entry(entry)) f.write(self.export_entry(entry))
except IOError as e: except OSError as e:
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR) return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
return "[Journal exported to {0}]".format(path) return f"[Journal exported to {path}]"
def export(self, journal, format="text", output=None): def export(self, journal, format="text", output=None):
"""Exports to individual files if output is an existing path, or into """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 a single file if output is a file name, or returns the exporter's
representation as unicode if output is None.""" representation as string if output is None."""
if output and os.path.isdir(output): # multiple files if output and os.path.isdir(output): # multiple files
return self.write_files(journal, output) return self.write_files(journal, output)
elif output: # single file elif output: # single file

View file

@ -1,8 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import
import readline
import glob import glob
import getpass import getpass
import os import os
@ -16,6 +13,9 @@ from .util import UserAbort
import yaml import yaml
import logging import logging
import sys import sys
if "win32" not in sys.platform:
# readline is not included in Windows Active Python
import readline
DEFAULT_CONFIG_NAME = 'jrnl.yaml' DEFAULT_CONFIG_NAME = 'jrnl.yaml'
DEFAULT_JOURNAL_NAME = 'journal.txt' DEFAULT_JOURNAL_NAME = 'journal.txt'
@ -69,7 +69,7 @@ def upgrade_config(config):
for key in missing_keys: for key in missing_keys:
config[key] = default_config[key] config[key] = default_config[key]
save_config(config) save_config(config)
print("[Configuration updated to newest version at {}]".format(CONFIG_FILE_PATH)) print(f"[Configuration updated to newest version at {CONFIG_FILE_PATH}]", file=sys.stderr)
def save_config(config): def save_config(config):
@ -91,10 +91,10 @@ def load_or_install_jrnl():
try: try:
upgrade.upgrade_jrnl_if_necessary(config_path) upgrade.upgrade_jrnl_if_necessary(config_path)
except upgrade.UpgradeValidationException: except upgrade.UpgradeValidationException:
util.prompt("Aborting upgrade.") print("Aborting upgrade.", file=sys.stderr)
util.prompt("Please tell us about this problem at the following URL:") print("Please tell us about this problem at the following URL:", file=sys.stderr)
util.prompt("https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException") print("https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException", file=sys.stderr)
util.prompt("Exiting.") print("Exiting.", file=sys.stderr)
sys.exit(1) sys.exit(1)
upgrade_config(config) upgrade_config(config)
@ -110,18 +110,14 @@ def load_or_install_jrnl():
def install(): def install():
def autocomplete(text, state): if "win32" not in sys.platform:
expansions = glob.glob(os.path.expanduser(os.path.expandvars(text)) + '*') readline.set_completer_delims(' \t\n;')
expansions = [e + "/" if os.path.isdir(e) else e for e in expansions] readline.parse_and_bind("tab: complete")
expansions.append(None) readline.set_completer(autocomplete)
return expansions[state]
readline.set_completer_delims(' \t\n;')
readline.parse_and_bind("tab: complete")
readline.set_completer(autocomplete)
# Where to create the journal? # Where to create the journal?
path_query = 'Path to your journal file (leave blank for {}): '.format(JOURNAL_FILE_PATH) path_query = f'Path to your journal file (leave blank for {JOURNAL_FILE_PATH}): '
journal_path = util.py23_input(path_query).strip() or 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'] = 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 path = os.path.split(default_config['journals']['default'])[0] # If the folder doesn't exist, create it
@ -139,7 +135,7 @@ def install():
else: else:
util.set_keychain("default", None) util.set_keychain("default", None)
EncryptedJournal._create(default_config['journals']['default'], password) EncryptedJournal._create(default_config['journals']['default'], password)
print("Journal will be encrypted.") print("Journal will be encrypted.", file=sys.stderr)
else: else:
PlainJournal._create(default_config['journals']['default']) PlainJournal._create(default_config['journals']['default'])
@ -148,3 +144,9 @@ def install():
if password: if password:
config['password'] = password config['password'] = password
return config return config
def autocomplete(text, state):
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]

View file

@ -1,8 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .text_exporter import TextExporter from .text_exporter import TextExporter
from .jrnl_importer import JRNLImporter from .jrnl_importer import JRNLImporter
from .json_exporter import JSONExporter from .json_exporter import JSONExporter
@ -11,12 +9,13 @@ from .tag_exporter import TagExporter
from .xml_exporter import XMLExporter from .xml_exporter import XMLExporter
from .yaml_exporter import YAMLExporter from .yaml_exporter import YAMLExporter
from .template_exporter import __all__ as template_exporters from .template_exporter import __all__ as template_exporters
from .fancy_exporter import FancyExporter
__exporters =[JSONExporter, MarkdownExporter, TagExporter, TextExporter, XMLExporter, YAMLExporter] + template_exporters __exporters =[JSONExporter, MarkdownExporter, TagExporter, TextExporter, XMLExporter, YAMLExporter, FancyExporter] + template_exporters
__importers =[JRNLImporter] __importers =[JRNLImporter]
__exporter_types = dict([(name, plugin) for plugin in __exporters for name in plugin.names]) __exporter_types = {name: plugin for plugin in __exporters for name in plugin.names}
__importer_types = dict([(name, plugin) for plugin in __importers for name in plugin.names]) __importer_types = {name: plugin for plugin in __importers for name in plugin.names}
EXPORT_FORMATS = sorted(__exporter_types.keys()) EXPORT_FORMATS = sorted(__exporter_types.keys())
IMPORT_FORMATS = sorted(__importer_types.keys()) IMPORT_FORMATS = sorted(__importer_types.keys())

View file

@ -0,0 +1,56 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals, print_function
from .text_exporter import TextExporter
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=""
@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
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+' ')
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)
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)
if entry.body:
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)
return "\n".join(card)
@classmethod
def export_journal(cls, journal):
"""Returns a unicode representation of an entire journal."""
return "\n".join(cls.export_entry(entry) for entry in journal)

View file

@ -1,12 +1,10 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
from __future__ import absolute_import, unicode_literals
import codecs
import sys import sys
from .. import util from .. import util
class JRNLImporter(object): class JRNLImporter:
"""This plugin imports entries from other jrnl files.""" """This plugin imports entries from other jrnl files."""
names = ["jrnl"] names = ["jrnl"]
@ -17,15 +15,15 @@ class JRNLImporter(object):
old_cnt = len(journal.entries) old_cnt = len(journal.entries)
old_entries = journal.entries old_entries = journal.entries
if input: if input:
with codecs.open(input, "r", "utf-8") as f: with open(input, "r", encoding="utf-8") as f:
other_journal_txt = f.read() other_journal_txt = f.read()
else: else:
try: try:
other_journal_txt = util.py23_read() other_journal_txt = sys.stdin.read()
except KeyboardInterrupt: except KeyboardInterrupt:
util.prompt("[Entries NOT imported into journal.]") print("[Entries NOT imported into journal.]", file=sys.stderr)
sys.exit(0) sys.exit(0)
journal.import_(other_journal_txt) journal.import_(other_journal_txt)
new_cnt = len(journal.entries) new_cnt = len(journal.entries)
util.prompt("[{0} imported to {1} journal]".format(new_cnt - old_cnt, journal.name)) print("[{} imported to {} journal]".format(new_cnt - old_cnt, journal.name), file=sys.stderr)
journal.write() journal.write()

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .text_exporter import TextExporter from .text_exporter import TextExporter
import json import json
from .util import get_tags_count from .util import get_tags_count
@ -35,7 +34,7 @@ class JSONExporter(TextExporter):
"""Returns a json representation of an entire journal.""" """Returns a json representation of an entire journal."""
tags = get_tags_count(journal) tags = get_tags_count(journal)
result = { result = {
"tags": dict((tag, count) for count, tag in tags), "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) return json.dumps(result, indent=2)

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
from __future__ import absolute_import, unicode_literals, print_function
from .text_exporter import TextExporter from .text_exporter import TextExporter
import os import os
import re import re
@ -51,15 +50,11 @@ class MarkdownExporter(TextExporter):
newbody = newbody + previous_line # add very last line newbody = newbody + previous_line # add very last line
if warn_on_heading_level is True: 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(f"{WARNING_COLOR}WARNING{RESET_COLOR}: "
f"Headings increased past H6 on export - {date_str} {entry.title}",
file=sys.stderr)
return "{md} {date} {title}\n{body} {space}".format( return f"{heading} {date_str} {entry.title}\n{newbody} "
md=heading,
date=date_str,
title=entry.title,
body=newbody,
space=""
)
@classmethod @classmethod
def export_journal(cls, journal): def export_journal(cls, journal):

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .text_exporter import TextExporter from .text_exporter import TextExporter
from .util import get_tags_count from .util import get_tags_count
@ -26,5 +25,5 @@ class TagExporter(TextExporter):
elif min(tag_counts)[0] == 0: elif min(tag_counts)[0] == 0:
tag_counts = filter(lambda x: x[0] > 1, tag_counts) tag_counts = filter(lambda x: x[0] > 1, tag_counts)
result += '[Removed tags that appear only once.]\n' result += '[Removed tags that appear only once.]\n'
result += "\n".join("{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True)) result += "\n".join("{:20} : {}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True))
return result return result

View file

@ -12,7 +12,7 @@ BLOCK_RE = r"{% *block +(.+?) *%}((?:.|\n)+?){% *endblock *%}"
INCLUDE_RE = r"{% *include +(.+?) *%}" INCLUDE_RE = r"{% *include +(.+?) *%}"
class Template(object): class Template:
def __init__(self, template): def __init__(self, template):
self.template = template self.template = template
self.clean_template = None self.clean_template = None

View file

@ -1,8 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .text_exporter import TextExporter from .text_exporter import TextExporter
from .template import Template from .template import Template
import os import os
@ -14,7 +12,7 @@ class GenericTemplateExporter(TextExporter):
@classmethod @classmethod
def export_entry(cls, entry): def export_entry(cls, entry):
"""Returns a unicode representation of a single entry.""" """Returns a string representation of a single entry."""
vars = { vars = {
'entry': entry, 'entry': entry,
'tags': entry.tags 'tags': entry.tags
@ -23,7 +21,7 @@ class GenericTemplateExporter(TextExporter):
@classmethod @classmethod
def export_journal(cls, journal): def export_journal(cls, journal):
"""Returns a unicode representation of an entire journal.""" """Returns a string representation of an entire journal."""
vars = { vars = {
'journal': journal, 'journal': journal,
'entries': journal.entries, 'entries': journal.entries,
@ -36,7 +34,7 @@ def __exporter_from_file(template_file):
"""Create a template class from a file""" """Create a template class from a file"""
name = os.path.basename(template_file).replace(".template", "") name = os.path.basename(template_file).replace(".template", "")
template = Template.from_file(template_file) template = Template.from_file(template_file)
return type(str("{}Exporter".format(name.title())), (GenericTemplateExporter, ), { return type(str(f"{name.title()}Exporter"), (GenericTemplateExporter, ), {
"names": [name], "names": [name],
"extension": template.extension, "extension": template.extension,
"template": template "template": template

View file

@ -1,41 +1,39 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
from __future__ import absolute_import, unicode_literals from ..util import slugify
import codecs
from ..util import u, slugify
import os import os
from ..util import ERROR_COLOR, RESET_COLOR from ..util import ERROR_COLOR, RESET_COLOR
class TextExporter(object): class TextExporter:
"""This Exporter can convert entries and journals into text files.""" """This Exporter can convert entries and journals into text files."""
names = ["text", "txt"] names = ["text", "txt"]
extension = "txt" extension = "txt"
@classmethod @classmethod
def export_entry(cls, entry): def export_entry(cls, entry):
"""Returns a unicode representation of a single entry.""" """Returns a string representation of a single entry."""
return entry.__unicode__() return str(entry)
@classmethod @classmethod
def export_journal(cls, journal): def export_journal(cls, journal):
"""Returns a unicode representation of an entire journal.""" """Returns a string representation of an entire journal."""
return "\n".join(cls.export_entry(entry) for entry in journal) return "\n".join(cls.export_entry(entry) for entry in journal)
@classmethod @classmethod
def write_file(cls, journal, path): def write_file(cls, journal, path):
"""Exports a journal into a single file.""" """Exports a journal into a single file."""
try: try:
with codecs.open(path, "w", "utf-8") as f: with open(path, "w", encoding="utf-8") as f:
f.write(cls.export_journal(journal)) f.write(cls.export_journal(journal))
return "[Journal exported to {0}]".format(path) return f"[Journal exported to {path}]"
except IOError as e: except IOError as e:
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR) return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
@classmethod @classmethod
def make_filename(cls, entry): def make_filename(cls, entry):
return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), cls.extension)) return entry.date.strftime("%Y-%m-%d_{}.{}".format(slugify(str(entry.title)), cls.extension))
@classmethod @classmethod
def write_files(cls, journal, path): def write_files(cls, journal, path):
@ -43,17 +41,17 @@ class TextExporter(object):
for entry in journal.entries: for entry in journal.entries:
try: try:
full_path = os.path.join(path, cls.make_filename(entry)) full_path = os.path.join(path, cls.make_filename(entry))
with codecs.open(full_path, "w", "utf-8") as f: with open(full_path, "w", encoding="utf-8") as f:
f.write(cls.export_entry(entry)) f.write(cls.export_entry(entry))
except IOError as e: 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 {0}]".format(path) return "[Journal exported to {}]".format(path)
@classmethod @classmethod
def export(cls, journal, output=None): def export(cls, journal, output=None):
"""Exports to individual files if output is an existing path, or into """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 a single file if output is a file name, or returns the exporter's
representation as unicode if output is None.""" representation as string if output is None."""
if output and os.path.isdir(output): # multiple files if output and os.path.isdir(output): # multiple files
return cls.write_files(journal, output) return cls.write_files(journal, output)
elif output: # single file elif output: # single file

View file

@ -10,7 +10,7 @@ def get_tags_count(journal):
for entry in journal.entries for entry in journal.entries
for tag in set(entry.tags)] for tag in set(entry.tags)]
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag] # To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
tag_counts = set([(tags.count(tag), tag) for tag in tags]) tag_counts = {(tags.count(tag), tag) for tag in tags}
return tag_counts return tag_counts

View file

@ -1,10 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .json_exporter import JSONExporter from .json_exporter import JSONExporter
from .util import get_tags_count from .util import get_tags_count
from ..util import u
from xml.dom import minidom from xml.dom import minidom
@ -20,7 +18,7 @@ class XMLExporter(JSONExporter):
entry_el = doc_el.createElement('entry') entry_el = doc_el.createElement('entry')
for key, value in cls.entry_to_dict(entry).items(): for key, value in cls.entry_to_dict(entry).items():
elem = doc_el.createElement(key) elem = doc_el.createElement(key)
elem.appendChild(doc_el.createTextNode(u(value))) elem.appendChild(doc_el.createTextNode(value))
entry_el.appendChild(elem) entry_el.appendChild(elem)
if not doc: if not doc:
doc_el.appendChild(entry_el) doc_el.appendChild(entry_el)
@ -33,8 +31,8 @@ class XMLExporter(JSONExporter):
entry_el = doc.createElement('entry') entry_el = doc.createElement('entry')
entry_el.setAttribute('date', entry.date.isoformat()) entry_el.setAttribute('date', entry.date.isoformat())
if hasattr(entry, "uuid"): if hasattr(entry, "uuid"):
entry_el.setAttribute('uuid', u(entry.uuid)) entry_el.setAttribute('uuid', entry.uuid)
entry_el.setAttribute('starred', u(entry.starred)) entry_el.setAttribute('starred', entry.starred)
entry_el.appendChild(doc.createTextNode(entry.fulltext)) entry_el.appendChild(doc.createTextNode(entry.fulltext))
return entry_el return entry_el
@ -49,7 +47,7 @@ class XMLExporter(JSONExporter):
for count, tag in tags: for count, tag in tags:
tag_el = doc.createElement('tag') tag_el = doc.createElement('tag')
tag_el.setAttribute('name', tag) tag_el.setAttribute('name', tag)
count_node = doc.createTextNode(u(count)) count_node = doc.createTextNode(str(count))
tag_el.appendChild(count_node) tag_el.appendChild(count_node)
tags_el.appendChild(tag_el) tags_el.appendChild(tag_el)
for entry in journal.entries: for entry in journal.entries:

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
from __future__ import absolute_import, unicode_literals, print_function
from .text_exporter import TextExporter from .text_exporter import TextExporter
import os import os
import re import re
@ -18,7 +17,8 @@ class YAMLExporter(TextExporter):
def export_entry(cls, entry, to_multifile=True): def export_entry(cls, entry, to_multifile=True):
"""Returns a markdown representation of a single entry, with YAML front matter.""" """Returns a markdown representation of a single entry, with YAML front matter."""
if to_multifile is False: 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("\033[31m", "\033[0m"), file=sys.stderr)
return return
date_str = entry.date.strftime(entry.journal.config['timeformat']) date_str = entry.date.strftime(entry.journal.config['timeformat'])
@ -27,7 +27,7 @@ class YAMLExporter(TextExporter):
tagsymbols = entry.journal.config['tagsymbols'] tagsymbols = entry.journal.config['tagsymbols']
# see also Entry.Entry.rag_regex # see also Entry.Entry.rag_regex
multi_tag_regex = re.compile(r'(?u)^\s*([{tags}][-+*#/\w]+\s*)+$'.format(tags=tagsymbols), re.UNICODE) multi_tag_regex = re.compile(r'(?u)^\s*([{tags}][-+*#/\w]+\s*)+$'.format(tags=tagsymbols))
'''Increase heading levels in body text''' '''Increase heading levels in body text'''
newbody = '' newbody = ''

View file

@ -12,15 +12,16 @@ consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
CALENDAR = pdt.Calendar(consts) CALENDAR = pdt.Calendar(consts)
def parse(date_str, inclusive=False, default_hour=None, default_minute=None): 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""" """Parses a string containing a fuzzy date and returns a datetime.datetime object"""
if not date_str: if not date_str:
return None return None
elif isinstance(date_str, datetime): elif isinstance(date_str, datetime):
return date_str return date_str
# Don't try to parse anything with 6 or less characters. It's probably a markdown footnote # Don't try to parse anything with 6 or less characters and was parsed from the existing journal.
if len(date_str) <= 6: # It's probably a markdown footnote
if len(date_str) <= 6 and bracketed:
return None return None
default_date = DEFAULT_FUTURE if inclusive else DEFAULT_PAST default_date = DEFAULT_FUTURE if inclusive else DEFAULT_PAST
@ -52,7 +53,10 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None):
return None return None
if flag is 1: # Date found, but no time. Use the default time. if flag is 1: # Date found, but no time. Use the default time.
date = datetime(*date[:3], hour=default_hour or 0, minute=default_minute or 0) date = datetime(*date[:3],
hour=23 if inclusive else default_hour or 0,
minute=59 if inclusive else default_minute or 0,
second=59 if inclusive else 0)
else: else:
date = datetime(*date[:6]) date = datetime(*date[:6])

View file

@ -1,4 +1,4 @@
from __future__ import absolute_import, unicode_literals import sys
from . import __version__ from . import __version__
from . import Journal from . import Journal
@ -6,11 +6,10 @@ from . import util
from .EncryptedJournal import EncryptedJournal from .EncryptedJournal import EncryptedJournal
from .util import UserAbort from .util import UserAbort
import os import os
import codecs
def backup(filename, binary=False): def backup(filename, binary=False):
util.prompt(" Created a backup at {}.backup".format(filename)) print(f" Created a backup at {filename}.backup", file=sys.stderr)
filename = os.path.expanduser(os.path.expandvars(filename)) 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() contents = original.read()
@ -19,14 +18,14 @@ def backup(filename, binary=False):
def upgrade_jrnl_if_necessary(config_path): def upgrade_jrnl_if_necessary(config_path):
with codecs.open(config_path, "r", "utf-8") as f: with open(config_path, "r", encoding="utf-8") as f:
config_file = f.read() config_file = f.read()
if not config_file.strip().startswith("{"): if not config_file.strip().startswith("{"):
return return
config = util.load_config(config_path) config = util.load_config(config_path)
util.prompt("""Welcome to jrnl {}. print("""Welcome to jrnl {}.
It looks like you've been using an older version of jrnl until now. That's 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 okay - jrnl will now upgrade your configuration and journal files. Afterwards
@ -54,6 +53,8 @@ older versions of jrnl anymore.
encrypt = config.get('encrypt') encrypt = config.get('encrypt')
path = journal_conf path = journal_conf
path = os.path.expanduser(path)
if encrypt: if encrypt:
encrypted_journals[journal_name] = path encrypted_journals[journal_name] = path
elif os.path.isdir(path): elif os.path.isdir(path):
@ -63,19 +64,19 @@ older versions of jrnl anymore.
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: if encrypted_journals:
util.prompt("\nFollowing encrypted journals will be upgraded to jrnl {}:".format(__version__)) print(f"\nFollowing encrypted journals will be upgraded to jrnl {__version__}:", file=sys.stderr)
for journal, path in encrypted_journals.items(): for journal, path in encrypted_journals.items():
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name)) print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr)
if plain_journals: if plain_journals:
util.prompt("\nFollowing plain text journals will upgraded to jrnl {}:".format(__version__)) print(f"\nFollowing plain text journals will upgraded to jrnl {__version__}:", file=sys.stderr)
for journal, path in plain_journals.items(): for journal, path in plain_journals.items():
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name)) print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr)
if other_journals: if other_journals:
util.prompt("\nFollowing journals will be not be touched:") print("\nFollowing journals will be not be touched:", file=sys.stderr)
for journal, path in other_journals.items(): for journal, path in other_journals.items():
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name)) print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr)
try: try:
cont = util.yesno("\nContinue upgrading jrnl?", default=False) cont = util.yesno("\nContinue upgrading jrnl?", default=False)
@ -85,13 +86,13 @@ older versions of jrnl anymore.
raise UserAbort("jrnl NOT upgraded, exiting.") raise UserAbort("jrnl NOT upgraded, exiting.")
for journal_name, path in encrypted_journals.items(): for journal_name, path in encrypted_journals.items():
util.prompt("\nUpgrading encrypted '{}' journal stored in {}...".format(journal_name, path)) print(f"\nUpgrading encrypted '{journal_name}' journal stored in {path}...", file=sys.stderr)
backup(path, binary=True) 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)) all_journals.append(EncryptedJournal.from_journal(old_journal))
for journal_name, path in plain_journals.items(): for journal_name, path in plain_journals.items():
util.prompt("\nUpgrading plain text '{}' journal stored in {}...".format(journal_name, path)) print(f"\nUpgrading plain text '{journal_name}' journal stored in {path}...", file=sys.stderr)
backup(path) 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)) all_journals.append(Journal.PlainJournal.from_journal(old_journal))
@ -100,8 +101,9 @@ older versions of jrnl anymore.
failed_journals = [j for j in all_journals if not j.validate_parsing()] failed_journals = [j for j in all_journals if not j.validate_parsing()]
if len(failed_journals) > 0: if len(failed_journals) > 0:
util.prompt("\nThe following journal{} failed to upgrade:\n{}".format( 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)) 's' if len(failed_journals) > 1 else '', "\n".join(j.name for j in failed_journals)),
file=sys.stderr
) )
raise UpgradeValidationException raise UpgradeValidationException
@ -110,10 +112,11 @@ older versions of jrnl anymore.
for j in all_journals: for j in all_journals:
j.write() j.write()
util.prompt("\nUpgrading config...") print("\nUpgrading config...", file=sys.stderr)
backup(config_path) backup(config_path)
util.prompt("\nWe're all done here and you can start enjoying jrnl 2.".format(config_path)) print("\nWe're all done here and you can start enjoying jrnl 2.", file=sys.stderr)
class UpgradeValidationException(Exception): class UpgradeValidationException(Exception):
"""Raised when the contents of an upgraded journal do not match the old journal""" """Raised when the contents of an upgraded journal do not match the old journal"""

View file

@ -1,8 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8
from __future__ import unicode_literals
from __future__ import absolute_import
import sys import sys
import os import os
@ -14,22 +10,12 @@ if "win32" in sys.platform:
import re import re
import tempfile import tempfile
import subprocess import subprocess
import codecs
import unicodedata import unicodedata
import shlex import shlex
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
PY3 = sys.version_info[0] == 3
PY2 = sys.version_info[0] == 2
STDIN = sys.stdin
STDERR = sys.stderr
STDOUT = sys.stdout
TEST = False
__cached_tz = None
WARNING_COLOR = "\033[33m" WARNING_COLOR = "\033[33m"
ERROR_COLOR = "\033[31m" ERROR_COLOR = "\033[31m"
RESET_COLOR = "\033[0m" RESET_COLOR = "\033[0m"
@ -44,43 +30,48 @@ SENTENCE_SPLITTER = re.compile(r"""
\s+ # a sequence of required spaces. \s+ # a sequence of required spaces.
| # Otherwise, | # Otherwise,
\n # a sentence also terminates newlines. \n # a sentence also terminates newlines.
)""", re.UNICODE | re.VERBOSE) )""", re.VERBOSE)
class UserAbort(Exception): class UserAbort(Exception):
pass pass
def getpass(prompt="Password: "): def create_password():
if not TEST: while True:
return gp.getpass(bytes(prompt)) pw = gp.getpass("Enter password for new journal: ")
else: if pw == gp.getpass("Enter password again: "):
return py23_input(prompt) return pw
print("Passwords did not match, please try again", file=sys.stderr)
def get_password(validator, keychain=None, max_attempts=3): def get_password(validator, keychain=None, max_attempts=3):
pwd_from_keychain = keychain and get_keychain(keychain) pwd_from_keychain = keychain and get_keychain(keychain)
password = pwd_from_keychain or getpass() password = pwd_from_keychain or gp.getpass()
result = validator(password) result = validator(password)
# Password is bad: # Password is bad:
if result is None and pwd_from_keychain: if result is None and pwd_from_keychain:
set_keychain(keychain, None) set_keychain(keychain, None)
attempt = 1 attempt = 1
while result is None and attempt < max_attempts: while result is None and attempt < max_attempts:
prompt("Wrong password, try again.") print("Wrong password, try again.", file=sys.stderr)
password = getpass() password = gp.getpass()
result = validator(password) result = validator(password)
attempt += 1 attempt += 1
if result is not None: if result is not None:
return result return result
else: else:
prompt("Extremely wrong password.") print("Extremely wrong password.", file=sys.stderr)
sys.exit(1) sys.exit(1)
def get_keychain(journal_name): def get_keychain(journal_name):
import keyring import keyring
return keyring.get_password('jrnl', journal_name) try:
return keyring.get_password('jrnl', journal_name)
except RuntimeError:
return ""
def set_keychain(journal_name, password): def set_keychain(journal_name, password):
@ -88,57 +79,16 @@ def set_keychain(journal_name, password):
if password is None: if password is None:
try: try:
keyring.delete_password('jrnl', journal_name) keyring.delete_password('jrnl', journal_name)
except: except RuntimeError:
pass pass
elif not TEST: else:
keyring.set_password('jrnl', journal_name, password) keyring.set_password('jrnl', journal_name, password)
def u(s):
"""Mock unicode function for python 2 and 3 compatibility."""
if not isinstance(s, str):
s = str(s)
return s if PY3 or type(s) is unicode else s.decode("utf-8")
def py2encode(s):
"""Encodes to UTF-8 in Python 2 but not in Python 3."""
return s.encode("utf-8") if PY2 and type(s) is unicode else s
def bytes(s):
"""Returns bytes, no matter what."""
if PY3:
return s.encode("utf-8") if type(s) is not bytes else s
return s.encode("utf-8") if type(s) is unicode else s
def prnt(s):
"""Encode and print a string"""
STDOUT.write(u(s + "\n"))
def prompt(msg):
"""Prints a message to the std err stream defined in util."""
if not msg.endswith("\n"):
msg += "\n"
STDERR.write(u(msg))
def py23_input(msg=""):
prompt(msg)
return STDIN.readline().strip()
def py23_read(msg=""):
print(msg)
return STDIN.read()
def yesno(prompt, default=True): def yesno(prompt, default=True):
prompt = prompt.strip() + (" [Y/n]" if default else " [y/N]") prompt = f"{prompt.strip()} {'[Y/n]' if default else '[y/N]'} "
raw = py23_input(prompt) response = input(prompt)
return {'y': True, 'n': False}.get(raw.lower(), default) return {"y": True, "n": False}.get(response.lower(), default)
def load_config(config_path): def load_config(config_path):
@ -164,51 +114,35 @@ def scope_config(config, journal_name):
def get_text_from_editor(config, template=""): def get_text_from_editor(config, template=""):
filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt") filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt")
with codecs.open(tmpfile, 'w', "utf-8") as f: with open(tmpfile, 'w', encoding="utf-8") as f:
if template: if template:
f.write(template) f.write(template)
try: 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: except AttributeError:
subprocess.call(config['editor'] + [tmpfile]) subprocess.call(config['editor'] + [tmpfile])
with codecs.open(tmpfile, "r", "utf-8") as f: with open(tmpfile, "r", encoding="utf-8") as f:
raw = f.read() raw = f.read()
os.close(filehandle) os.close(filehandle)
os.remove(tmpfile) os.remove(tmpfile)
if not raw: if not raw:
prompt('[Nothing saved to file]') print('[Nothing saved to file]', file=sys.stderr)
return raw return raw
def colorize(string): def colorize(string):
"""Returns the string wrapped in cyan ANSI escape""" """Returns the string wrapped in cyan ANSI escape"""
return u"\033[36m{}\033[39m".format(string) return f"\033[36m{string}\033[39m"
def slugify(string): def slugify(string):
"""Slugifies a string. """Slugifies a string.
Based on public domain code from https://github.com/zacharyvoase/slugify Based on public domain code from https://github.com/zacharyvoase/slugify
and ported to deal with all kinds of python 2 and 3 strings
""" """
string = u(string) normalized_string = str(unicodedata.normalize('NFKD', string))
ascii_string = str(unicodedata.normalize('NFKD', string).encode('ascii', 'ignore')) no_punctuation = re.sub(r'[^\w\s-]', '', normalized_string).strip().lower()
if PY3:
ascii_string = ascii_string[1:] # removed the leading 'b'
no_punctuation = re.sub(r'[^\w\s-]', '', ascii_string).strip().lower()
slug = re.sub(r'[-\s]+', '-', no_punctuation) slug = re.sub(r'[-\s]+', '-', no_punctuation)
return u(slug) return slug
def int2byte(i):
"""Converts an integer to a byte.
This is equivalent to chr() in Python 2 and bytes((i,)) in Python 3."""
return chr(i) if PY2 else bytes((i,))
def byte2int(b):
"""Converts a byte to an integer.
This is equivalent to ord(bs[0]) on Python 2 and bs[0] on Python 3."""
return ord(b)if PY2 else b
def split_title(text): def split_title(text):

132
poetry.lock generated
View file

@ -6,25 +6,13 @@ optional = false
python-versions = "*" python-versions = "*"
version = "1.4.3" 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]] [[package]]
category = "main" category = "main"
description = "Safe, minimalistic evaluator of python expression using ast module" description = "Safe, minimalistic evaluator of python expression using ast module"
name = "asteval" name = "asteval"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.9.14" version = "0.9.17"
[package.dependencies]
numpy = "*"
six = "*"
[[package]] [[package]]
category = "dev" category = "dev"
@ -32,7 +20,7 @@ description = "Classes Without Boilerplate"
name = "attrs" name = "attrs"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "19.1.0" version = "19.3.0"
[[package]] [[package]]
category = "dev" category = "dev"
@ -67,7 +55,7 @@ description = "Foreign Function Interface for Python calling C code."
name = "cffi" name = "cffi"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "1.12.3" version = "1.13.2"
[package.dependencies] [package.dependencies]
pycparser = "*" pycparser = "*"
@ -95,15 +83,14 @@ description = "cryptography is a package which provides cryptographic recipes an
name = "cryptography" name = "cryptography"
optional = false optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "2.7" version = "2.8"
[package.dependencies] [package.dependencies]
asn1crypto = ">=0.21.0"
cffi = ">=1.8,<1.11.3 || >1.11.3" cffi = ">=1.8,<1.11.3 || >1.11.3"
six = ">=1.4.1" six = ">=1.4.1"
[[package]] [[package]]
category = "main" category = "dev"
description = "Discover and load entry points from installed packages." description = "Discover and load entry points from installed packages."
name = "entrypoints" name = "entrypoints"
optional = false optional = false
@ -116,7 +103,7 @@ description = "the modular source code checker: pep8, pyflakes and co"
name = "flake8" name = "flake8"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "3.7.8" version = "3.7.9"
[package.dependencies] [package.dependencies]
entrypoints = ">=0.3.0,<0.4.0" entrypoints = ">=0.3.0,<0.4.0"
@ -126,11 +113,15 @@ pyflakes = ">=2.1.0,<2.2.0"
[[package]] [[package]]
category = "main" category = "main"
description = "Clean single-source support for Python 3 and 2" description = "Read metadata from Python packages"
name = "future" marker = "python_version < \"3.8\""
name = "importlib-metadata"
optional = false optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3"
version = "0.17.1" version = "1.0.0"
[package.dependencies]
zipp = ">=0.5"
[[package]] [[package]]
category = "main" category = "main"
@ -139,15 +130,15 @@ marker = "sys_platform == \"linux\""
name = "jeepney" name = "jeepney"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
version = "0.4" version = "0.4.1"
[[package]] [[package]]
category = "dev" 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" name = "jinja2"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "2.10.1" version = "2.10.3"
[package.dependencies] [package.dependencies]
MarkupSafe = ">=0.23" MarkupSafe = ">=0.23"
@ -158,13 +149,16 @@ description = "Store and access your passwords safely."
name = "keyring" name = "keyring"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
version = "19.0.2" version = "19.3.0"
[package.dependencies] [package.dependencies]
entrypoints = "*"
pywin32-ctypes = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1" pywin32-ctypes = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1"
secretstorage = "*" secretstorage = "*"
[package.dependencies.importlib-metadata]
python = "<3.8"
version = "*"
[[package]] [[package]]
category = "dev" category = "dev"
description = "Python LiveReload is an awesome tool for web developers" description = "Python LiveReload is an awesome tool for web developers"
@ -222,11 +216,12 @@ tornado = ">=5.0"
[[package]] [[package]]
category = "main" category = "main"
description = "NumPy is the fundamental package for array computing with Python." description = "More routines for operating on iterables, beyond itertools"
name = "numpy" marker = "python_version < \"3.8\""
name = "more-itertools"
optional = false optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" python-versions = ">=3.5"
version = "1.16.4" version = "8.0.0"
[[package]] [[package]]
category = "dev" category = "dev"
@ -234,7 +229,7 @@ description = "parse() is the opposite of format()"
name = "parse" name = "parse"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "1.12.0" version = "1.12.1"
[[package]] [[package]]
category = "dev" category = "dev"
@ -242,10 +237,10 @@ description = "Simplifies to build parse types based on the parse module"
name = "parse-type" name = "parse-type"
optional = false optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*" python-versions = ">=2.6, !=3.0.*, !=3.1.*"
version = "0.4.2" version = "0.5.2"
[package.dependencies] [package.dependencies]
parse = ">=1.8" parse = ">=1.8.4"
six = ">=1.11" six = ">=1.11"
[[package]] [[package]]
@ -254,10 +249,7 @@ description = "Parse human-readable date/time text."
name = "parsedatetime" name = "parsedatetime"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "2.4" version = "2.5"
[package.dependencies]
future = "*"
[[package]] [[package]]
category = "main" category = "main"
@ -265,7 +257,7 @@ description = "comprehensive password hashing framework supporting over 30 schem
name = "passlib" name = "passlib"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "1.7.1" version = "1.7.2"
[[package]] [[package]]
category = "dev" category = "dev"
@ -296,8 +288,8 @@ category = "main"
description = "Extensions to the standard Python datetime module" description = "Extensions to the standard Python datetime module"
name = "python-dateutil" name = "python-dateutil"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
version = "2.8.0" version = "2.8.1"
[package.dependencies] [package.dependencies]
six = ">=1.5" six = ">=1.5"
@ -308,7 +300,7 @@ description = "World timezone definitions, modern and historical"
name = "pytz" name = "pytz"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "2019.1" version = "2019.3"
[[package]] [[package]]
category = "main" category = "main"
@ -354,7 +346,7 @@ description = "Python 2 and 3 compatibility utilities"
name = "six" name = "six"
optional = false optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*" python-versions = ">=2.6, !=3.0.*, !=3.1.*"
version = "1.12.0" version = "1.13.0"
[[package]] [[package]]
category = "dev" category = "dev"
@ -383,47 +375,59 @@ version = "1.5.1"
[package.dependencies] [package.dependencies]
pytz = "*" 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] [metadata]
content-hash = "9896cf59c7552b6ad95219ee5555c7445a3fab39c2e4f4c6f3d991a36635e44b" content-hash = "f84f27e8b11b6f6a749ae558b693d013c09c086f64a122a1f239eb206370219b"
python-versions = "^3.7" python-versions = ">=3.6.0, <3.9.0"
[metadata.hashes] [metadata.hashes]
appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"] appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"]
asn1crypto = ["2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", "9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"] asteval = ["3e291b2cb71284bc55816fe7ff65ee15e47fa66df98490cdd95f5531fc37b81e"]
asteval = ["7c81fee6707a7a28e8beae891b858535a7e61f9ce275a0a4cf5f428fbc934cb8"] attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"]
attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"]
behave = ["b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86", "ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c"] behave = ["b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86", "ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c"]
black = ["817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", "e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"] 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"] 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"] click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"]
colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"]
cryptography = ["24b61e5fcb506424d3ec4e18bca995833839bf13c59fc43e530e488f28d46b8c", "25dd1581a183e9e7a806fe0543f485103232f940fcfc301db65e630512cce643", "3452bba7c21c69f2df772762be0066c7ed5dc65df494a1d53a58b683a83e1216", "41a0be220dd1ed9e998f5891948306eb8c812b512dc398e5a01846d855050799", "5751d8a11b956fbfa314f6553d186b94aa70fdb03d8a4d4f1c82dcacf0cbe28a", "5f61c7d749048fa6e3322258b4263463bfccefecb0dd731b6561cb617a1d9bb9", "72e24c521fa2106f19623a3851e9f89ddfdeb9ac63871c7643790f872a305dfc", "7b97ae6ef5cba2e3bb14256625423413d5ce8d1abb91d4f29b6d1a081da765f8", "961e886d8a3590fd2c723cf07be14e2a91cf53c25f02435c04d39e90780e3b53", "96d8473848e984184b6728e2c9d391482008646276c3ff084a1bd89e15ff53a1", "ae536da50c7ad1e002c3eee101871d93abdc90d9c5f651818450a0d3af718609", "b0db0cecf396033abb4a93c95d1602f268b3a68bb0a9cc06a7cff587bb9a7292", "cfee9164954c186b191b91d4193989ca994703b2fff406f71cf454a2d3c7327e", "e6347742ac8f35ded4a46ff835c60e68c22a536a8ae5c4422966d06946b6d4c6", "f27d93f0139a3c056172ebb5d4f9056e770fdf0206c2f422ff2ebbad142e09ed", "f57b76e46a58b63d1c6375017f4564a28f19a5ca912691fd2e4261b3414b618d"] 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"] entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"]
flake8 = ["19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", "8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"] flake8 = ["45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", "49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"]
future = ["67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"] importlib-metadata = ["a82ca8c109e194d7d6aee3f7531b0470dd4dd6b36ec14fd55087142a96bd55a7", "f4a7ba72e93bc97ff491b66d69063819ae2b75238bb653cd4c95e3f2847ce76e"]
jeepney = ["6089412a5de162c04747f0220f6b2223b8ba660acd041e52a76426ca550e3c70", "f6f8b1428403b4afad04b6b82f9ab9fc426c253d7504c9031c41712a2c01dc74"] jeepney = ["13806f91a96e9b2623fd2a81b950d763ee471454aafd9eb6d75dbe7afce428fb", "f6a3f93464a0cf052f4e87da3c8b3ed1e27696758fb9739c63d3a74d9a1b6774"]
jinja2 = ["065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", "14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"] jinja2 = ["74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", "9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"]
keyring = ["1b74595f7439e4581a11d4f9a12790ac34addce64ca389c86272ff465f5e0b90", "afbfe7bc9bdba69d25c551b0c738adde533d87e0b51ad6bbe332cbea19ad8476"] keyring = ["9b80469783d3f6106bce1d389c6b8b20c8d4d739943b1b8cd0ddc2a45d065f9d", "ee3d35b7f1ac3cb69e9a1e4323534649d3ab2fea402738a77e4250c152970fed"]
livereload = ["78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", "89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"] livereload = ["78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", "89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"]
markdown = ["2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a", "56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c"] 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"] 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"] mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"]
mkdocs = ["17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939", "8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"] 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"] more-itertools = ["53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2", "a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"]
parse = ["1b68657434d371e5156048ca4a0c5aea5afc6ca59a2fea4dd1a575354f617142"] parse = ["a5fca7000c6588d77bc65c28f3f21bfce03b5e44daa8f9f07c17fe364990d717"]
parse-type = ["6e906a66f340252e4c324914a60d417d33a4bea01292ea9bbf68b4fc123be8c9", "f596bdc75d3dd93036fbfe3d04127da9f6df0c26c36e01e76da85adef4336b3c"] parse-type = ["089a471b06327103865dfec2dd844230c3c658a4a1b5b4c8b6c16c8f77577f9e", "7f690b18d35048c15438d6d0571f9045cffbec5907e0b1ccf006f889e3a38c0b"]
parsedatetime = ["3d817c58fb9570d1eec1dd46fa9448cd644eeed4fb612684b02dfda3a79cb84b", "9ee3529454bf35c40a77115f5a596771e59e1aee8c53306f346c461b8e913094"] parsedatetime = ["3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1", "d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667"]
passlib = ["3d948f64138c25633613f303bcc471126eae67c04d5e3f6b7b8ce6242f8653e0", "43526aea08fa32c6b6dbbbe9963c4c767285b78147b7437597f992812f69d280"] passlib = ["68c35c98a7968850e17f1b6892720764cc7eed0ef2b7cb3116a89a28e43fe177", "8d666cef936198bc2ab47ee9b0410c94adf2ba798e5a84bf220be079ae7ab6a8"]
pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"]
pycparser = ["a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"] pycparser = ["a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"]
pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"]
python-dateutil = ["7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"] python-dateutil = ["73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"]
pytz = ["303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", "d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"] pytz = ["1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", "b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"]
pywin32-ctypes = ["24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", "9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"] pywin32-ctypes = ["24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", "9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"]
pyxdg = ["1948ff8e2db02156c0cccd2529b43c0cff56ebaa71f5f021bbd755bc1419190e", "fe2928d3f532ed32b39c32a482b54136fe766d19936afc96c8f00645f9da1a06"] pyxdg = ["1948ff8e2db02156c0cccd2529b43c0cff56ebaa71f5f021bbd755bc1419190e", "fe2928d3f532ed32b39c32a482b54136fe766d19936afc96c8f00645f9da1a06"]
pyyaml = ["0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", "01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", "5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", "5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", "7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", "7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", "87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", "9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", "a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", "b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", "b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", "bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", "f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"] pyyaml = ["0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", "01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", "5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", "5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", "7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", "7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", "87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", "9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", "a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", "b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", "b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", "bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", "f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"]
secretstorage = ["20c797ae48a4419f66f8d28fc221623f11fc45b6828f96bdb1ad9990acb59f92", "7a119fb52a88e398dbb22a4b3eb39b779bfbace7e4153b7bc6e5954d86282a8a"] secretstorage = ["20c797ae48a4419f66f8d28fc221623f11fc45b6828f96bdb1ad9990acb59f92", "7a119fb52a88e398dbb22a4b3eb39b779bfbace7e4153b7bc6e5954d86282a8a"]
six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] six = ["1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", "30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"]
toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"]
tornado = ["349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", "398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", "4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", "559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", "abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", "c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", "c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"] tornado = ["349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", "398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", "4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", "559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", "abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", "c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", "c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"]
tzlocal = ["4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e"] tzlocal = ["4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e"]
zipp = ["3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", "f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"]

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "jrnl" name = "jrnl"
version = "0.0.0-source" version = "v2.1.1"
description = "Collect your thoughts and notes without leaving the command line." description = "Collect your thoughts and notes without leaving the command line."
authors = [ authors = [
"Manuel Ebert <manuel@1450.me>", "Manuel Ebert <manuel@1450.me>",
@ -13,7 +13,7 @@ homepage = "https://jrnl.sh"
repository = "https://github.com/jrnl-org/jrnl" repository = "https://github.com/jrnl-org/jrnl"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.7" python = ">=3.6.0, <3.9.0"
pyxdg = "^0.26.0" pyxdg = "^0.26.0"
cryptography = "^2.7" cryptography = "^2.7"
passlib = "^1.7" passlib = "^1.7"