Skip to content

Commit 254fe49

Browse files
committed
first release
0 parents  commit 254fe49

File tree

13 files changed

+570
-0
lines changed

13 files changed

+570
-0
lines changed

.gitignore

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
build/
12+
develop-eggs/
13+
dist/
14+
downloads/
15+
eggs/
16+
.eggs/
17+
lib/
18+
lib64/
19+
parts/
20+
sdist/
21+
var/
22+
*.egg-info/
23+
.installed.cfg
24+
*.egg
25+
26+
# PyInstaller
27+
# Usually these files are written by a python script from a template
28+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
29+
*.manifest
30+
*.spec
31+
32+
# Django stuff:
33+
*.log
34+
local_settings.py
35+
36+
# Flask stuff:
37+
instance/
38+
.webassets-cache
39+
40+
# Scrapy stuff:
41+
.scrapy
42+
43+
# Sphinx documentation
44+
docs/_build/
45+
46+
# PyBuilder
47+
target/
48+
49+
# IPython Notebook
50+
.ipynb_checkpoints
51+
52+
# pyenv
53+
.python-version
54+
55+
# dotenv
56+
.env
57+
58+
# virtualenv
59+
env/
60+
venv/
61+
ENV/
62+
63+
# Spyder project settings
64+
.spyderproject

