This commit is contained in:
Nick Sweeting 2024-10-25 01:06:12 -07:00
parent 4b6f08b0fe
commit 5d9a32c364
No known key found for this signature in database
178 changed files with 2982 additions and 1322 deletions

View file

@ -0,0 +1,61 @@
version: 2.1
orbs:
python: circleci/python@2.0.3
jobs:
build_and_test_3_7:
docker:
- image: circleci/python:3.7
executor: python/default
steps:
- checkout
- python/install-packages:
pkg-manager: pip
- run:
name: Run tests
command: nosetests
build_and_test_3_8:
docker:
- image: circleci/python:3.8
executor: python/default
steps:
- checkout
- python/install-packages:
pkg-manager: pip
- run:
name: Run tests
command: nosetests
build_and_test_3_9:
docker:
- image: circleci/python:3.9
executor: python/default
steps:
- checkout
- python/install-packages:
pkg-manager: pip
- run:
name: Run tests
command: nosetests
build_and_test_3_10:
docker:
- image: circleci/python:3.10
executor: python/default
steps:
- checkout
- python/install-packages:
pkg-manager: pip
- run:
name: Run tests
command: nosetests
workflows:
test_pocket:
jobs:
- build_and_test_3_7
- build_and_test_3_8
- build_and_test_3_9
- build_and_test_3_10

43
packages/archivebox-pocket/.gitignore vendored Normal file
View file

@ -0,0 +1,43 @@
*.py[co]
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
.pypirc
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
#Translations
*.mo
#Mr Developer
.mr.developer.cfg
# Virtualenv
include/
lib/
local/
.Python
# ViM files
.*.swp
.*.swo
# Misc
*.log
*.pid
*.sql

View file

@ -0,0 +1,27 @@
Copyright (c) 2014, Tapan Pandita
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
* Neither the name of pocket nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,2 @@
include LICENSE.md
include README.md

View file

