Skip to content

Commit

Permalink
first release
Browse files Browse the repository at this point in the history
  • Loading branch information
Linusp committed Jul 1, 2018
0 parents commit 254fe49
Show file tree
Hide file tree
Showing 13 changed files with 570 additions and 0 deletions.
64 changes: 64 additions & 0 deletions .gitignore
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
16 changes: 16 additions & 0 deletions Makefile
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
44 changes: 44 additions & 0 deletions README.md
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
5 changes: 5 additions & 0 deletions inoreader/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# coding: utf-8
from .client import InoreaderClient


__all__ = ['InoreaderClient']
43 changes: 43 additions & 0 deletions inoreader/article.py
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)
139 changes: 139 additions & 0 deletions inoreader/client.py
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')
3 changes: 3 additions & 0 deletions inoreader/consts.py
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'
8 changes: 8 additions & 0 deletions inoreader/exception.py
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>'
Loading

0 comments on commit 254fe49

Please sign in to comment.