Makefile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
lint: clean
2+
flake8 inoreader --format=pylint || true
3+
4+
clean:
5+
- find . -iname "*__pycache__" | xargs rm -rf
6+
- find . -iname "*.pyc" | xargs rm -rf
7+
- rm cobertura.xml -f
8+
- rm testresult.xml -f
9+
- rm .coverage -f
10+
11+
venv:
12+
- virtualenv --python=$(shell which python3) --prompt '<venv:inoreader>' venv
13+
14+
deps: venv
15+
- venv/bin/pip install -U pip setuptools
16+
- venv/bin/pip install -r requirements.txt

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
Inoreader
2+
=========
3+
4+
Python wrapper of Inoreader API.
5+
6+
## Installation
7+
8+
1. Clone this repository and install it with `setup.py`
9+
10+
```shell
11+
python setup.py install
12+
```
13+
14+
2. Install with `pip` directly
15+
16+
```shell
17+
pip install git+https://github.com/Linusp/python-inoreader.git
18+
```
19+
20+
## Usage
21+
22+
1. [Register your application](https://www.inoreader.com/developers/register-app)
23+
24+
2. Set `appid` and `appkey` in your system, you can set them with environment variables like
25+
26+
```shell
27+
export INOREADER_APP_ID = 'your-app-id'
28+
export INOREADER_APP_KEY = 'your-app-key'
29+
```
30+
31+
or write them in `$HOME/.inoreader`, e.g.:
32+
```shell
33+
[auth]
34+
appid = your-app-id
35+
appkey = your-app-key
36+
```
37+
38+
3. Login to your Inoreader acount
39+
40+
```shell
41+
inoreader login
42+
```
43+
44+
3. Use the command line tool `inoreader` to do something, run `inoreader --help` for details

inoreader/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# coding: utf-8
2+
from .client import InoreaderClient
3+
4+
5+
__all__ = ['InoreaderClient']

inoreader/article.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# coding: utf-8
2+
from __future__ import print_function, unicode_literals
3+
4+
from .utils import normalize_whitespace, extract_text
5+
6+
7+
class Article(object):
8+
def __init__(self, id, title, categories, link,
9+
published=None, content=None, author=None,
10+
feed_id=None, feed_title=None, feed_link=None):
11+
self.id = id
12+
self.title = normalize_whitespace(title)
13+
self.categories = categories
14+
self.link = link
15+
self.published = published
16+
self.content = content.strip() if content else ''
17+
self.text = extract_text(self.content)
18+
self.author = author
19+
self.feed_id = feed_id
20+
self.feed_title = feed_title.strip()
21+
self.feed_link = feed_link
22+
23+
@classmethod
24+
def from_json(cls, data):
25+
article_data = {
26+
'id': data['id'],
27+
'title': data['title'],
28+
'categories': data['categories'],
29+
'published': data['published'],
30+
'content': data.get('summary', {}).get('content'),
31+
'author': data.get('author'),
32+
}
33+
links = [item['href'] for item in data['canonical']]
34+
article_data['link'] = links[0] if links else ''
35+
36+
# feed info
37+
article_data.update({
38+
'feed_id': data['origin']['streamId'],
39+
'feed_title': normalize_whitespace(data['origin']['title']),
40+
'feed_link': data['origin']['htmlUrl'],
41+
})
42+
43+
return cls(**article_data)

inoreader/client.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# coding: utf-8
2+
from __future__ import print_function, unicode_literals
3+
4+
from uuid import uuid4
5+
from operator import itemgetter
6+
try: # python2
7+
from urlparse import urljoin
8+
from urllib import quote_plus
9+
except ImportError: # python3
10+
from urllib.parse import urljoin, quote_plus
11+
12+
import requests
13+
14+
from .consts import BASE_URL, LOGIN_URL
15+
from .exception import NotLoginError, APIError
16+
from .article import Article
17+
18+
19+
class InoreaderClient(object):
20+
21+
def __init__(self, app_id, app_key, auth_token=None):
22+
self.app_id = app_id
23+
self.app_key = app_key
24+
self.auth_token = auth_token
25+
self.session = requests.Session()
26+
self.session.headers.update({
27+
'AppId': self.app_id,
28+
'AppKey': self.app_key,
29+
'Authorization': 'GoogleLogin auth={}'.format(self.auth_token)
30+
})
31+
self.userid = None if not self.auth_token else self.userinfo()['userId']
32+
33+
def userinfo(self):
34+
if not self.auth_token:
35+
raise NotLoginError
36+
37+
url = urljoin(BASE_URL, 'user-info')
38+
resp = self.session.post(url)
39+
if resp.status_code != 200:
40+
raise APIError(resp.text)
41+
42+
return resp.json()
43+
44+
def login(self, username, password):
45+
resp = self.session.get(LOGIN_URL, params={'Email': username, 'Passwd': password})
46+
if resp.status_code != 200:
47+
return False
48+
49+
for line in resp.text.split('\n'):
50+
if line.startswith('Auth'):
51+
self.auth_token = line.replace('Auth=', '').strip()
52+
53+
return bool(self.auth_token)
54+
55+
def get_folders(self):
56+
if not self.auth_token:
57+
raise NotLoginError
58+
59+
url = urljoin(BASE_URL, 'tag/list')
60+
params = {'types': 1, 'counts': 1}
61+
resp = self.session.post(url, params=params)
62+
if resp.status_code != 200:
63+
raise APIError(resp.text)
64+
65+
folders = []
66+
for item in resp.json()['tags']:
67+
if item.get('type') != 'folder':
68+
continue
69+
70+
folder_name = item['id'].split('/')[-1]
71+
folders.append({'name': folder_name, 'unread_count': item['unread_count']})
72+
73+
folders.sort(key=itemgetter('name'))
74+
return folders
75+
76+
def get_tags(self):
77+
if not self.auth_token:
78+
raise NotLoginError
79+
80+
url = urljoin(BASE_URL, 'tag/list')
81+
params = {'types': 1, 'counts': 1}
82+
resp = self.session.post(url, params=params)
83+
if resp.status_code != 200:
84+
raise APIError(resp.text)
85+
86+
tags = []
87+
for item in resp.json()['tags']:
88+
if item.get('type') != 'tag':
89+
continue
90+
91+
folder_name = item['id'].split('/')[-1]
92+
tags.append({'name': folder_name, 'unread_count': item['unread_count']})
93+
94+
tags.sort(key=itemgetter('name'))
95+
return tags
96+
97+
def fetch_unread(self, folder=None, tags=None):
98+
if not self.auth_token:
99+
raise NotLoginError
100+
101+
url = urljoin(BASE_URL, 'stream/contents/')
102+
if folder:
103+
url = urljoin(
104+
url,
105+
quote_plus('user/{}/label/{}'.format(self.userid, folder))
106+
)
107+
params = {
108+
'xt': 'user/{}/state/com.google/read'.format(self.userid),
109+
'c': str(uuid4())
110+
}
111+
112+
resp = self.session.post(url, params=params)
113+
if resp.status_code != 200:
114+
raise APIError(resp.text)
115+
116+
for data in resp.json()['items']:
117+
categories = set([
118+
category.split('/')[-1] for category in data.get('categories', [])
119+
if category.find('label') > 0
120+
])
121+
if tags and not categories.issuperset(set(tags)):
122+
continue
123+
yield Article.from_json(data)
124+
125+
continuation = resp.json().get('continuation')
126+
while continuation:
127+
params['c'] = continuation
128+
resp = self.session.post(url, params=params)
129+
if resp.status_code != 200:
130+
raise APIError(resp.text)
131+
for data in resp.json()['items']:
132+
categories = set([
133+
category.split('/')[-1] for category in data.get('categories', [])
134+
if category.find('label') > 0
135+
])
136+
if tags and not categories.issuperset(set(tags)):
137+
continue
138+
yield Article.from_json(data)
139+
continuation = resp.json().get('continuation')

inoreader/consts.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# coding: utf-8
2+
BASE_URL = 'https://www.inoreader.com/reader/api/0/'
3+
LOGIN_URL = 'https://www.inoreader.com/accounts/ClientLogin'

inoreader/exception.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class NotLoginError(ValueError):
2+
def __repr__(self):
3+
return '<NotLoginError>'
4+
5+
6+
class APIError(ValueError):
7+
def __repr__(self):
8+
return '<APIError>'

0 commit comments

Comments
 (0)