From 756fe56dc06097e8646e2b76f7038837752b4df1 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Wed, 20 Nov 2019 20:57:22 +1100 Subject: [PATCH 1/4] package with setuptools --- README.md | 5 +- __init__.py | 0 dist/pocketsnack-2.0.1.tar.gz | Bin 0 -> 4889 bytes pocketsnack.egg-info/PKG-INFO | 167 ++++++++++++++++++++++ pocketsnack.egg-info/SOURCES.txt | 8 ++ pocketsnack.egg-info/dependency_links.txt | 1 + pocketsnack.egg-info/entry_points.txt | 3 + pocketsnack.egg-info/requires.txt | 1 + pocketsnack.egg-info/top_level.txt | 1 + setup.py | 27 ++++ 10 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 __init__.py create mode 100644 dist/pocketsnack-2.0.1.tar.gz create mode 100644 pocketsnack.egg-info/PKG-INFO create mode 100644 pocketsnack.egg-info/SOURCES.txt create mode 100644 pocketsnack.egg-info/dependency_links.txt create mode 100644 pocketsnack.egg-info/entry_points.txt create mode 100644 pocketsnack.egg-info/requires.txt create mode 100644 pocketsnack.egg-info/top_level.txt create mode 100644 setup.py diff --git a/README.md b/README.md index 61f5e77..331795a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # pocket-snack + When your Pocket list is overwhelming, pocket-snack lets you see just what you can read today This is the version 2 README. If you haven't yet upgraded you can still use the [version 1 README](v1_README.md). @@ -8,9 +9,11 @@ This is the version 2 README. If you haven't yet upgraded you can still use the All commands have changed since version 1 - read the _Usage_ section carefully. This was necessary in order to provide better functionality without making the code too confusing. One of the changes is that the `refresh` command from version 1 no longer exists. This is so that `--since`, and `--before` can be used with both `--stash` and `lucky_dip`. If you want replicate `refresh` you simply need to run `--stash` followed by `--lucky_dip`. From the command line you could do: + ```bash pocketsnack -s && pocketsnack -d ``` + The automation of `pocketsnack refresh` has _also_ been removed. This didn't really work very consistently, and was causing a lot of maintenance headaches. I'm looking at how to bring it back in a different way, but for now it's been removed. ## Getting started @@ -147,4 +150,4 @@ Don't like _pocket-snack_ any more or want to re-install it in a new directory? ## Bugs and suggestions -Please log an issue - but check the existing issues first in case it's already been/being dealt with. \ No newline at end of file +Please log an issue - but check the existing issues first in case it's already been/being dealt with. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dist/pocketsnack-2.0.1.tar.gz b/dist/pocketsnack-2.0.1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..14e9e796587db843f33d98c8d37af5d28cac7bd0 GIT binary patch literal 4889 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 X>o3EP|1U3assuVzqlq=33;_5a?_+~x literal 0 HcmV?d00001 diff --git a/pocketsnack.egg-info/PKG-INFO b/pocketsnack.egg-info/PKG-INFO new file mode 100644 index 0000000..7c1ca63 --- /dev/null +++ b/pocketsnack.egg-info/PKG-INFO @@ -0,0 +1,167 @@ +Metadata-Version: 2.1 +Name: pocketsnack +Version: 2.0.1 +Summary: When your Pocket list is overwhelming, pocket-snack lets you see just what you can read today. A script for managing your getpocket.com list. +Home-page: https://github.com/hughrun/pocket-snack +Author: Hugh Rundle +Author-email: hugh@hughrundle.net +License: UNKNOWN +Description: # pocket-snack + + When your Pocket list is overwhelming, pocket-snack lets you see just what you can read today + + This is the version 2 README. If you haven't yet upgraded you can still use the [version 1 README](v1_README.md). + + ## A note on version 2 + + All commands have changed since version 1 - read the _Usage_ section carefully. This was necessary in order to provide better functionality without making the code too confusing. + + One of the changes is that the `refresh` command from version 1 no longer exists. This is so that `--since`, and `--before` can be used with both `--stash` and `lucky_dip`. If you want replicate `refresh` you simply need to run `--stash` followed by `--lucky_dip`. From the command line you could do: + + ```bash + pocketsnack -s && pocketsnack -d + ``` + + The automation of `pocketsnack refresh` has _also_ been removed. This didn't really work very consistently, and was causing a lot of maintenance headaches. I'm looking at how to bring it back in a different way, but for now it's been removed. + + ## Getting started + + ### Quick version + + 1. make sure you have installed Python 3 and it is callable with `python3` + 2. copy `settings-example.py` to `settings.py` + 3. create Pocket app and paste consumer key into `settings.py` + 4. run `bash install.sh` and follow the prompts + + ### Dependencies + + You will need Python 3.x installed and it must be called by `python3`, rather than `python`. These instructions, and the install script, assume you are using a Unix-like (Linux, BSD, MacOS) operating system. Microsoft Windows is not currently supported. + + On MacOS the easiest thing to do is to [install Python 3 using Homebrew](https://docs.brew.sh/Homebrew-and-Python): `brew install python`. + + The install script should install the `requests` module for you when you run `bash install.sh`. If you prefer, you can install it manually using **pip**: `pip3 install requests` + + ### Settings + + You will need to copy **settings-example.py** to a new file called **settings.py** before you start. You can do this however you like, but from the command line you could use `cp settings-example.py settings.py`, and then edit it with a text editor like `nano`. + + You can adjust most settings, but the defaults in **settings-example.py** should be ok for most users. Check the comments in **settings.example.py** for an explanation of each setting. + + ### Creating a Pocket consumer key for your app + + 1. Log in to Pocket in a web browser + 2. Go to https://getpocket.com/developer and click 'CREATE NEW APP' + 3. Complete the form: you will need all permissions, and the platform should be 'Desktop (other)' + 4. Your new app will show a 'consumer key', which you need to paste into the first line in settings.py + + ### Pocket access token + + Pocket uses OAuth to confirm that your app has permission from your user account to do stuff in your account. This means you need to authorise the app before you can do anything else. Once you have copied you app consumer key into settings.py, when you run the `install.sh` bash script, this will run `authorise` for you. If you prefer to install manually, or want to change the Pocket account details, you should run `pocketsnack --authorise` to get your token (see below). + + You should now have a line at the bottom of settings.py saying something like `pocket_access_token = 'aa11bb-zz9900xx'` + + ## Usage + + To run commands, use `pocketsnack [command]`. + + ### -h, --help + + Outputs help for each command + + ## admin commands + + ### -t, --test + + Outputs the first article returned by a call to the API. Normally you will never need to use this. + + ### -u, --authorise + + This command has an 's', not a 'z', and the short version is a 'u', not an 'a'. + + You need this to authorise your app. This command is automatically run by `install.sh`. Everything else works exclusively on the command line, but _authorise_ needs to open a browser to complete the authorisation process, so you need to run this on a machine with a web browser. It will authorise your app with your user, wait for you to confirm that you have completed the authorisation (by typing 'done') and then add the token to `settings.py`. Use it if you want to change the Pocket account you are using with pocketsnack. + + ## action commands + + ### -d, --lucky_dip + + Returns items with the archive tag from the archive to the list, and removes the archive tag. The number of items returned is determined by `items_per_cycle` in `settings.py`. Note that if `num_videos` and `num_images` add up to more than `items_per_cycle`, _lucky_dip_ will only return the total specified in `items_per_cycle`. Videos take precedence. + + ### -p, --purge + + You can use **purge_tags** to clear all tags in your List, Archive, or both, excluding the `archive_tag` and any `retain_tags`. This is useful if you've been using the Aus GLAM Blogs Pocket tool or anything else that retains the original tags from articles. + + `--purge` requires a second argument: `--list`, `--archive`, or `--all`, depending on where you want to purge tags. + + **NOTE** that by design, `--purge` will process **all** items in your archive, not just items with the `archive_tag`. This may lead to miss-matches between the number returned by `--info --archive` and the number of items processed by `--purge --archive`. + + ### -s, --stash + + Adds the archive tag to everything in your list, and then archives them. Depending on the value of `ignore_faves` and `ignore_tags` in `settings.py`, and any before/since values, some items may be excluded and remain in the List. + + ## optional flags + + ### -a, --archive + + Used in combination with `--info`, this tells you how many items are in your archive and how many of them are 'long reads'. You can set the wordcount defining a long read in `settings.py`. Used with `--purge`, it purges tags on items in the archive. + + ### -l, --list + + Same as _archive_ but for your list instead of your archive. + + ### -b, -all + + For use with `--purge` - purge tags from _both_ the List and the Archive. + + ### -n, --since SINCE + + Restrict the current _action command_ to only items updated more recently than _SINCE_ number of days. + + ### -o, --before BEFORE + + Restrict the current _action command_ to only items updated less recently than _BEFORE_ number of days. + + ### What does 'updated' mean? + + The Pocket API does not store a value for the date an items was first added. The only value we can get is _since_, which is a timestamp updated every time there is an update made to an item via or equivalent to any `add` or `modify` [API action](https://getpocket.com/developer/docs/overview). This could be when it is added to the List, move to the archive, moved out of the archive back into the List, or has changes made to tags (even if that tag update results in no actual change - i.e. if `--purge` has been run against the item, regardless of whether it had any tags to begin with). + + ## examples + + Stash only items updated in the last 2 days: + + `pocketsnack --stash -n 2` + + Stash only items NOT updated in the last 7 days: + + `pocketsnack --stash -o 7` + + Purge tags on all items in the List that were updated in the last day: + + `pocketsnack -pln 1` + + Run lucky_dip: + + `pocketsnack --lucky_dip` + + Run lucky_dip but only choose from items last updated longer ago than one week: + + `pocketsnack -d -o 7` + + ## Uninstalling or moving to a new directory + + Don't like _pocket-snack_ any more or want to re-install it in a new directory? No problem, you will just need to do a little maintenance: + + 1. Delete the executable link - if you don't do this when re-installing in a different directory, running `pocketsnack` will fail because it will still be pointing at the old directory. + + `rm /usr/local/bin/pocketsnack` + + 2. Now you can safely delete the pocket-snack directory. + + ## Bugs and suggestions + + Please log an issue - but check the existing issues first in case it's already been/being dealt with. + +Platform: UNKNOWN +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: GPL-3.0-or-later +Classifier: Operating System :: OS Independent +Description-Content-Type: text/markdown diff --git a/pocketsnack.egg-info/SOURCES.txt b/pocketsnack.egg-info/SOURCES.txt new file mode 100644 index 0000000..aae0912 --- /dev/null +++ b/pocketsnack.egg-info/SOURCES.txt @@ -0,0 +1,8 @@ +README.md +setup.py +pocketsnack.egg-info/PKG-INFO +pocketsnack.egg-info/SOURCES.txt +pocketsnack.egg-info/dependency_links.txt +pocketsnack.egg-info/entry_points.txt +pocketsnack.egg-info/requires.txt +pocketsnack.egg-info/top_level.txt \ No newline at end of file diff --git a/pocketsnack.egg-info/dependency_links.txt b/pocketsnack.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/pocketsnack.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/pocketsnack.egg-info/entry_points.txt b/pocketsnack.egg-info/entry_points.txt new file mode 100644 index 0000000..a43f030 --- /dev/null +++ b/pocketsnack.egg-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +pocketsnack = pocketsnack:main_func + diff --git a/pocketsnack.egg-info/requires.txt b/pocketsnack.egg-info/requires.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/pocketsnack.egg-info/requires.txt @@ -0,0 +1 @@ +requests diff --git a/pocketsnack.egg-info/top_level.txt b/pocketsnack.egg-info/top_level.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/pocketsnack.egg-info/top_level.txt @@ -0,0 +1 @@ + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..a23315e --- /dev/null +++ b/setup.py @@ -0,0 +1,27 @@ +from setuptools import setup, find_packages + +with open("README.md", "r") as fh: + long_description = fh.read() + +setup( + name="pocketsnack", + version="2.0.1", + author="Hugh Rundle", + author_email="hugh@hughrundle.net", + description="When your Pocket list is overwhelming, pocket-snack lets you see just what you can read today. A script for managing your getpocket.com list.", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/hughrun/pocket-snack", + packages=find_packages(), + install_requires=["requests"], + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GPL-3.0-or-later", + "Operating System :: OS Independent" + ], + entry_points={ + 'console_scripts': [ + 'pocketsnack = pocketsnack:main_func' + ] + } +) \ No newline at end of file From 163944f1ec8d09dcdcf99179506bafcdacc3a9a7 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 22 Nov 2019 10:37:11 +1100 Subject: [PATCH 2/4] delete old shit from faulty setuptools attempt --- .gitignore | 10 +- README.md | 46 +- __init__.py | 0 dist/pocketsnack-2.0.1.tar.gz | Bin 4889 -> 0 bytes install.sh | 37 -- main.py | 207 -------- pocket_toolkit.py | 544 ---------------------- pocketsnack.egg-info/PKG-INFO | 167 ------- pocketsnack.egg-info/SOURCES.txt | 8 - pocketsnack.egg-info/dependency_links.txt | 1 - pocketsnack.egg-info/entry_points.txt | 3 - pocketsnack.egg-info/requires.txt | 1 - pocketsnack.egg-info/top_level.txt | 1 - settings-example.py | 13 - setup.py | 42 +- 15 files changed, 46 insertions(+), 1034 deletions(-) delete mode 100644 __init__.py delete mode 100644 dist/pocketsnack-2.0.1.tar.gz delete mode 100644 install.sh delete mode 100755 main.py delete mode 100755 pocket_toolkit.py delete mode 100644 pocketsnack.egg-info/PKG-INFO delete mode 100644 pocketsnack.egg-info/SOURCES.txt delete mode 100644 pocketsnack.egg-info/dependency_links.txt delete mode 100644 pocketsnack.egg-info/entry_points.txt delete mode 100644 pocketsnack.egg-info/requires.txt delete mode 100644 pocketsnack.egg-info/top_level.txt delete mode 100644 settings-example.py diff --git a/.gitignore b/.gitignore index b498d61..a05c3f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ settings.py -schedule.sh -week.plist -day.plist \ No newline at end of file + +# Setuptools distribution folder. +/dist/ + +# Python egg metadata, regenerated from source files by setuptools. +/*.egg-info +/*.egg \ No newline at end of file diff --git a/README.md b/README.md index 331795a..5ef7a81 100644 --- a/README.md +++ b/README.md @@ -18,37 +18,32 @@ The automation of `pocketsnack refresh` has _also_ been removed. This didn't rea ## Getting started -### Quick version +1. make sure you have installed Python 3 +2. download `pocketsnack` using git or the download link in **Releases** +3. move into the top `pocketsnack` directory +4. `pip3 install .` or if pip points to Python3, `pip install .` +5. Edit `settings/settings.py` -1. make sure you have installed Python 3 and it is callable with `python3` -2. copy `settings-example.py` to `settings.py` -3. create Pocket app and paste consumer key into `settings.py` -4. run `bash install.sh` and follow the prompts +### Installing Python 3 -### Dependencies - -You will need Python 3.x installed and it must be called by `python3`, rather than `python`. These instructions, and the install script, assume you are using a Unix-like (Linux, BSD, MacOS) operating system. Microsoft Windows is not currently supported. - -On MacOS the easiest thing to do is to [install Python 3 using Homebrew](https://docs.brew.sh/Homebrew-and-Python): `brew install python`. - -The install script should install the `requests` module for you when you run `bash install.sh`. If you prefer, you can install it manually using **pip**: `pip3 install requests` +You will need Python 3.x installed. On MacOS the easiest thing to do is to [install Python 3 using Homebrew](https://docs.brew.sh/Homebrew-and-Python): `brew install python`. ### Settings -You will need to copy **settings-example.py** to a new file called **settings.py** before you start. You can do this however you like, but from the command line you could use `cp settings-example.py settings.py`, and then edit it with a text editor like `nano`. +You will need to copy `settings/settings-example.py` to a new file - `settings/settings.py` before you start. You can do this however you like, but from the command line you could use `cp settings/settings-example.py settings/settings.py`, and then edit it with a text editor like `nano` or VS Code, but any text editor will do the job. -You can adjust most settings, but the defaults in **settings-example.py** should be ok for most users. Check the comments in **settings.example.py** for an explanation of each setting. +You can adjust most settings, but the defaults in `settings-example.py` should be ok for most users. Check the comments in `settings.example.py` for an explanation of each setting. ### Creating a Pocket consumer key for your app 1. Log in to Pocket in a web browser -2. Go to https://getpocket.com/developer and click 'CREATE NEW APP' -3. Complete the form: you will need all permissions, and the platform should be 'Desktop (other)' -4. Your new app will show a 'consumer key', which you need to paste into the first line in settings.py +2. Go to `https://getpocket.com/developer` and click 'CREATE NEW APP' +3. Complete the form: you will need all permissions, and the platform should be _Desktop (other)_ +4. Your new app will show a **consumer key**, which you need to paste into the first line in `settings.py` -### Pocket access token +### Authorising your app with a Pocket access token -Pocket uses OAuth to confirm that your app has permission from your user account to do stuff in your account. This means you need to authorise the app before you can do anything else. Once you have copied you app consumer key into settings.py, when you run the `install.sh` bash script, this will run `authorise` for you. If you prefer to install manually, or want to change the Pocket account details, you should run `pocketsnack --authorise` to get your token (see below). +Pocket uses OAuth to confirm that your app has permission from your user account to do stuff in your account. This means you need to authorise the app before you can do anything else. Once you have copied your app consumer key into settings.py, run `pocketsnack --authorise` to get your token. You should now have a line at the bottom of settings.py saying something like `pocket_access_token = 'aa11bb-zz9900xx'` @@ -70,7 +65,7 @@ Outputs the first article returned by a call to the API. Normally you will never This command has an 's', not a 'z', and the short version is a 'u', not an 'a'. -You need this to authorise your app. This command is automatically run by `install.sh`. Everything else works exclusively on the command line, but _authorise_ needs to open a browser to complete the authorisation process, so you need to run this on a machine with a web browser. It will authorise your app with your user, wait for you to confirm that you have completed the authorisation (by typing 'done') and then add the token to `settings.py`. Use it if you want to change the Pocket account you are using with pocketsnack. +You need this to authorise your app. Everything else works exclusively on the command line, but _authorise_ needs to open a browser to complete the authorisation process, so you need to run this on a machine with a web browser. It will authorise your app with your user, wait for you to confirm that you have completed the authorisation (by typing 'done') and then add the token to `settings.py`. You also need to run `--authorise` if you want to change the Pocket account you are using with `pocketsnack`. ## action commands @@ -140,13 +135,16 @@ Run lucky_dip but only choose from items last updated longer ago than one week: ## Uninstalling or moving to a new directory -Don't like _pocket-snack_ any more or want to re-install it in a new directory? No problem, you will just need to do a little maintenance: +### If you installed with pip + +Just run `pip uninstall pocketsnack` or `pip3 uninstall pocketsnack`. -1. Delete the executable link - if you don't do this when re-installing in a different directory, running `pocketsnack` will fail because it will still be pointing at the old directory. +### If you installed using the legacy install.sh script - `rm /usr/local/bin/pocketsnack` +1. Delete the executable link: `rm /usr/local/bin/pocketsnack` -2. Now you can safely delete the pocket-snack directory. +If you don't do this when re-installing in a different directory, running `pocketsnack` will fail because it will still be pointing at the old directory. +2. Now you can safely delete the pocket-snack directory: `rm -r pocketsnack` ## Bugs and suggestions diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/dist/pocketsnack-2.0.1.tar.gz b/dist/pocketsnack-2.0.1.tar.gz deleted file mode 100644 index 14e9e796587db843f33d98c8d37af5d28cac7bd0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4889 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 X>o3EP|1U3assuVzqlq=33;_5a?_+~x diff --git a/install.sh b/install.sh deleted file mode 100644 index 5f0ba75..0000000 --- a/install.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -set -e # throw for all errors instead of continuing - -echo 'Making executable..' -chmod +x main.py -echo 'Linking to local binary directory..' -if [ ! -h /usr/local/bin/pocketsnack ] ; then # check whether symlink already exists - ln -s $(pwd)/main.py /usr/local/bin/pocketsnack -fi -echo 'checking for Python3...' -if [ $(which python3) ]; then - echo 'Python 3 is installed and python3 command works' - pip3 install requests >/dev/null - echo 'Installation complete ✅ You can now get started!' - echo '-------------------------------------------------' - # check if user wants to just authorise straight away - echo 'Do you want to authorise your app with Pocket now? Choose 1 for Yes or 2 for No' - select response in Yes No - do - case $response in - Yes) - echo 'Authorise pocketsnack in the browser window that opens in a moment, then return to this command line to finish setting up.' - sleep 5s - pocketsnack authorise - ;; - No) - echo 'Run "pocketsnack authorise" when you are ready to authorise your app.' - exit 0 - ;; - *) - echo 'Enter "1" to authorise or "2" to exit.' - ;; - esac - done -else - echo '❌ Python 3 must be installed and called by "python3"' -fi \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100755 index 08213b8..0000000 --- a/main.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env python3 - -# pocket-snack - a Python3 tool to help you retain your sanity when using your Pocket account - -# Copyright (C) 2019 Hugh Rundle - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# You can contact Hugh on Mastodon at @hugh@ausglam.space -# or Twitter at @hughrundle -# or email hugh [at] hughrundle [dot] net - -# ---------------- -# Import libraries -# ---------------- - -from argparse import ArgumentParser - -# bundled with Python -import os -import subprocess - -# local modules -import settings -import pocket_toolkit as pt - -# ---------------- -# Settings -# ---------------- - -# TODO: clean this up - -# assign short variable names from the settings file -consumer_key = settings.pocket_consumer_key -redirect_uri = settings.pocket_redirect_uri -archive_tag = settings.archive_tag - -# settings dict for other vars -refresh_settings = [ - consumer_key, - settings.pocket_access_token, - archive_tag, - settings.replace_all_tags, - settings.retain_tags, - settings.ignore_faves, - settings.ignore_tags, - settings.items_per_cycle, - settings.num_videos, - settings.num_images, - settings.num_longreads, - settings.longreads_wordcount -] - -# ---------------- -# argparser arguments -# ---------------- - -parser = ArgumentParser(description='\033[1;36mpocketsnack: a command line tool for decluttering your Pocket account\033[1;m') -admin = parser.add_argument_group('admin commands') -actions = parser.add_argument_group('action commands') -mex = parser.add_mutually_exclusive_group() -timers = parser.add_mutually_exclusive_group() - -mex.add_argument( - "-a", "--archive", action="store_true", help="get information on TBR items in archive (with -i) or purge tags in archive (with -p)" -) -mex.add_argument( - "-b", "--all", action="store_true", help="purge all tags in both list and archive (with -p)" -) -actions.add_argument( - "-d", "--lucky_dip", action="store_true", help="move random items tagged 'tbr' from archive to list, depending on settings" -) -actions.add_argument( - "-i", "--info", action="store_true", help="get information on items in list or TBR items in archive" -) -mex.add_argument( - "-l", "--list", action="store_true", help="get information on items in list (with -i) or purge tags in list (with -p)" -) -timers.add_argument( - "-n", "--since", type=int, help="only act on items where last activity is newer than a given number of days. Use with any action command" -) -timers.add_argument( - "-o", "--before", type=int, help="only act on items where last activity is older than a given number of days. Use with any action command" -) -actions.add_argument( - "-p", "--purge", action="store_true", help="remove all tags from list, archive, or both, depending on the second argument provided and excepting tags listed in 'retain_tags' in settings" -) -actions.add_argument( - "-s", "--stash", action="store_true", help="add 'tbr' tag to all items in user list and archive them, with exceptions as per settings" -) -admin.add_argument( - "-t", "--test", action="store_true", help="test whether API call returns data" -) -admin.add_argument( - "-u", "--authorise", action="store_true", help="authorise app to connect to a Pocket account" -) - -options = parser.parse_args() - -# ---------------- -# What happens with each combination? -# ---------------- - -if __name__ == '__main__': - - # Find all args that have a value other than False - # This helps with error messages for optional args - # that need to be used in combination with something else - true_vars = [] - orphans = ['list', 'archive', 'all', 'since', 'before'] - for x in vars(options): - if vars(options)[x]: - true_vars.append(x) - - if options.authorise: - # Run authorise once first to retrieve a pocket_access_token - auth = pt.authorise(consumer_key, redirect_uri) - print(auth) - - elif options.lucky_dip: - print('\033[0;36mRunning lucky dip...\033[0;m') - dip = pt.lucky_dip(consumer_key, settings.pocket_access_token, settings.archive_tag, settings.items_per_cycle, settings.num_videos, settings.num_images, settings.num_longreads, settings.longreads_wordcount, options.before, options.since) - print('\033[0;36m' + dip + '\033[0;m') - - elif options.info: - - def print_info(response, collection): - items = str(len(response)) - longreads = 0 - for item in response: - # is it a long read? - if 'word_count' in response[item]: - words = int(response[item]['word_count']) - longread = True if words > settings.longreads_wordcount else False - else: - longread = False - if longread: - longreads += 1 - - if options.before: - print(collection + 'has ' + items + ' items ' + 'updated prior to ' + str(options.before) + ' days ago and ' + str(longreads) + ' are longreads.') - elif options.since: - print(collection + 'has ' + items + ' items ' + 'updated since ' + str(options.since) + ' days ago and ' + str(longreads) + ' are longreads.') - else: - print(collection + 'has ' + items + ' items and ' + str(longreads) + ' are longreads.') - - if options.archive: - response = pt.info(consumer_key, settings.pocket_access_token, archive_tag, options.before, options.since) - if len(response) > 0: - print_info(response, 'The TBR archive ') - else: - print('No items match that query') - - elif options.list: - response = pt.info(consumer_key, settings.pocket_access_token, False, options.before, options.since) - if len(response) > 0: - print_info(response, 'The user List ') - else: - print('No items match that query') - - else: - print('\n \033[0;36m--info\033[0;m requires a second argument (-a or -l). Check \033[0;36mpocketsnack --help\033[0;m for more information\n') - - elif options.purge: - - if options.list: - print('\033[0;36mPurging tags in the list\033[0;m') - purge = pt.purge_tags('unread', settings.retain_tags, archive_tag, consumer_key, settings.pocket_access_token, options.before, options.since) - print(purge) - - elif options.archive: - print('\033[0;36mPurging tags in the archive\033[0;m') - purge = pt.purge_tags('archive', settings.retain_tags, archive_tag, consumer_key, settings.pocket_access_token, options.before, options.since) - print(purge) - - elif options.all: - print('\033[0;36mPurging tags in both the archive and the list\033[0;m') - purge = pt.purge_tags('all', settings.retain_tags, archive_tag, consumer_key, settings.pocket_access_token, options.before, options.since) - print(purge) - - else: - print('\n \033[0;36m--purge\033[0;m requires a second argument (-a, -l or -b). Check \033[0;36mpocketsnack --help\033[0;m for more information\n') - - elif options.stash: - stash = pt.stash(consumer_key, settings.pocket_access_token, archive_tag, settings.replace_all_tags, settings.retain_tags, settings.ignore_faves, settings.ignore_tags, options.before, options.since) - print('\033[0;36m' + stash + '\033[0;m') - - elif options.test: - result = pt.test(consumer_key, settings.pocket_access_token) - print(result) - - elif set(true_vars).intersection(orphans): - print('\n That command cannot be used by itself. Check \033[0;36mpocketsnack --help\033[0;m for more information\n') - - else: - print('\033[0;36mpocketsnack\033[0;m requires commands and/or flags to do anything useful. Try \033[0;36mpocketsnack -h\033[0;m for more information') \ No newline at end of file diff --git a/pocket_toolkit.py b/pocket_toolkit.py deleted file mode 100755 index 3960185..0000000 --- a/pocket_toolkit.py +++ /dev/null @@ -1,544 +0,0 @@ -# pocket-toolkit - a collection of functions to manage your Pocket account - -# Copyright (C) 2019 Hugh Rundle - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# You can contact Hugh on Mastodon at @hugh@ausglam.space -# or Twitter at @hughrundle -# or email hugh [at] hughrundle [dot] net - -# ---------------- -# Import libraries -# ---------------- - -import requests - -# bundled with Python -from datetime import datetime, time, timedelta -import fileinput -import json -import random -import re -import socket -import time -import urllib -import webbrowser - -# local modules -import settings - -# ---------------- -# Create app -# ---------------- - -# Log in to Pocket in a web browser -# Go to https://getpocket.com/developer and click 'CREATE NEW APP' -# Complete the form: you will need all permissions, and the platform should be 'Desktop (other)' -# Your new app will show a 'consumer key', which you need to paste into the first line in settings.py - -# ----------------- -# reusable functions -# ----------------- - -# (TODO: make this a proper class object) -# Pocket expects particular HTTP headers to send and receive JSON -headers = {"Content-Type": "application/json; charset=UTF-8", "X-Accept": "application/json"} - -# send GET requests -def get(params): - return requests.post('https://getpocket.com/v3/get', headers=headers, params=params) - -# send POST requests -def send(actions_escaped, consumer_key, pocket_access_token): - # POST changes to tags - return requests.post('https://getpocket.com/v3/send?actions=' + actions_escaped + '&access_token=' + pocket_access_token + '&consumer_key=' + consumer_key) - -# check the internet connection is live -def connection_live(): - try: - socket.create_connection(("getpocket.com", 443), 10) - return True - except OSError: - pass - return False - -# make a unix timestamp for before/after flags with Pocket's 'since' param -def get_timestamp(since): - now = datetime.now() - delta = timedelta(days=since) - since_time = datetime.strftime(now - delta, '%c') - strptime = time.strptime(since_time) - return time.mktime(strptime) # return Unix timestamp - -def get_item_list(params, before, since): - if before: - all_items = get(params) - params['since'] = get_timestamp(before) - since_items = get(params) - # get non-intersection of 2 groups to get only items last changed 'before' - item_list = all_items.json()['list'] # everything - since_list = since_items.json()['list'] # only things since 'before' - if len(since_list) > 0: - for key in since_list.keys(): - item_list.pop(key, None) # remove everything from items_list that is in since_list - return item_list - elif since: - timestamp = get_timestamp(since) - params['since'] = timestamp - return get(params).json()['list'] - else: - return get(params).json()['list'] - -# -------------------- -# process tag updates -# -------------------- - -def process_items(actions, consumer_key, pocket_access_token): - # Update the tags - # group into smaller chunks of 20 to avoid a 414 (URL too long) error - tag_chunks = [actions[i:i+20] for i in range(0, len(actions), 20)] - - # process each chunk - for i, chunk in enumerate(tag_chunks): - - actions_string = json.dumps(chunk) - # now URL encode it using urllib - actions_escaped = urllib.parse.quote(actions_string) - print(' Processing ' + str(i*20) + ' to ' + str((i*20)+len(tag_chunks[i])) + ' of ' + str(len(actions)) + '...', end="", flush=True) # printing like this means the return callback is appended to the line - # post update to tags - update = send(actions_escaped, consumer_key, pocket_access_token) - if update.raise_for_status() == None: - print('\033[0;32mOk\033[0;m') # Print 'Ok' in green. - else: - print('\031[0;41mOh dear, something went wrong.\033[0;m') # Print error in red - time.sleep(2) # don't fire off requests too quickly - -# ---------------- -# Authorise -# ---------------- - -def authorise(consumer_key, redirect_uri): # With an 's'. Deal with it. - paramsOne = {"consumer_key": consumer_key, "redirect_uri": redirect_uri} - # set up step 1 request - this should return a 'code' aka 'request token' - requestOne = requests.post('https://getpocket.com/v3/oauth/request', headers=headers, params=paramsOne) - # get the JSON response and save the token to a param for the next step - request_token = requestOne.json()['code'] - # print the request token to the console so you know it happened - print('\033[0;36mYour request token (code) is \033[0;m' + request_token) - - # now you need to authorise the app in your Pocket account - # build the url - auth_url = 'https://getpocket.com/auth/authorize?request_token=' + request_token + '&redirect_uri=' + redirect_uri - # open the authorisation page in a new tab of your default web browser - webbrowser.open(auth_url, new=2) - - # We're not writing a server app here so we use a little hack to check whether the user has finished authorising before we continue - # Just wait for the user (you!) to indicate they have finished authorising the app - # the '\n' prints a new line - print('\033[0;36mAuthorise your app in the browser tab that just opened.\033[0;m') - user_input = input('Type "done" when you have finished authorising the app in Pocket \n>>') - - if user_input == "done": - # now we can continue - # do a new request, this time to the oauth/authorize endpoint with the same JSON headers, but also sending the code as a param - paramsTwo = {"consumer_key": consumer_key, "code": request_token} - requestTwo = requests.post('https://getpocket.com/v3/oauth/authorize', headers=headers, params=paramsTwo) - # get the JSON response as a Python dictionary and call it 'res'. - res = requestTwo.json() - # Finally we have the access token! - print('\033[0;36mAccess token for ' + res['username'] + ' is \033[0;m' + res['access_token']) - # Assign the access token to a parameter called access_token - access_token = res['access_token'] - # replace the pocket_access_token line rather than just adding an extra at the end - settings_file = fileinput.FileInput("settings.py", inplace=True) - repl = "pocket_access_token = " + "'" + access_token + "'" - for line in settings_file: - line = re.sub('(pocket_access_token)+.*', repl, line) - print(line.rstrip()) - return '\033[0;36mToken added to settings.py - you are ready to use pocketsnack.\033[0;m' - -# ------------------------------ -# Read info about Pocket account -# ------------------------------ - -def info(consumer_key, pocket_access_token, archive_tag, before, since): - - params = { - "consumer_key": consumer_key, - "access_token": pocket_access_token, - } - if archive_tag: - # state is archive & use archive tag - params['state'] = 'archive' - params['tag'] = archive_tag - else: - # state is unread - params['state'] = 'unread' - - items = get_item_list(params, before, since) - return items - -# choose items to put back into the user List -def lucky_dip(consumer_key, pocket_access_token, archive_tag, items_per_cycle, num_videos, num_images, num_longreads, longreads_wordcount, before, since): - - def run_lucky_dip(attempts): - if connection_live() == True: - # This function is called below to un-archive everything in the selection - def readd(selection): - actions = [] - for item in selection: - item_readd = {"item_id": item, "action": "readd"} - actions.append(item_readd) - # remove archive_tag as well, otherwise the item will keep appearing after it's read and archived by the user - item_detag = {"item_id": item, "action": "tags_remove", "tags": archive_tag} - actions.append(item_detag) - # stringify - items_string = json.dumps(actions) - escaped = urllib.parse.quote(items_string) - # re-add items - return send(escaped, consumer_key, pocket_access_token) - - # get everything in the archive with the archive_tag - params = {"consumer_key": consumer_key, "access_token": pocket_access_token, "state": "archive", "tag": archive_tag} - - tbr = get_item_list(params, before, since) - - # before we go any further, make sure there actually is something in the TBR list! - if len(tbr) > 0: - available = len(tbr) # set this value here before it gets changed - # just to make it clearer, and create a copy - items_needed = items_per_cycle - - # we'll use this to report what was added - chosen = {} - ##################### - # filtering formats - ##################### - videos = [] - images = [] - - for item in tbr: - if num_videos and 'has_video' in tbr[item]: - if tbr[item]['has_video'] == '2': - # it's a video - videos.append(item) - if num_images and 'has_image' in tbr[item]: - if tbr[item]['has_image'] == '2': - # it's an image - images.append(item) - - if num_videos: - # don't select more than the total items_needed - required_videos = items_needed if items_needed <= num_videos else num_videos - selected_videos = videos if len(videos) <= required_videos else random.sample(videos, required_videos) - # re-add the videos - if len(selected_videos) > 0: - readd(selected_videos) - # adjust the total items we need to look for - items_needed -= len(selected_videos) - chosen['videos'] = len(selected_videos) - - if num_images: - required_images = num_images if num_images <= items_needed else items_needed - selected_images = images if len(images) <= required_images else random.sample(images, required_images) - if len(selected_images) > 0: - readd(selected_images) - # adjust the total items we need to look for - items_needed -= len(selected_images) - chosen['images'] = len(selected_images) - - ##################### - # filtering longreads - ##################### - if num_longreads: - long_reads = [] - for item in tbr: - # is it a long read? - if 'word_count' in tbr[item]: - words = int(tbr[item]['word_count']) - if words > longreads_wordcount: - long_reads.append(item) # we only need to append the item_id - - total_longreads = len(long_reads) - total_shortreads = len(tbr) - total_longreads - required_shortreads = items_needed - num_longreads - enough_longreads = total_longreads >= num_longreads - enough_shortreads = total_shortreads >= required_shortreads - - # if there are enough longreads AND enough shortreads, go ahead - if enough_longreads and enough_shortreads: - # select random longreads - # note we need to check whether any are actually required at this point, otherwise we run the risk of items_needed becoming a negative number - selected_longreads = (random.sample(long_reads, num_longreads)) if items_needed > 0 else [] - # add random shortreads - # first we need to remove the longreads from tbr: - for article in long_reads: - tbr.pop(article, None) - # now grab a random selection to fill our items_needed quota - selected_shortreads = random.sample(list(tbr), required_shortreads) if required_shortreads > 0 else [] - # add the two lists together - selection = selected_longreads + selected_shortreads - chosen['longreads'] = len(selected_longreads) - chosen['shortreads'] = len(selected_shortreads) - readd(selection) - # If there are too few longreads but enough shortreads, take what LR we have and make up the difference - elif enough_shortreads: - # select all of the longreads - selected_longreads = long_reads - # add random shortreads - # first we need to remove the longreads from tbr: - for article in long_reads: - tbr.pop(article, None) - # now grab a random selection to fill our items_needed quota - difference = items_needed - len(selected_longreads) - # make sure you have enough of the new total - enough_difference = difference <= total_shortreads - if enough_difference: - selected_shortreads = random.sample(list(tbr), difference) if difference > 0 else [] - # if there are too few longreads AND too few shortreads just grab what shortreads you have - else: - selected_shortreads = list(tbr) # note this is the new tbr after we popped the longreads - # add the two lists together - selection = selected_longreads + selected_shortreads - chosen['longreads'] = len(selected_longreads) - chosen['shortreads'] = len(selected_shortreads) - readd(selection) - # if there are enough longreads but too few shortreads, use all the shortreads and make up the difference with longreads - elif enough_longreads: - # add all the shortreads there are - # first we need to remove the longreads from tbr: - for article in long_reads: - tbr.pop(article, None) - # the new tbr is now entirely shortreads - selected_shortreads = list(tbr) - # now grab a random selection of long reads to fill our items_needed quota - difference = items_needed - len(selected_shortreads) - # make sure you have enough of the new total - enough_difference = difference <= total_longreads - if enough_difference: - selected_longreads = random.sample(long_reads, difference) if difference > 0 else [] - # if there are too few longreads AND too few shortreads just grab what longreads you have - else: - selected_longreads = long_reads - # add the two lists together - selection = selected_longreads + selected_shortreads - chosen['longreads'] = len(selected_longreads) - chosen['shortreads'] = len(selected_shortreads) - readd(selection) - else: - # if we get to here there aren't enough of either, so we should just return everything - chosen['longreads'] = total_longreads - chosen['shortreads'] = total_shortreads - readd(list(tbr)) - else: # if num_longreads is False or 0 (which are the same thing) - # check how many items in total there are in the TBR list - # if there are fewer than items_needed, just return all of them, otherwise get a random selection - if items_needed > 0: - selection = list(tbr) if len(tbr) < items_needed else random.sample(list(tbr), items_needed) - chosen['random'] = len(selection) - readd(selection) - # return a total of what was moved into the list (faves, format etc) - tot_videos = chosen['videos'] if 'videos' in chosen else None - tot_images = chosen['images'] if 'images' in chosen else None - random_choice = 'random' in chosen - tot_added = 0 - for v in chosen.values(): - tot_added += v - remaining = available - tot_added - completed_message = 'Success! ' + str(tot_added) + ' items added to your reading list, including ' - if random_choice: - completed_message += str(chosen['random']) + ' random articles, ' - if tot_images: - completed_message += str(tot_images) + ' images, ' - if tot_videos: - completed_message += str(tot_videos) + ' videos, ' - if not random_choice: - completed_message += str(chosen['longreads']) + ' long reads and ' + str(chosen['shortreads']) + ' short reads, ' - # add this to the end regardless - caveat = '' - if before: - caveat = 'last updated earlier than ' + str(before) + ' days ago ' - if since: - caveat = 'last updated more recently than ' + str(since) + ' days ago ' - completed_message += 'with ' + str(remaining) + ' other items ' + caveat + 'remaining to be read.' - return completed_message - # else if there's nothing tagged with the archive_tag - else: - return 'Nothing to be read!' - else: - if attempts < 4: - attempts += 1 - time.sleep(10) - print('\033[0;36mAttempting to connect...\033[0;m') - return run_lucky_dip(attempts) - else: - msg = "\033[0;36mSorry, no connection after 4 attempts.\033[0;m" - return msg - - return run_lucky_dip(0) - -# ----------------- -# purge tags -# ----------------- - -def purge_tags(state, retain_tags, archive_tag, consumer_key, pocket_access_token, before, since): - - params = { - "consumer_key": consumer_key, - "access_token": pocket_access_token, - "state": state, - "detailType": "complete" - } - - # check we're online - if connection_live() == True: - # GET the list - request = get_item_list(params, before, since) - actions = [] - - if len(request) > 0: - for item in request: - item_tags = [] - # find the item tags - if 'tags' in request[item]: - for tag in request[item]['tags']: - item_tags.append(tag) - # keep any retain_tags like we use in stash - retain_tags.add(archive_tag) # we don't want to wipe out the archive tag on archived items! - update = {"item_id": item} - intersect = list(retain_tags.intersection(item_tags)) - if len(intersect) > 0: - update['action'] = 'tags_replace' # item is the ID because it's the dict key - update["tags"] = intersect # update tags to keep the retain_tags - # otherwise just clear all tags - else: - update['action'] = 'tags_clear' # item is the ID because it's the dict key - actions.append(update) - - process_items(actions, consumer_key, pocket_access_token) - return '\033[1;36mUndesirable elements have been purged.\033[1;m' - - else: - return '\033[0;36mNo items from which to purge tags.\033[0;m' - -""" -Stash - -stash(consumer_key, pocket_access_token, archive_tag [, retain_tags] [, favorite]) - -Stash applies the archive_tag to all items in the User List, then archives everything in the List. - -Options: - -retain_tags - a list of tags that should not be removed from items when adding the archive_tag. If you don't want to retain any tags, this value should be False. Defaults to False -favorite - boolean indicating whether to ignore (i.e. leave in the user list) favorite items. Defaults to True - -""" - -# ----------------- -# stash items -# ----------------- - -def stash(consumer_key, pocket_access_token, archive_tag, replace_all_tags, retain_tags, favorite, ignore_tags, before, since): - print('\033[0;36mStashing items...\033[0;m') - # if ignore_faves is set to True, don't get favorite items - params = {"consumer_key": consumer_key, "access_token": pocket_access_token, "detailType": "complete", "state": "unread"} - if favorite: - params['favorite'] = "0" - print('\033[0;36mSkipping favorited items...\033[0;m') - - def run_stash(attempts): - if connection_live() == True: - # GET the list - item_list = get_item_list(params, before, since) - # we store all the 'actions' in an array, then send one big HTTP request to the Pocket API - actions = [] - # copy items_list so we can alter the copy whilst iterating through the original - items_to_stash = dict(item_list) - for item in item_list: - item_tags = [] - if 'tags' in item_list[item]: - for tag in item_list[item]['tags']: - item_tags.append(tag) - - # filter out any items with the ignore tags before dealing with the rest - if len(ignore_tags) > 0 and len(ignore_tags.intersection(item_tags)) > 0: - # pop it out of the items_to_stash - items_to_stash.pop(item, None) - # Now we process all the tags first, before we archive everything - elif replace_all_tags: - # set up the action dict - action = {"item_id": item, "action": "tags_replace"} # item is the ID because it's the dict key - # are we retaining any tags? - if retain_tags: # retain_tags should either be False or a Set - # find the common tags between retain_tags and item_tags - # to do this we need retain_tags to be a set, but you can't JSON serialise a set, so we need to turn the result into a list afterwards - tags_to_keep = list(retain_tags.intersection(item_tags)) - # don't forget to add the archive_tag! - tags_to_keep.append(archive_tag) - action["tags"] = tags_to_keep - # Anything that is still in the user list can be presumed to not have been read - # when they read it they will archive it (without the archive_tag because lucky_dip removes it) - else: - action["tags"] = archive_tag - actions.append(action) - else: # if replace_all_tags is False, just add the archive tag without removing any tags - action = {"item_id": item, "action": "tags_add"} # add new tag rather than replacing all of them - action["tags"] = archive_tag - actions.append(action) - - # Update the tags - process_items(actions, consumer_key, pocket_access_token) - - # Now archive everything - archive_actions = [] - - for item in items_to_stash: - item_action = {"item_id": item, "action": "archive"} - archive_actions.append(item_action) - - print('\033[0;36mArchiving ' + str(len(archive_actions)) + ' items...\033[0;m') - - # archive items - process_items(archive_actions, consumer_key, pocket_access_token) - - # return a list of what was stashed and, if relevant, what wasn't - skipped_items = len(item_list) - len(items_to_stash) - return str(len(items_to_stash)) + ' items archived with "' + archive_tag + '" and ' + str(skipped_items) + ' items skipped due to retain tag.' - else: - if attempts < 4: - attempts += 1 - time.sleep(10) - print('\033[0;36mAttempting to connect...\033[0;m') - return run_stash(attempts) - else: - msg = "\033[0;31mSorry, no connection after 4 attempts.\033[0;m" - return msg - - return run_stash(0) - -# ----------------- -# test -# ----------------- - -def test(consumer_key, pocket_access_token): - - params = {"consumer_key": consumer_key, "access_token": pocket_access_token, "state": "archive", "count": "1", "detailType": "complete"} - - # GET the list - request = get(params) - list_items = request.json() - - return json.dumps(list_items, indent=4, default=str) \ No newline at end of file diff --git a/pocketsnack.egg-info/PKG-INFO b/pocketsnack.egg-info/PKG-INFO deleted file mode 100644 index 7c1ca63..0000000 --- a/pocketsnack.egg-info/PKG-INFO +++ /dev/null @@ -1,167 +0,0 @@ -Metadata-Version: 2.1 -Name: pocketsnack -Version: 2.0.1 -Summary: When your Pocket list is overwhelming, pocket-snack lets you see just what you can read today. A script for managing your getpocket.com list. -Home-page: https://github.com/hughrun/pocket-snack -Author: Hugh Rundle -Author-email: hugh@hughrundle.net -License: UNKNOWN -Description: # pocket-snack - - When your Pocket list is overwhelming, pocket-snack lets you see just what you can read today - - This is the version 2 README. If you haven't yet upgraded you can still use the [version 1 README](v1_README.md). - - ## A note on version 2 - - All commands have changed since version 1 - read the _Usage_ section carefully. This was necessary in order to provide better functionality without making the code too confusing. - - One of the changes is that the `refresh` command from version 1 no longer exists. This is so that `--since`, and `--before` can be used with both `--stash` and `lucky_dip`. If you want replicate `refresh` you simply need to run `--stash` followed by `--lucky_dip`. From the command line you could do: - - ```bash - pocketsnack -s && pocketsnack -d - ``` - - The automation of `pocketsnack refresh` has _also_ been removed. This didn't really work very consistently, and was causing a lot of maintenance headaches. I'm looking at how to bring it back in a different way, but for now it's been removed. - - ## Getting started - - ### Quick version - - 1. make sure you have installed Python 3 and it is callable with `python3` - 2. copy `settings-example.py` to `settings.py` - 3. create Pocket app and paste consumer key into `settings.py` - 4. run `bash install.sh` and follow the prompts - - ### Dependencies - - You will need Python 3.x installed and it must be called by `python3`, rather than `python`. These instructions, and the install script, assume you are using a Unix-like (Linux, BSD, MacOS) operating system. Microsoft Windows is not currently supported. - - On MacOS the easiest thing to do is to [install Python 3 using Homebrew](https://docs.brew.sh/Homebrew-and-Python): `brew install python`. - - The install script should install the `requests` module for you when you run `bash install.sh`. If you prefer, you can install it manually using **pip**: `pip3 install requests` - - ### Settings - - You will need to copy **settings-example.py** to a new file called **settings.py** before you start. You can do this however you like, but from the command line you could use `cp settings-example.py settings.py`, and then edit it with a text editor like `nano`. - - You can adjust most settings, but the defaults in **settings-example.py** should be ok for most users. Check the comments in **settings.example.py** for an explanation of each setting. - - ### Creating a Pocket consumer key for your app - - 1. Log in to Pocket in a web browser - 2. Go to https://getpocket.com/developer and click 'CREATE NEW APP' - 3. Complete the form: you will need all permissions, and the platform should be 'Desktop (other)' - 4. Your new app will show a 'consumer key', which you need to paste into the first line in settings.py - - ### Pocket access token - - Pocket uses OAuth to confirm that your app has permission from your user account to do stuff in your account. This means you need to authorise the app before you can do anything else. Once you have copied you app consumer key into settings.py, when you run the `install.sh` bash script, this will run `authorise` for you. If you prefer to install manually, or want to change the Pocket account details, you should run `pocketsnack --authorise` to get your token (see below). - - You should now have a line at the bottom of settings.py saying something like `pocket_access_token = 'aa11bb-zz9900xx'` - - ## Usage - - To run commands, use `pocketsnack [command]`. - - ### -h, --help - - Outputs help for each command - - ## admin commands - - ### -t, --test - - Outputs the first article returned by a call to the API. Normally you will never need to use this. - - ### -u, --authorise - - This command has an 's', not a 'z', and the short version is a 'u', not an 'a'. - - You need this to authorise your app. This command is automatically run by `install.sh`. Everything else works exclusively on the command line, but _authorise_ needs to open a browser to complete the authorisation process, so you need to run this on a machine with a web browser. It will authorise your app with your user, wait for you to confirm that you have completed the authorisation (by typing 'done') and then add the token to `settings.py`. Use it if you want to change the Pocket account you are using with pocketsnack. - - ## action commands - - ### -d, --lucky_dip - - Returns items with the archive tag from the archive to the list, and removes the archive tag. The number of items returned is determined by `items_per_cycle` in `settings.py`. Note that if `num_videos` and `num_images` add up to more than `items_per_cycle`, _lucky_dip_ will only return the total specified in `items_per_cycle`. Videos take precedence. - - ### -p, --purge - - You can use **purge_tags** to clear all tags in your List, Archive, or both, excluding the `archive_tag` and any `retain_tags`. This is useful if you've been using the Aus GLAM Blogs Pocket tool or anything else that retains the original tags from articles. - - `--purge` requires a second argument: `--list`, `--archive`, or `--all`, depending on where you want to purge tags. - - **NOTE** that by design, `--purge` will process **all** items in your archive, not just items with the `archive_tag`. This may lead to miss-matches between the number returned by `--info --archive` and the number of items processed by `--purge --archive`. - - ### -s, --stash - - Adds the archive tag to everything in your list, and then archives them. Depending on the value of `ignore_faves` and `ignore_tags` in `settings.py`, and any before/since values, some items may be excluded and remain in the List. - - ## optional flags - - ### -a, --archive - - Used in combination with `--info`, this tells you how many items are in your archive and how many of them are 'long reads'. You can set the wordcount defining a long read in `settings.py`. Used with `--purge`, it purges tags on items in the archive. - - ### -l, --list - - Same as _archive_ but for your list instead of your archive. - - ### -b, -all - - For use with `--purge` - purge tags from _both_ the List and the Archive. - - ### -n, --since SINCE - - Restrict the current _action command_ to only items updated more recently than _SINCE_ number of days. - - ### -o, --before BEFORE - - Restrict the current _action command_ to only items updated less recently than _BEFORE_ number of days. - - ### What does 'updated' mean? - - The Pocket API does not store a value for the date an items was first added. The only value we can get is _since_, which is a timestamp updated every time there is an update made to an item via or equivalent to any `add` or `modify` [API action](https://getpocket.com/developer/docs/overview). This could be when it is added to the List, move to the archive, moved out of the archive back into the List, or has changes made to tags (even if that tag update results in no actual change - i.e. if `--purge` has been run against the item, regardless of whether it had any tags to begin with). - - ## examples - - Stash only items updated in the last 2 days: - - `pocketsnack --stash -n 2` - - Stash only items NOT updated in the last 7 days: - - `pocketsnack --stash -o 7` - - Purge tags on all items in the List that were updated in the last day: - - `pocketsnack -pln 1` - - Run lucky_dip: - - `pocketsnack --lucky_dip` - - Run lucky_dip but only choose from items last updated longer ago than one week: - - `pocketsnack -d -o 7` - - ## Uninstalling or moving to a new directory - - Don't like _pocket-snack_ any more or want to re-install it in a new directory? No problem, you will just need to do a little maintenance: - - 1. Delete the executable link - if you don't do this when re-installing in a different directory, running `pocketsnack` will fail because it will still be pointing at the old directory. - - `rm /usr/local/bin/pocketsnack` - - 2. Now you can safely delete the pocket-snack directory. - - ## Bugs and suggestions - - Please log an issue - but check the existing issues first in case it's already been/being dealt with. - -Platform: UNKNOWN -Classifier: Programming Language :: Python :: 3 -Classifier: License :: OSI Approved :: GPL-3.0-or-later -Classifier: Operating System :: OS Independent -Description-Content-Type: text/markdown diff --git a/pocketsnack.egg-info/SOURCES.txt b/pocketsnack.egg-info/SOURCES.txt deleted file mode 100644 index aae0912..0000000 --- a/pocketsnack.egg-info/SOURCES.txt +++ /dev/null @@ -1,8 +0,0 @@ -README.md -setup.py -pocketsnack.egg-info/PKG-INFO -pocketsnack.egg-info/SOURCES.txt -pocketsnack.egg-info/dependency_links.txt -pocketsnack.egg-info/entry_points.txt -pocketsnack.egg-info/requires.txt -pocketsnack.egg-info/top_level.txt \ No newline at end of file diff --git a/pocketsnack.egg-info/dependency_links.txt b/pocketsnack.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/pocketsnack.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/pocketsnack.egg-info/entry_points.txt b/pocketsnack.egg-info/entry_points.txt deleted file mode 100644 index a43f030..0000000 --- a/pocketsnack.egg-info/entry_points.txt +++ /dev/null @@ -1,3 +0,0 @@ -[console_scripts] -pocketsnack = pocketsnack:main_func - diff --git a/pocketsnack.egg-info/requires.txt b/pocketsnack.egg-info/requires.txt deleted file mode 100644 index f229360..0000000 --- a/pocketsnack.egg-info/requires.txt +++ /dev/null @@ -1 +0,0 @@ -requests diff --git a/pocketsnack.egg-info/top_level.txt b/pocketsnack.egg-info/top_level.txt deleted file mode 100644 index 8b13789..0000000 --- a/pocketsnack.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/settings-example.py b/settings-example.py deleted file mode 100644 index f3caff1..0000000 --- a/settings-example.py +++ /dev/null @@ -1,13 +0,0 @@ -pocket_consumer_key = '' # paste the 'consumer key' from your Pocket app between the quotes here -pocket_redirect_uri = 'https://hugh.run/success' -items_per_cycle = 10 # integer -archive_tag = 'tbr' # apply this tag before archiving items. This is case-insensitive and Pocket will always downcase it -ignore_tags = {'ignore'} # don't stash any items with these tags -ignore_faves = True # don't stash items that are favorited -replace_all_tags = False # if set to True this will replace ALL tags on an item in the user's List with the archive_tag and anything in retain_tags. If set to False, the archive tag is still added but any existing tags are retained. -retain_tags = {'glam blog club', 'aus glam blogs', 'empocketer'} # if replace_all_tags is set to True, you can still retain particular tags by adding them to the exclude_tags list. Note that this is a Set, not a List -longreads_wordcount = 3000 # integer - if there are more than this many words, the article will be classified as a 'long read' -num_videos = None # if you change this to an integer this many items_per_cycle will be videos (if there are any videos in your list) -num_images = None # if you change this to an integer this many items_per_cycle will be images (if there are any images in your list) -num_longreads = 2 # if this is an integer this many items_per_cycle will be longreads (if there are any longreads in your list). If all your items are longreads you will still get the total items_per_cycle. Change to None or False if you don't want to filter longreads -pocket_access_token = None \ No newline at end of file diff --git a/setup.py b/setup.py index a23315e..dc30ee4 100644 --- a/setup.py +++ b/setup.py @@ -1,27 +1,19 @@ from setuptools import setup, find_packages -with open("README.md", "r") as fh: - long_description = fh.read() - -setup( - name="pocketsnack", - version="2.0.1", - author="Hugh Rundle", - author_email="hugh@hughrundle.net", - description="When your Pocket list is overwhelming, pocket-snack lets you see just what you can read today. A script for managing your getpocket.com list.", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/hughrun/pocket-snack", - packages=find_packages(), - install_requires=["requests"], - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: GPL-3.0-or-later", - "Operating System :: OS Independent" - ], - entry_points={ - 'console_scripts': [ - 'pocketsnack = pocketsnack:main_func' - ] - } -) \ No newline at end of file +setup(name='pocketsnack', + version='2.0.1', + url='https://hughrundle.net', + license='GPL-3.0-or-later', + packages=find_packages(), + scripts=['bin/pocketsnack'], + install_requires=['request'], + zip_safe=False, + author='Hugh Rundle', + author_email='hugh@hughrundle.net', + description='A command line package for managing Pocket accounts.', + keywords='pocket, pocket-api', + project_urls={ + 'Source Code': 'https://github.com/hughrun/pocketsnack', + 'Documentation': 'https://github.com/hughrun/pocketsnack' + } +) From 77d035dfab1a136d896e8bedededccb86ec0a67d Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 22 Nov 2019 10:37:55 +1100 Subject: [PATCH 3/4] add and rename files for installation with pip --- bin/pocketsnack | 207 ++++++++++++++ lib/__init__.py | 0 lib/toolkit.py | 541 +++++++++++++++++++++++++++++++++++ settings/__init__.py | 0 settings/settings-example.py | 13 + 5 files changed, 761 insertions(+) create mode 100644 bin/pocketsnack create mode 100644 lib/__init__.py create mode 100755 lib/toolkit.py create mode 100644 settings/__init__.py create mode 100644 settings/settings-example.py diff --git a/bin/pocketsnack b/bin/pocketsnack new file mode 100644 index 0000000..b453388 --- /dev/null +++ b/bin/pocketsnack @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 + +# pocket-snack - a Python3 tool to help you retain your sanity when using your Pocket account + +# Copyright (C) 2019 Hugh Rundle + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# You can contact Hugh on Mastodon at @hugh@ausglam.space +# or Twitter at @hughrundle +# or email hugh [at] hughrundle [dot] net + +# ---------------- +# Import libraries +# ---------------- + +from argparse import ArgumentParser + +# bundled with Python +import os +import subprocess + +# local modules +from settings import settings +from lib import toolkit as pt + +# ---------------- +# Settings +# ---------------- + +# TODO: clean this up + +# assign short variable names from the settings file +consumer_key = settings.pocket_consumer_key +redirect_uri = settings.pocket_redirect_uri +archive_tag = settings.archive_tag + +# settings dict for other vars +refresh_settings = [ + consumer_key, + settings.pocket_access_token, + archive_tag, + settings.replace_all_tags, + settings.retain_tags, + settings.ignore_faves, + settings.ignore_tags, + settings.items_per_cycle, + settings.num_videos, + settings.num_images, + settings.num_longreads, + settings.longreads_wordcount +] + +# ---------------- +# argparser arguments +# ---------------- + +parser = ArgumentParser(description='\033[1;36mpocketsnack: a command line tool for decluttering your Pocket account\033[1;m') +admin = parser.add_argument_group('admin commands') +actions = parser.add_argument_group('action commands') +mex = parser.add_mutually_exclusive_group() +timers = parser.add_mutually_exclusive_group() + +mex.add_argument( + "-a", "--archive", action="store_true", help="get information on TBR items in archive (with -i) or purge tags in archive (with -p)" +) +mex.add_argument( + "-b", "--all", action="store_true", help="purge all tags in both list and archive (with -p)" +) +actions.add_argument( + "-d", "--lucky_dip", action="store_true", help="move random items tagged 'tbr' from archive to list, depending on settings" +) +actions.add_argument( + "-i", "--info", action="store_true", help="get information on items in list or TBR items in archive" +) +mex.add_argument( + "-l", "--list", action="store_true", help="get information on items in list (with -i) or purge tags in list (with -p)" +) +timers.add_argument( + "-n", "--since", type=int, help="only act on items where last activity is newer than a given number of days. Use with any action command" +) +timers.add_argument( + "-o", "--before", type=int, help="only act on items where last activity is older than a given number of days. Use with any action command" +) +actions.add_argument( + "-p", "--purge", action="store_true", help="remove all tags from list, archive, or both, depending on the second argument provided and excepting tags listed in 'retain_tags' in settings" +) +actions.add_argument( + "-s", "--stash", action="store_true", help="add 'tbr' tag to all items in user list and archive them, with exceptions as per settings" +) +admin.add_argument( + "-t", "--test", action="store_true", help="test whether API call returns data" +) +admin.add_argument( + "-u", "--authorise", action="store_true", help="authorise app to connect to a Pocket account" +) + +options = parser.parse_args() + +# ---------------- +# What happens with each combination? +# ---------------- + +if __name__ == '__main__': + + # Find all args that have a value other than False + # This helps with error messages for optional args + # that need to be used in combination with something else + true_vars = [] + orphans = ['list', 'archive', 'all', 'since', 'before'] + for x in vars(options): + if vars(options)[x]: + true_vars.append(x) + + if options.authorise: + # Run authorise once first to retrieve a pocket_access_token + auth = pt.authorise(consumer_key, redirect_uri) + print(auth) + + elif options.lucky_dip: + print('\033[0;36m Running lucky dip...\033[0;m') + dip = pt.lucky_dip(consumer_key, settings.pocket_access_token, settings.archive_tag, settings.items_per_cycle, settings.num_videos, settings.num_images, settings.num_longreads, settings.longreads_wordcount, options.before, options.since) + print('\033[0;36m' + dip + '\033[0;m') + + elif options.info: + + def print_info(response, collection): + items = str(len(response)) + longreads = 0 + for item in response: + # is it a long read? + if 'word_count' in response[item]: + words = int(response[item]['word_count']) + longread = True if words > settings.longreads_wordcount else False + else: + longread = False + if longread: + longreads += 1 + + if options.before: + print(collection + 'has ' + items + ' items ' + 'updated prior to ' + str(options.before) + ' days ago and ' + str(longreads) + ' are longreads.\033[0;m') + elif options.since: + print(collection + 'has ' + items + ' items ' + 'updated since ' + str(options.since) + ' days ago and ' + str(longreads) + ' are longreads.\033[0;m') + else: + print(collection + 'has ' + items + ' items and ' + str(longreads) + ' are longreads.\033[0;m') + + if options.archive: + response = pt.info(consumer_key, settings.pocket_access_token, archive_tag, options.before, options.since) + if len(response) > 0: + print_info(response, '\033[0;36m The TBR archive ') + else: + print('\033[0;36m No items match that query\033[0;m') + + elif options.list: + response = pt.info(consumer_key, settings.pocket_access_token, False, options.before, options.since) + if len(response) > 0: + print_info(response, '\033[0;36m The user List ') + else: + print('No items match that query') + + else: + print('\n \033[0;36m--info\033[0;m requires a second argument (-a or -l). Check \033[0;36mpocketsnack --help\033[0;m for more information\n') + + elif options.purge: + + if options.list: + print('\033[0;36m Purging tags in the list\033[0;m') + purge = pt.purge_tags('unread', settings.retain_tags, archive_tag, consumer_key, settings.pocket_access_token, options.before, options.since) + print(purge) + + elif options.archive: + print('\033[0;36m Purging tags in the archive\033[0;m') + purge = pt.purge_tags('archive', settings.retain_tags, archive_tag, consumer_key, settings.pocket_access_token, options.before, options.since) + print(purge) + + elif options.all: + print('\033[0;36m Purging tags in both the archive and the list\033[0;m') + purge = pt.purge_tags('all', settings.retain_tags, archive_tag, consumer_key, settings.pocket_access_token, options.before, options.since) + print(purge) + + else: + print('\n \033[0;36m--purge\033[0;m requires a second argument (-a, -l or -b). Check \033[0;36mpocketsnack --help\033[0;m for more information\n') + + elif options.stash: + stash = pt.stash(consumer_key, settings.pocket_access_token, archive_tag, settings.replace_all_tags, settings.retain_tags, settings.ignore_faves, settings.ignore_tags, options.before, options.since) + print('\033[0;36m' + stash + '\033[0;m') + + elif options.test: + result = pt.test(consumer_key, settings.pocket_access_token) + print(result) + + elif set(true_vars).intersection(orphans): + print('\n That command cannot be used by itself. Check \033[0;36mpocketsnack --help\033[0;m for more information\n') + + else: + print('\033[0;36m pocketsnack\033[0;m requires commands and/or flags to do anything useful. Try \033[0;36mpocketsnack -h\033[0;m for more information') \ No newline at end of file diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/toolkit.py b/lib/toolkit.py new file mode 100755 index 0000000..2eeadba --- /dev/null +++ b/lib/toolkit.py @@ -0,0 +1,541 @@ +# pocket-toolkit - a collection of functions to manage your Pocket account + +# Copyright (C) 2019 Hugh Rundle + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# You can contact Hugh on Mastodon at @hugh@ausglam.space +# or Twitter at @hughrundle +# or email hugh [at] hughrundle [dot] net + +# ---------------- +# Import libraries +# ---------------- + +import requests + +# bundled with Python +from datetime import datetime, time, timedelta +import fileinput +import json +import random +import re +import socket +import time +import urllib +import webbrowser + +# ---------------- +# Create app +# ---------------- + +# Log in to Pocket in a web browser +# Go to https://getpocket.com/developer and click 'CREATE NEW APP' +# Complete the form: you will need all permissions, and the platform should be 'Desktop (other)' +# Your new app will show a 'consumer key', which you need to paste into the first line in settings.py + +# ----------------- +# reusable functions +# ----------------- + +# Pocket expects particular HTTP headers to send and receive JSON +headers = {"Content-Type": "application/json; charset=UTF-8", "X-Accept": "application/json"} + +# send GET requests +def get(params): + return requests.post('https://getpocket.com/v3/get', headers=headers, params=params) + +# send POST requests +def send(actions_escaped, consumer_key, pocket_access_token): + # POST changes to tags + return requests.post('https://getpocket.com/v3/send?actions=' + actions_escaped + '&access_token=' + pocket_access_token + '&consumer_key=' + consumer_key) + +# check the internet connection is live +def connection_live(): + try: + socket.create_connection(("getpocket.com", 443), 10) + return True + except OSError: + pass + return False + +# make a unix timestamp for before/after flags with Pocket's 'since' param +def get_timestamp(since): + now = datetime.now() + delta = timedelta(days=since) + since_time = datetime.strftime(now - delta, '%c') + strptime = time.strptime(since_time) + return time.mktime(strptime) # return Unix timestamp + +def get_item_list(params, before, since): + if before: + all_items = get(params) + params['since'] = get_timestamp(before) + since_items = get(params) + # get non-intersection of 2 groups to get only items last changed 'before' + item_list = all_items.json()['list'] # everything + since_list = since_items.json()['list'] # only things since 'before' + if len(since_list) > 0: + for key in since_list.keys(): + item_list.pop(key, None) # remove everything from items_list that is in since_list + return item_list + elif since: + timestamp = get_timestamp(since) + params['since'] = timestamp + return get(params).json()['list'] + else: + return get(params).json()['list'] + +# -------------------- +# process tag updates +# -------------------- + +def process_items(actions, consumer_key, pocket_access_token): + # Update the tags + # group into smaller chunks of 20 to avoid a 414 (URL too long) error + tag_chunks = [actions[i:i+20] for i in range(0, len(actions), 20)] + + # process each chunk + for i, chunk in enumerate(tag_chunks): + + actions_string = json.dumps(chunk) + # now URL encode it using urllib + actions_escaped = urllib.parse.quote(actions_string) + print(' Processing ' + str(i*20) + ' to ' + str((i*20)+len(tag_chunks[i])) + ' of ' + str(len(actions)) + '...', end="", flush=True) # printing like this means the return callback is appended to the line + # post update to tags + update = send(actions_escaped, consumer_key, pocket_access_token) + if update.raise_for_status() == None: + print('\033[0;32mOk\033[0;m') # Print 'Ok' in green. + else: + print('\031[0;41mOh dear, something went wrong.\033[0;m') # Print error in red + time.sleep(2) # don't fire off requests too quickly + +# ---------------- +# Authorise +# ---------------- + +def authorise(consumer_key, redirect_uri): # With an 's'. Deal with it. + paramsOne = {"consumer_key": consumer_key, "redirect_uri": redirect_uri} + # set up step 1 request - this should return a 'code' aka 'request token' + requestOne = requests.post('https://getpocket.com/v3/oauth/request', headers=headers, params=paramsOne) + # get the JSON response and save the token to a param for the next step + request_token = requestOne.json()['code'] + # print the request token to the console so you know it happened + print('\033[0;36m Your request token (code) is \033[0;m' + request_token) + + # now you need to authorise the app in your Pocket account + # build the url + auth_url = 'https://getpocket.com/auth/authorize?request_token=' + request_token + '&redirect_uri=' + redirect_uri + # open the authorisation page in a new tab of your default web browser + webbrowser.open(auth_url, new=2) + + # We're not writing a server app here so we use a little hack to check whether the user has finished authorising before we continue + # Just wait for the user (you!) to indicate they have finished authorising the app + # the '\n' prints a new line + print('\033[0;36m Authorise your app in the browser tab that just opened.\033[0;m') + user_input = input('Type "done" when you have finished authorising the app in Pocket \n>>') + + if user_input == "done": + # now we can continue + # do a new request, this time to the oauth/authorize endpoint with the same JSON headers, but also sending the code as a param + paramsTwo = {"consumer_key": consumer_key, "code": request_token} + requestTwo = requests.post('https://getpocket.com/v3/oauth/authorize', headers=headers, params=paramsTwo) + # get the JSON response as a Python dictionary and call it 'res'. + res = requestTwo.json() + # Finally we have the access token! + print('\033[0;36m Access token for ' + res['username'] + ' is \033[0;m' + res['access_token']) + # Assign the access token to a parameter called access_token + access_token = res['access_token'] + # replace the pocket_access_token line rather than just adding an extra at the end + # TODO: change this file path + settings_file = fileinput.FileInput("pocketsnack/settings.py", inplace=True) + repl = "pocket_access_token = " + "'" + access_token + "'" + for line in settings_file: + line = re.sub('(pocket_access_token)+.*', repl, line) + print(line.rstrip()) + return '\033[0;36m Token added to settings.py - you are ready to use pocketsnack!\033[0;m 🎉' + +# ------------------------------ +# Read info about Pocket account +# ------------------------------ + +def info(consumer_key, pocket_access_token, archive_tag, before, since): + + params = { + "consumer_key": consumer_key, + "access_token": pocket_access_token, + } + if archive_tag: + # state is archive & use archive tag + params['state'] = 'archive' + params['tag'] = archive_tag + else: + # state is unread + params['state'] = 'unread' + + items = get_item_list(params, before, since) + return items + +# choose items to put back into the user List +def lucky_dip(consumer_key, pocket_access_token, archive_tag, items_per_cycle, num_videos, num_images, num_longreads, longreads_wordcount, before, since): + + def run_lucky_dip(attempts): + if connection_live() == True: + # This function is called below to un-archive everything in the selection + def readd(selection): + actions = [] + for item in selection: + item_readd = {"item_id": item, "action": "readd"} + actions.append(item_readd) + # remove archive_tag as well, otherwise the item will keep appearing after it's read and archived by the user + item_detag = {"item_id": item, "action": "tags_remove", "tags": archive_tag} + actions.append(item_detag) + # stringify + items_string = json.dumps(actions) + escaped = urllib.parse.quote(items_string) + # re-add items + return send(escaped, consumer_key, pocket_access_token) + + # get everything in the archive with the archive_tag + params = {"consumer_key": consumer_key, "access_token": pocket_access_token, "state": "archive", "tag": archive_tag} + + tbr = get_item_list(params, before, since) + + # before we go any further, make sure there actually is something in the TBR list! + if len(tbr) > 0: + available = len(tbr) # set this value here before it gets changed + # just to make it clearer, and create a copy + items_needed = items_per_cycle + + # we'll use this to report what was added + chosen = {} + ##################### + # filtering formats + ##################### + videos = [] + images = [] + + for item in tbr: + if num_videos and 'has_video' in tbr[item]: + if tbr[item]['has_video'] == '2': + # it's a video + videos.append(item) + if num_images and 'has_image' in tbr[item]: + if tbr[item]['has_image'] == '2': + # it's an image + images.append(item) + + if num_videos: + # don't select more than the total items_needed + required_videos = items_needed if items_needed <= num_videos else num_videos + selected_videos = videos if len(videos) <= required_videos else random.sample(videos, required_videos) + # re-add the videos + if len(selected_videos) > 0: + readd(selected_videos) + # adjust the total items we need to look for + items_needed -= len(selected_videos) + chosen['videos'] = len(selected_videos) + + if num_images: + required_images = num_images if num_images <= items_needed else items_needed + selected_images = images if len(images) <= required_images else random.sample(images, required_images) + if len(selected_images) > 0: + readd(selected_images) + # adjust the total items we need to look for + items_needed -= len(selected_images) + chosen['images'] = len(selected_images) + + ##################### + # filtering longreads + ##################### + if num_longreads: + long_reads = [] + for item in tbr: + # is it a long read? + if 'word_count' in tbr[item]: + words = int(tbr[item]['word_count']) + if words > longreads_wordcount: + long_reads.append(item) # we only need to append the item_id + + total_longreads = len(long_reads) + total_shortreads = len(tbr) - total_longreads + required_shortreads = items_needed - num_longreads + enough_longreads = total_longreads >= num_longreads + enough_shortreads = total_shortreads >= required_shortreads + + # if there are enough longreads AND enough shortreads, go ahead + if enough_longreads and enough_shortreads: + # select random longreads + # note we need to check whether any are actually required at this point, otherwise we run the risk of items_needed becoming a negative number + selected_longreads = (random.sample(long_reads, num_longreads)) if items_needed > 0 else [] + # add random shortreads + # first we need to remove the longreads from tbr: + for article in long_reads: + tbr.pop(article, None) + # now grab a random selection to fill our items_needed quota + selected_shortreads = random.sample(list(tbr), required_shortreads) if required_shortreads > 0 else [] + # add the two lists together + selection = selected_longreads + selected_shortreads + chosen['longreads'] = len(selected_longreads) + chosen['shortreads'] = len(selected_shortreads) + readd(selection) + # If there are too few longreads but enough shortreads, take what LR we have and make up the difference + elif enough_shortreads: + # select all of the longreads + selected_longreads = long_reads + # add random shortreads + # first we need to remove the longreads from tbr: + for article in long_reads: + tbr.pop(article, None) + # now grab a random selection to fill our items_needed quota + difference = items_needed - len(selected_longreads) + # make sure you have enough of the new total + enough_difference = difference <= total_shortreads + if enough_difference: + selected_shortreads = random.sample(list(tbr), difference) if difference > 0 else [] + # if there are too few longreads AND too few shortreads just grab what shortreads you have + else: + selected_shortreads = list(tbr) # note this is the new tbr after we popped the longreads + # add the two lists together + selection = selected_longreads + selected_shortreads + chosen['longreads'] = len(selected_longreads) + chosen['shortreads'] = len(selected_shortreads) + readd(selection) + # if there are enough longreads but too few shortreads, use all the shortreads and make up the difference with longreads + elif enough_longreads: + # add all the shortreads there are + # first we need to remove the longreads from tbr: + for article in long_reads: + tbr.pop(article, None) + # the new tbr is now entirely shortreads + selected_shortreads = list(tbr) + # now grab a random selection of long reads to fill our items_needed quota + difference = items_needed - len(selected_shortreads) + # make sure you have enough of the new total + enough_difference = difference <= total_longreads + if enough_difference: + selected_longreads = random.sample(long_reads, difference) if difference > 0 else [] + # if there are too few longreads AND too few shortreads just grab what longreads you have + else: + selected_longreads = long_reads + # add the two lists together + selection = selected_longreads + selected_shortreads + chosen['longreads'] = len(selected_longreads) + chosen['shortreads'] = len(selected_shortreads) + readd(selection) + else: + # if we get to here there aren't enough of either, so we should just return everything + chosen['longreads'] = total_longreads + chosen['shortreads'] = total_shortreads + readd(list(tbr)) + else: # if num_longreads is False or 0 (which are the same thing) + # check how many items in total there are in the TBR list + # if there are fewer than items_needed, just return all of them, otherwise get a random selection + if items_needed > 0: + selection = list(tbr) if len(tbr) < items_needed else random.sample(list(tbr), items_needed) + chosen['random'] = len(selection) + readd(selection) + # return a total of what was moved into the list (faves, format etc) + tot_videos = chosen['videos'] if 'videos' in chosen else None + tot_images = chosen['images'] if 'images' in chosen else None + random_choice = 'random' in chosen + tot_added = 0 + for v in chosen.values(): + tot_added += v + remaining = available - tot_added + completed_message = ' \033[0;36mSuccess! ' + str(tot_added) + ' items added to your reading list, including ' + if random_choice: + completed_message += str(chosen['random']) + ' random articles, ' + if tot_images: + completed_message += str(tot_images) + ' images, ' + if tot_videos: + completed_message += str(tot_videos) + ' videos, ' + if not random_choice: + completed_message += str(chosen['longreads']) + ' long reads and ' + str(chosen['shortreads']) + ' short reads, ' + # add this to the end regardless + caveat = '' + if before: + caveat = 'last updated earlier than ' + str(before) + ' days ago ' + if since: + caveat = 'last updated more recently than ' + str(since) + ' days ago ' + completed_message += 'with ' + str(remaining) + ' other items ' + caveat + 'remaining to be read.\033[0;m' + return completed_message + # else if there's nothing tagged with the archive_tag + else: + return '\033[0;36m Nothing to be read!\033[0;m' + else: + if attempts < 4: + attempts += 1 + time.sleep(10) + print('\033[0;36m Attempting to connect...\033[0;m') + return run_lucky_dip(attempts) + else: + msg = "\033[0;36m Sorry, no connection after 4 attempts.\033[0;m" + return msg + + return run_lucky_dip(0) + +# ----------------- +# purge tags +# ----------------- + +def purge_tags(state, retain_tags, archive_tag, consumer_key, pocket_access_token, before, since): + + params = { + "consumer_key": consumer_key, + "access_token": pocket_access_token, + "state": state, + "detailType": "complete" + } + + # check we're online + if connection_live() == True: + # GET the list + request = get_item_list(params, before, since) + actions = [] + + if len(request) > 0: + for item in request: + item_tags = [] + # find the item tags + if 'tags' in request[item]: + for tag in request[item]['tags']: + item_tags.append(tag) + # keep any retain_tags like we use in stash + retain_tags.add(archive_tag) # we don't want to wipe out the archive tag on archived items! + update = {"item_id": item} + intersect = list(retain_tags.intersection(item_tags)) + if len(intersect) > 0: + update['action'] = 'tags_replace' # item is the ID because it's the dict key + update["tags"] = intersect # update tags to keep the retain_tags + # otherwise just clear all tags + else: + update['action'] = 'tags_clear' # item is the ID because it's the dict key + actions.append(update) + + process_items(actions, consumer_key, pocket_access_token) + return '\033[1;36m Undesirable elements have been purged.\033[1;m' + + else: + return '\033[0;36m No items from which to purge tags.\033[0;m' + +""" +Stash + +stash(consumer_key, pocket_access_token, archive_tag [, retain_tags] [, favorite]) + +Stash applies the archive_tag to all items in the User List, then archives everything in the List. + +Options: + +retain_tags - a list of tags that should not be removed from items when adding the archive_tag. If you don't want to retain any tags, this value should be False. Defaults to False +favorite - boolean indicating whether to ignore (i.e. leave in the user list) favorite items. Defaults to True + +""" + +# ----------------- +# stash items +# ----------------- + +def stash(consumer_key, pocket_access_token, archive_tag, replace_all_tags, retain_tags, favorite, ignore_tags, before, since): + print('\033[0;36m Stashing items...\033[0;m') + # if ignore_faves is set to True, don't get favorite items + params = {"consumer_key": consumer_key, "access_token": pocket_access_token, "detailType": "complete", "state": "unread"} + if favorite: + params['favorite'] = "0" + print('\033[0;36m Skipping favorited items...\033[0;m') + + def run_stash(attempts): + if connection_live() == True: + # GET the list + item_list = get_item_list(params, before, since) + # we store all the 'actions' in an array, then send one big HTTP request to the Pocket API + actions = [] + # copy items_list so we can alter the copy whilst iterating through the original + items_to_stash = dict(item_list) + for item in item_list: + item_tags = [] + if 'tags' in item_list[item]: + for tag in item_list[item]['tags']: + item_tags.append(tag) + + # filter out any items with the ignore tags before dealing with the rest + if len(ignore_tags) > 0 and len(ignore_tags.intersection(item_tags)) > 0: + # pop it out of the items_to_stash + items_to_stash.pop(item, None) + # Now we process all the tags first, before we archive everything + elif replace_all_tags: + # set up the action dict + action = {"item_id": item, "action": "tags_replace"} # item is the ID because it's the dict key + # are we retaining any tags? + if retain_tags: # retain_tags should either be False or a Set + # find the common tags between retain_tags and item_tags + # to do this we need retain_tags to be a set, but you can't JSON serialise a set, so we need to turn the result into a list afterwards + tags_to_keep = list(retain_tags.intersection(item_tags)) + # don't forget to add the archive_tag! + tags_to_keep.append(archive_tag) + action["tags"] = tags_to_keep + # Anything that is still in the user list can be presumed to not have been read + # when they read it they will archive it (without the archive_tag because lucky_dip removes it) + else: + action["tags"] = archive_tag + actions.append(action) + else: # if replace_all_tags is False, just add the archive tag without removing any tags + action = {"item_id": item, "action": "tags_add"} # add new tag rather than replacing all of them + action["tags"] = archive_tag + actions.append(action) + + # Update the tags + process_items(actions, consumer_key, pocket_access_token) + + # Now archive everything + archive_actions = [] + + for item in items_to_stash: + item_action = {"item_id": item, "action": "archive"} + archive_actions.append(item_action) + + print('\033[0;36m Archiving ' + str(len(archive_actions)) + ' items...\033[0;m') + + # archive items + process_items(archive_actions, consumer_key, pocket_access_token) + + # return a list of what was stashed and, if relevant, what wasn't + skipped_items = len(item_list) - len(items_to_stash) + return ' ' + str(len(items_to_stash)) + ' items archived with "' + archive_tag + '" and ' + str(skipped_items) + ' items skipped due to retain tag.' + else: + if attempts < 4: + attempts += 1 + time.sleep(10) + print('\033[0;36m Attempting to connect...\033[0;m') + return run_stash(attempts) + else: + msg = "\033[0;31m Sorry, no connection after 4 attempts.\033[0;m" + return msg + + return run_stash(0) + +# ----------------- +# test +# ----------------- + +def test(consumer_key, pocket_access_token): + + params = {"consumer_key": consumer_key, "access_token": pocket_access_token, "state": "archive", "count": "1", "detailType": "complete"} + + # GET the list + request = get(params) + list_items = request.json() + + return json.dumps(list_items, indent=4, default=str) \ No newline at end of file diff --git a/settings/__init__.py b/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/settings/settings-example.py b/settings/settings-example.py new file mode 100644 index 0000000..28f631b --- /dev/null +++ b/settings/settings-example.py @@ -0,0 +1,13 @@ +pocket_consumer_key = '' # paste the 'consumer key' from your Pocket app between the quotes here +pocket_redirect_uri = 'https://hugh.run/success' +items_per_cycle = 10 # integer, not a string +archive_tag = 'tbr' # apply this tag before archiving items. This is case-insensitive and Pocket will always downcase it +ignore_tags = {'ignore'} # don't stash any items with these tags +ignore_faves = True # don't stash items that are favorited +replace_all_tags = False # if set to True this will replace ALL tags on an item in the user's List with the archive_tag and anything in retain_tags. If set to False, the archive tag is still added but any existing tags are retained. +retain_tags = {'glam blog club', 'aus glam blogs', 'empocketer'} # if replace_all_tags is set to True, you can still retain particular tags by adding them to the exclude_tags list. Note that this is a Set, not a List +longreads_wordcount = 3000 # integer - if there are more than this many words, the article will be classified as a 'long read' +num_videos = None # if you change this to an integer this many items_per_cycle will be videos (if there are any videos in your list) +num_images = None # if you change this to an integer this many items_per_cycle will be images (if there are any images in your list) +num_longreads = 2 # if this is an integer this many items_per_cycle will be longreads (if there are any longreads in your list). If all your items are longreads you will still get the total items_per_cycle. Change to None or False if you don't want to filter longreads +pocket_access_token = None \ No newline at end of file From d3fa2f20be78cfc451d28b0cfd8cf9af66986312 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Fri, 22 Nov 2019 10:45:34 +1100 Subject: [PATCH 4/4] update README and bump version --- README.md | 6 +++--- setup.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5ef7a81..3282ee4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# pocket-snack +# pocketsnack When your Pocket list is overwhelming, pocket-snack lets you see just what you can read today -This is the version 2 README. If you haven't yet upgraded you can still use the [version 1 README](v1_README.md). +This is the version 2 documentation. If you haven't yet upgraded you can still use the [version 1 README](v1_README.md). ## A note on version 2 @@ -19,7 +19,7 @@ The automation of `pocketsnack refresh` has _also_ been removed. This didn't rea ## Getting started 1. make sure you have installed Python 3 -2. download `pocketsnack` using git or the download link in **Releases** +2. download `pocketsnack` using git or the download link in [releases](releases) 3. move into the top `pocketsnack` directory 4. `pip3 install .` or if pip points to Python3, `pip install .` 5. Edit `settings/settings.py` diff --git a/setup.py b/setup.py index dc30ee4..0ee3506 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='pocketsnack', - version='2.0.1', + version='2.1.0', url='https://hughrundle.net', license='GPL-3.0-or-later', packages=find_packages(), @@ -11,9 +11,9 @@ author='Hugh Rundle', author_email='hugh@hughrundle.net', description='A command line package for managing Pocket accounts.', - keywords='pocket, pocket-api', + keywords='pocket, pocket api', project_urls={ - 'Source Code': 'https://github.com/hughrun/pocketsnack', - 'Documentation': 'https://github.com/hughrun/pocketsnack' + 'Source Code': 'https://github.com/hughrun/pocketsnack', + 'Documentation': 'https://github.com/hughrun/pocketsnack' } )