-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 254fe49
Showing
13 changed files
with
570 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# Byte-compiled / optimized / DLL files | ||
__pycache__/ | ||
*.py[cod] | ||
*$py.class | ||
|
||
# C extensions | ||
*.so | ||
|
||
# Distribution / packaging | ||
.Python | ||
build/ | ||
develop-eggs/ | ||
dist/ | ||
downloads/ | ||
eggs/ | ||
.eggs/ | ||
lib/ | ||
lib64/ | ||
parts/ | ||
sdist/ | ||
var/ | ||
*.egg-info/ | ||
.installed.cfg | ||
*.egg | ||
|
||
# PyInstaller | ||
# Usually these files are written by a python script from a template | ||
# before PyInstaller builds the exe, so as to inject date/other infos into it. | ||
*.manifest | ||
*.spec | ||
|
||
# Django stuff: | ||
*.log | ||
local_settings.py | ||
|
||
# Flask stuff: | ||
instance/ | ||
.webassets-cache | ||
|
||
# Scrapy stuff: | ||
.scrapy | ||
|
||
# Sphinx documentation | ||
docs/_build/ | ||
|
||
# PyBuilder | ||
target/ | ||
|
||
# IPython Notebook | ||
.ipynb_checkpoints | ||
|
||
# pyenv | ||
.python-version | ||
|
||
# dotenv | ||
.env | ||
|
||
# virtualenv | ||
env/ | ||
venv/ | ||
ENV/ | ||
|
||
# Spyder project settings | ||
.spyderproject |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
lint: clean | ||
flake8 inoreader --format=pylint || true | ||
|
||
clean: | ||
- find . -iname "*__pycache__" | xargs rm -rf | ||
- find . -iname "*.pyc" | xargs rm -rf | ||
- rm cobertura.xml -f | ||
- rm testresult.xml -f | ||
- rm .coverage -f | ||
|
||
venv: | ||
- virtualenv --python=$(shell which python3) --prompt '<venv:inoreader>' venv | ||
|
||
deps: venv | ||
- venv/bin/pip install -U pip setuptools | ||
- venv/bin/pip install -r requirements.txt |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
Inoreader | ||
========= | ||
|
||
Python wrapper of Inoreader API. | ||
|
||
## Installation | ||
|
||
1. Clone this repository and install it with `setup.py` | ||
|
||
```shell | ||
python setup.py install | ||
``` | ||
|
||
2. Install with `pip` directly | ||
|
||
```shell | ||
pip install git+https://github.com/Linusp/python-inoreader.git | ||
``` | ||
|
||
## Usage | ||
|
||
1. [Register your application](https://www.inoreader.com/developers/register-app) | ||
|
||
2. Set `appid` and `appkey` in your system, you can set them with environment variables like | ||
|
||
```shell | ||
export INOREADER_APP_ID = 'your-app-id' | ||
export INOREADER_APP_KEY = 'your-app-key' | ||
``` | ||
|
||
or write them in `$HOME/.inoreader`, e.g.: | ||
```shell | ||
[auth] | ||
appid = your-app-id | ||
appkey = your-app-key | ||
``` | ||
|
||
3. Login to your Inoreader acount | ||
|
||
```shell | ||
inoreader login | ||
``` | ||
|
||
3. Use the command line tool `inoreader` to do something, run `inoreader --help` for details |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# coding: utf-8 | ||
from .client import InoreaderClient | ||
|
||
|
||
__all__ = ['InoreaderClient'] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
# coding: utf-8 | ||
from __future__ import print_function, unicode_literals | ||
|
||
from .utils import normalize_whitespace, extract_text | ||
|
||
|
||
class Article(object): | ||
def __init__(self, id, title, categories, link, | ||
published=None, content=None, author=None, | ||
feed_id=None, feed_title=None, feed_link=None): | ||
self.id = id | ||
self.title = normalize_whitespace(title) | ||
self.categories = categories | ||
self.link = link | ||
self.published = published | ||
self.content = content.strip() if content else '' | ||
self.text = extract_text(self.content) | ||
self.author = author | ||
self.feed_id = feed_id | ||
self.feed_title = feed_title.strip() | ||
self.feed_link = feed_link | ||
|
||
@classmethod | ||
def from_json(cls, data): | ||
article_data = { | ||
'id': data['id'], | ||
'title': data['title'], | ||
'categories': data['categories'], | ||
'published': data['published'], | ||
'content': data.get('summary', {}).get('content'), | ||
'author': data.get('author'), | ||
} | ||
links = [item['href'] for item in data['canonical']] | ||
article_data['link'] = links[0] if links else '' | ||
|
||
# feed info | ||
article_data.update({ | ||
'feed_id': data['origin']['streamId'], | ||
'feed_title': normalize_whitespace(data['origin']['title']), | ||
'feed_link': data['origin']['htmlUrl'], | ||
}) | ||
|
||
return cls(**article_data) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
# coding: utf-8 | ||
from __future__ import print_function, unicode_literals | ||
|
||
from uuid import uuid4 | ||
from operator import itemgetter | ||
try: # python2 | ||
from urlparse import urljoin | ||
from urllib import quote_plus | ||
except ImportError: # python3 | ||
from urllib.parse import urljoin, quote_plus | ||
|
||
import requests | ||
|
||
from .consts import BASE_URL, LOGIN_URL | ||
from .exception import NotLoginError, APIError | ||
from .article import Article | ||
|
||
|
||
class InoreaderClient(object): | ||
|
||
def __init__(self, app_id, app_key, auth_token=None): | ||
self.app_id = app_id | ||
self.app_key = app_key | ||
self.auth_token = auth_token | ||
self.session = requests.Session() | ||
self.session.headers.update({ | ||
'AppId': self.app_id, | ||
'AppKey': self.app_key, | ||
'Authorization': 'GoogleLogin auth={}'.format(self.auth_token) | ||
}) | ||
self.userid = None if not self.auth_token else self.userinfo()['userId'] | ||
|
||
def userinfo(self): | ||
if not self.auth_token: | ||
raise NotLoginError | ||
|
||
url = urljoin(BASE_URL, 'user-info') | ||
resp = self.session.post(url) | ||
if resp.status_code != 200: | ||
raise APIError(resp.text) | ||
|
||
return resp.json() | ||
|
||
def login(self, username, password): | ||
resp = self.session.get(LOGIN_URL, params={'Email': username, 'Passwd': password}) | ||
if resp.status_code != 200: | ||
return False | ||
|
||
for line in resp.text.split('\n'): | ||
if line.startswith('Auth'): | ||
self.auth_token = line.replace('Auth=', '').strip() | ||
|
||
return bool(self.auth_token) | ||
|
||
def get_folders(self): | ||
if not self.auth_token: | ||
raise NotLoginError | ||
|
||
url = urljoin(BASE_URL, 'tag/list') | ||
params = {'types': 1, 'counts': 1} | ||
resp = self.session.post(url, params=params) | ||
if resp.status_code != 200: | ||
raise APIError(resp.text) | ||
|
||
folders = [] | ||
for item in resp.json()['tags']: | ||
if item.get('type') != 'folder': | ||
continue | ||
|
||
folder_name = item['id'].split('/')[-1] | ||
folders.append({'name': folder_name, 'unread_count': item['unread_count']}) | ||
|
||
folders.sort(key=itemgetter('name')) | ||
return folders | ||
|
||
def get_tags(self): | ||
if not self.auth_token: | ||
raise NotLoginError | ||
|
||
url = urljoin(BASE_URL, 'tag/list') | ||
params = {'types': 1, 'counts': 1} | ||
resp = self.session.post(url, params=params) | ||
if resp.status_code != 200: | ||
raise APIError(resp.text) | ||
|
||
tags = [] | ||
for item in resp.json()['tags']: | ||
if item.get('type') != 'tag': | ||
continue | ||
|
||
folder_name = item['id'].split('/')[-1] | ||
tags.append({'name': folder_name, 'unread_count': item['unread_count']}) | ||
|
||
tags.sort(key=itemgetter('name')) | ||
return tags | ||
|
||
def fetch_unread(self, folder=None, tags=None): | ||
if not self.auth_token: | ||
raise NotLoginError | ||
|
||
url = urljoin(BASE_URL, 'stream/contents/') | ||
if folder: | ||
url = urljoin( | ||
url, | ||
quote_plus('user/{}/label/{}'.format(self.userid, folder)) | ||
) | ||
params = { | ||
'xt': 'user/{}/state/com.google/read'.format(self.userid), | ||
'c': str(uuid4()) | ||
} | ||
|
||
resp = self.session.post(url, params=params) | ||
if resp.status_code != 200: | ||
raise APIError(resp.text) | ||
|
||
for data in resp.json()['items']: | ||
categories = set([ | ||
category.split('/')[-1] for category in data.get('categories', []) | ||
if category.find('label') > 0 | ||
]) | ||
if tags and not categories.issuperset(set(tags)): | ||
continue | ||
yield Article.from_json(data) | ||
|
||
continuation = resp.json().get('continuation') | ||
while continuation: | ||
params['c'] = continuation | ||
resp = self.session.post(url, params=params) | ||
if resp.status_code != 200: | ||
raise APIError(resp.text) | ||
for data in resp.json()['items']: | ||
categories = set([ | ||
category.split('/')[-1] for category in data.get('categories', []) | ||
if category.find('label') > 0 | ||
]) | ||
if tags and not categories.issuperset(set(tags)): | ||
continue | ||
yield Article.from_json(data) | ||
continuation = resp.json().get('continuation') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# coding: utf-8 | ||
BASE_URL = 'https://www.inoreader.com/reader/api/0/' | ||
LOGIN_URL = 'https://www.inoreader.com/accounts/ClientLogin' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
class NotLoginError(ValueError): | ||
def __repr__(self): | ||
return '<NotLoginError>' | ||
|
||
|
||
class APIError(ValueError): | ||
def __repr__(self): | ||
return '<APIError>' |
Oops, something went wrong.