@ -0,0 +1,66 @@
Pocket
======
[![CircleCI](https://img.shields.io/circleci/build/github/tapanpandita/pocket/master?logo=CircleCI)](https://circleci.com/gh/tapanpandita/pocket)
[![Pypi](https://img.shields.io/pypi/v/pocket.svg)](https://pypi.python.org/pypi/pocket)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/pocket.svg)](https://pypi.python.org/pypi/pocket)
![GitHub](https://img.shields.io/github/license/tapanpandita/pocket.svg)
A python wrapper for the [pocket api](http://getpocket.com/api/docs).
Installation
------------
```
pip install pocket
```
Usage
------
You'll need your pocket consumer key. You can find this from your account page.
You will also need the access token for the account you want to modify.
Then, you need to create an instance of the pocket object
```python
import pocket
pocket_instance = pocket.Pocket(consumer_key, access_token)
```
### Chaining Modify Methods
All the modify methods can be chained together for creating one bulk query. If you don't wish to chain the methods, just pass `wait=False`.
```python
import pocket
pocket_instance = pocket.Pocket(consumer_key, access_token)
# perfoms all these actions in one request
# NOTE: Each individual method returns the instance itself. The response
# dictionary is not returned till commit is called on the instance.
response, headers = pocket_instance.archive(item_id1).archive(item_id2).favorite(item_id3).delete(item_id4).commit()
# performs action immediately and returns a dictionary
pocket_instance.archive(item_id1, wait=False)
```
### OAUTH
To get request token, use the get_request_token class method. To get the access token use the get_access_token method.
```python
from pocket import Pocket
request_token = Pocket.get_request_token(consumer_key=consumer_key, redirect_uri=redirect_uri)
# URL to redirect user to, to authorize your app
auth_url = Pocket.get_auth_url(code=request_token, redirect_uri=redirect_uri)
# e.g. import subprocess; subprocess.run(['xdg-open', auth_url])
user_credentials = Pocket.get_credentials(consumer_key=consumer_key, code=request_token)
access_token = user_credentials['access_token']
```
For detailed documentation of the methods available, please visit the official [pocket api documentation](http://getpocket.com/api/docs).

View file

@ -0,0 +1,366 @@
import requests
import json
from functools import wraps
class PocketException(Exception):
'''
Base class for all pocket exceptions
http://getpocket.com/developer/docs/errors
'''
pass
class InvalidQueryException(PocketException):
pass
class AuthException(PocketException):
pass
class RateLimitException(PocketException):
'''
http://getpocket.com/developer/docs/rate-limits
'''
pass
class ServerMaintenanceException(PocketException):
pass
EXCEPTIONS = {
400: InvalidQueryException,
401: AuthException,
403: RateLimitException,
503: ServerMaintenanceException,
}
def method_wrapper(fn):
@wraps(fn)
def wrapped(self, *args, **kwargs):
arg_names = list(fn.__code__.co_varnames)
arg_names.remove('self')
kwargs.update(dict(zip(arg_names, args)))
url = self.api_endpoints[fn.__name__]
payload = dict([
(k, v) for k, v in kwargs.items()
if v is not None
])
payload.update(self.get_payload())
return self.make_request(url, payload)
return wrapped
def bulk_wrapper(fn):
@wraps(fn)
def wrapped(self, *args, **kwargs):
arg_names = list(fn.__code__.co_varnames)
arg_names.remove('self')
kwargs.update(dict(zip(arg_names, args)))
wait = kwargs.get('wait', True)
query = dict(
[(k, v) for k, v in kwargs.items() if v is not None]
)
# TODO: Fix this hack
query['action'] = 'add' if fn.__name__ == 'bulk_add' else fn.__name__
if wait:
self.add_bulk_query(query)
return self
else:
url = self.api_endpoints['send']
payload = {
'actions': [query],
}
payload.update(self.get_payload())
return self.make_request(
url,
json.dumps(payload),
headers={'content-type': 'application/json'},
)
return wrapped
class Pocket(object):
'''
This class implements a basic python wrapper around the pocket api. For a
detailed documentation of the methods and what they do please refer the
official pocket api documentation at
http://getpocket.com/developer/docs/overview
'''
api_endpoints = dict(
(method, 'https://getpocket.com/v3/%s' % method)
for method in "add,send,get".split(",")
)
statuses = {
200: 'Request was successful',
400: 'Invalid request, please make sure you follow the '
'documentation for proper syntax',
401: 'Problem authenticating the user',
403: 'User was authenticated, but access denied due to lack of '
'permission or rate limiting',
503: 'Pocket\'s sync server is down for scheduled maintenance.',
}
def __init__(self, consumer_key, access_token):
self.consumer_key = consumer_key
self.access_token = access_token
self._bulk_query = []
self._payload = {
'consumer_key': self.consumer_key,
'access_token': self.access_token,
}
def get_payload(self):
return self._payload
def add_bulk_query(self, query):
self._bulk_query.append(query)
@staticmethod
def _post_request(url, payload, headers):
r = requests.post(url, data=payload, headers=headers)
return r
@classmethod
def _make_request(cls, url, payload, headers=None):
r = cls._post_request(url, payload, headers)
if r.status_code > 399:
error_msg = cls.statuses.get(r.status_code)
extra_info = r.headers.get('X-Error')
raise EXCEPTIONS.get(r.status_code, PocketException)(
'%s. %s' % (error_msg, extra_info)
)
return r.json() or r.text, r.headers
@classmethod
def make_request(cls, url, payload, headers=None):
return cls._make_request(url, payload, headers)
@method_wrapper
def add(self, url, title=None, tags=None, tweet_id=None):
'''
This method allows you to add a page to a user's list.
In order to use the /v3/add endpoint, your consumer key must have the
"Add" permission.
http://getpocket.com/developer/docs/v3/add
'''
@method_wrapper
def get(
self, state=None, favorite=None, tag=None, contentType=None,
sort=None, detailType=None, search=None, domain=None, since=None,
count=None, offset=None
):
'''
This method allows you to retrieve a user's list. It supports
retrieving items changed since a specific time to allow for syncing.
http://getpocket.com/developer/docs/v3/retrieve
'''
@method_wrapper
def send(self, actions):
'''
This method allows you to make changes to a user's list. It supports
adding new pages, marking pages as read, changing titles, or updating
tags. Multiple changes to items can be made in one request.
http://getpocket.com/developer/docs/v3/modify
'''
@bulk_wrapper
def bulk_add(
self, item_id, ref_id=None, tags=None, time=None, title=None,
url=None, wait=True
):
'''
Add a new item to the user's list
http://getpocket.com/developer/docs/v3/modify#action_add
'''
@bulk_wrapper
def archive(self, item_id, time=None, wait=True):
'''
Move an item to the user's archive
http://getpocket.com/developer/docs/v3/modify#action_archive
'''
@bulk_wrapper
def readd(self, item_id, time=None, wait=True):
'''
Re-add (unarchive) an item to the user's list
http://getpocket.com/developer/docs/v3/modify#action_readd
'''
@bulk_wrapper
def favorite(self, item_id, time=None, wait=True):
'''
Mark an item as a favorite
http://getpocket.com/developer/docs/v3/modify#action_favorite
'''
@bulk_wrapper
def unfavorite(self, item_id, time=None, wait=True):
'''
Remove an item from the user's favorites
http://getpocket.com/developer/docs/v3/modify#action_unfavorite
'''
@bulk_wrapper
def delete(self, item_id, time=None, wait=True):
'''
Permanently remove an item from the user's account
http://getpocket.com/developer/docs/v3/modify#action_delete
'''
@bulk_wrapper
def tags_add(self, item_id, tags, time=None, wait=True):
'''
Add one or more tags to an item
http://getpocket.com/developer/docs/v3/modify#action_tags_add
'''
@bulk_wrapper
def tags_remove(self, item_id, tags, time=None, wait=True):
'''
Remove one or more tags from an item
http://getpocket.com/developer/docs/v3/modify#action_tags_remove
'''
@bulk_wrapper
def tags_replace(self, item_id, tags, time=None, wait=True):
'''
Replace all of the tags for an item with one or more provided tags
http://getpocket.com/developer/docs/v3/modify#action_tags_replace
'''
@bulk_wrapper
def tags_clear(self, item_id, time=None, wait=True):
'''
Remove all tags from an item.
http://getpocket.com/developer/docs/v3/modify#action_tags_clear
'''
@bulk_wrapper
def tag_rename(self, item_id, old_tag, new_tag, time=None, wait=True):
'''
Rename a tag. This affects all items with this tag.
http://getpocket.com/developer/docs/v3/modify#action_tag_rename
'''
def commit(self):
'''
This method executes the bulk query, flushes stored queries and
returns the response
'''
url = self.api_endpoints['send']
payload = {
'actions': self._bulk_query,
}
payload.update(self._payload)
self._bulk_query = []
return self._make_request(
url,
json.dumps(payload),
headers={'content-type': 'application/json'},
)
@classmethod
def get_request_token(
cls, consumer_key, redirect_uri='http://example.com/', state=None
):
'''
Returns the request token that can be used to fetch the access token
'''
headers = {
'X-Accept': 'application/json',
}
url = 'https://getpocket.com/v3/oauth/request'
payload = {
'consumer_key': consumer_key,
'redirect_uri': redirect_uri,
}
if state:
payload['state'] = state
return cls._make_request(url, payload, headers)[0]['code']
@classmethod
def get_credentials(cls, consumer_key, code):
'''
Fetches access token from using the request token and consumer key
'''
headers = {
'X-Accept': 'application/json',
}
url = 'https://getpocket.com/v3/oauth/authorize'
payload = {
'consumer_key': consumer_key,
'code': code,
}
return cls._make_request(url, payload, headers)[0]
@classmethod
def get_access_token(cls, consumer_key, code):
return cls.get_credentials(consumer_key, code)['access_token']
@classmethod
def get_auth_url(cls, code, redirect_uri='http://example.com'):
auth_url = ('https://getpocket.com/auth/authorize'
'?request_token=%s&redirect_uri=%s' % (code, redirect_uri))
return auth_url
@classmethod
def auth(
cls, consumer_key, redirect_uri='http://example.com/', state=None,
):
'''
This is a test method for verifying if oauth worked
http://getpocket.com/developer/docs/authentication
'''
code = cls.get_request_token(consumer_key, redirect_uri, state)
auth_url = 'https://getpocket.com/auth/authorize?request_token='\
'%s&redirect_uri=%s' % (code, redirect_uri)
raw_input(
'Please open %s in your browser to authorize the app and '
'press enter:' % auth_url
)
return cls.get_access_token(consumer_key, code)

View file

@ -0,0 +1,19 @@
[project]
name = "archivebox-pocket"
version = "0.3.7"
description = " api wrapper for getpocket.com"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"requests>=2.32.3",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.sdist]
packages = ["."]
[tool.hatch.build.targets.wheel]
packages = ["."]

View file

@ -0,0 +1,4 @@
coverage==3.7.1
mock==1.0.1
nose==1.3.0
requests==2.20.0

View file

@ -0,0 +1,41 @@
from setuptools import setup
setup(
name = "pocket", # pip install pocket
description = "api wrapper for getpocket.com",
#long_description=open('README.md', 'rt').read(),
# version
# third part for minor release
# second when api changes
# first when it becomes stable someday
version = "0.3.7",
author = 'Tapan Pandita',
author_email = "tapan.pandita@gmail.com",
url = 'http://github.com/tapanpandita/pocket/',
license = 'BSD',
# as a practice no need to hard code version unless you know program wont
# work unless the specific versions are used
install_requires = ["requests>=2.32.3"],
py_modules = ["pocket"],
zip_safe = True,
)
# TODO: Do all this and delete these lines
# register: Create an accnt on pypi, store your credentials in ~/.pypirc:
#
# [pypirc]
# servers =
# pypi
#
# [server-login]
# username:<username>
# password:<pass>
#
# $ python setup.py register # one time only, will create pypi page for pocket
# $ python setup.py sdist --formats=gztar,zip upload # create a new release
#

View file

@ -0,0 +1,52 @@
import unittest
import pocket
from mock import patch
class PocketTest(unittest.TestCase):
def setUp(self):
self.consumer_key = 'consumer_key'
self.access_token = 'access_token'
def tearDown(self):
pass
def test_pocket_init(self):
pocket_instance = pocket.Pocket(
self.consumer_key,
self.access_token,
)
self.assertEqual(self.consumer_key, pocket_instance.consumer_key)
self.assertEqual(self.access_token, pocket_instance.access_token)
def test_pocket_init_payload(self):
pocket_instance = pocket.Pocket(
self.consumer_key,
self.access_token,
)
expected_payload = {
'consumer_key': self.consumer_key,
'access_token': self.access_token,
}
self.assertEqual(expected_payload, pocket_instance._payload)
def test_post_request(self):
mock_payload = {
'consumer_key': self.consumer_key,
'access_token': self.access_token,
}
mock_url = 'https://getpocket.com/v3/'
mock_headers = {
'content-type': 'application/json',
}
with patch('pocket.requests') as mock_requests:
pocket.Pocket._post_request(mock_url, mock_payload, mock_headers)
mock_requests.post.assert_called_once_with(
mock_url,
data=mock_payload,
headers=mock_headers,
)