├── requirements.txt ├── .gitignore ├── .travis.yml ├── setup.py ├── habrahabr_test.py ├── README.md └── habrahabr.py /requirements.txt: -------------------------------------------------------------------------------- 1 | simplejson 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | # создаются при сборке 4 | MANIFEST 5 | build/ 6 | dist/ 7 | 8 | # потом выложу 9 | habrahabr_test.py 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 2.6 5 | - 2.7 6 | - 3.2 7 | - 3.3 8 | 9 | install: 10 | - pip install -r requirements.txt 11 | 12 | script: 13 | - python habrahabr_test.py 14 | 15 | notifications: 16 | email: 17 | - kafemanw@gmail.com 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2013 Kafeman 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from distutils.core import setup 18 | 19 | 20 | setup( 21 | name='habrahabr', 22 | version='0.0.1', 23 | description='Неофициальный клиент для API Хабрахабра, написанный на Python', 24 | author='Kafeman', 25 | author_email='kafemanw@gmail.com', 26 | url='https://github.com/kafeman/habrahabr-python', 27 | py_modules=['habrahabr'], 28 | ) 29 | -------------------------------------------------------------------------------- /habrahabr_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2014 Kafeman 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import unittest 18 | 19 | import habrahabr 20 | 21 | 22 | class TestCase(unittest.TestCase): 23 | def assert_is_instance(self, obj, cls): 24 | if not isinstance(obj, cls): 25 | msg = '%s is not an instance of %r' % (safe_repr(obj), cls) 26 | self.fail(msg) 27 | 28 | 29 | class ApiTestCase(TestCase): 30 | def setUp(self): 31 | self.api = habrahabr.Api(token='', client='') 32 | 33 | def test_comments(self): 34 | self.assert_is_instance(self.api.comments, habrahabr.CommentService) 35 | 36 | def test_companies(self): 37 | self.assert_is_instance(self.api.companies, habrahabr.CompanyService) 38 | 39 | def test_feed(self): 40 | self.assert_is_instance(self.api.feed, habrahabr.FeedService) 41 | 42 | def test_hubs(self): 43 | self.assert_is_instance(self.api.hubs, habrahabr.HubService) 44 | 45 | def test_polls(self): 46 | self.assert_is_instance(self.api.polls, habrahabr.PollService) 47 | 48 | def test_posts(self): 49 | self.assert_is_instance(self.api.posts, habrahabr.PostService) 50 | 51 | def test_search(self): 52 | self.assert_is_instance(self.api.search, habrahabr.SearchService) 53 | 54 | def test_settings(self): 55 | self.assert_is_instance(self.api.settings, habrahabr.SettingsService) 56 | 57 | def test_tracker(self): 58 | self.assert_is_instance(self.api.tracker, habrahabr.TrackerService) 59 | 60 | def test_users(self): 61 | self.assert_is_instance(self.api.users, habrahabr.UserService) 62 | 63 | 64 | if __name__ == '__main__': 65 | unittest.main() 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # habrahabr-python - клиент API Хабрахабра на Python [![Build Status](https://travis-ci.org/kafeman/habrahabr-python.svg?branch=master)](https://travis-ci.org/kafeman/habrahabr-python) 2 | 3 | Хабрахабр - самое крупное в Рунете сообщество людей, занятых в индустрии высоких 4 | технологий. Уникальная аудитория, свежая информация, конструктивное общение и 5 | коллективное творчество - всё это делает Хабрахабр самым оригинальным 6 | IT-проектом в России. 7 | 8 | Данная библиотека предоставляет доступ к недавно появившемуся API Хабрахабра на 9 | языке Python. Она не является официальной и поддерживается пользователем 10 | [kafeman](http://habrahabr.ru/users/kafeman/) в личных интересах. 11 | 12 | ## Установка 13 | 14 | ```bash 15 | $ sudo pip install habrahabr 16 | ``` 17 | 18 | Или можно собрать самую последнюю версию из исходников. Это несложно, достаточно 19 | выполнить следующие команды: 20 | 21 | ```bash 22 | $ git clone https://github.com/kafeman/habrahabr-python && cd habrahabr-python 23 | $ sudo python setup.py install 24 | ``` 25 | 26 | Теперь библиотека готова к использованию! 27 | 28 | ## Быстрый старт 29 | 30 | Перед началом работы с API Хабрахабра потребуется пройти несколько несложных 31 | этапов. 32 | 33 | 1. **Получение идентификатора приложения** 34 | 35 | Воспользовавшись [этой формой](http://habrahabr.ru/feedback/?type=8) на 36 | Хабрахабре, нужно кратко описать суть нового приложения и цель, для которой 37 | ему нужен API. 38 | 39 | Через некоторое время будет получен идентификатор и секрет нового приложения. 40 | *Держите секрет в секрете и никому его не давайте!* 41 | 42 | 2. **Получение токена пользователя** 43 | 44 | Каждое приложение может работать с API Хабра только от имени установившего 45 | его пользователя. 46 | 47 | Для получения токена можно воспользоваться следующим простым способом. 48 | Перейдите по следующей ссылке 49 | 50 | https://auth.habrahabr.ru/o/login/?redirect_uri=САЙТ&response_type=token&client_id=КЛИЕНТ 51 | 52 | поставив адрес сайта приложения вместо `САЙТ` и полученный на первом шаге 53 | идентификатор вместо `КЛИЕНТ`. 54 | 55 | После нажатия кнопки "Разрешить", Хабр выполнит перенаправление на `САЙТ`, 56 | добавив в конец адреса строку `#token=...`, которая и будет содержать 57 | требуемый токен. 58 | 59 | 3. **Создание тестового приложения** 60 | 61 | Дошедший до этого шага ужее имеет всю мощь API Хабрахабра. Теперь самое время 62 | воспользоваться библиотеку. Для начала инициализируем ее: 63 | 64 | ```python 65 | import habrahabr 66 | api = habrahabr.Api(client='ваш ClientID', 67 | token='ваш токен') 68 | ``` 69 | 70 | Все дальнейшее взаимодействие с Хабром теперь будет происходить через объект 71 | `api`. Для примера, получим данные о пользователе "kafeman": 72 | 73 | ```python 74 | author = api.users.get('kafeman') 75 | ``` 76 | 77 | Переменная `author` теперь содержит объект с данными об авторе библиотеки. 78 | 79 | ## Полная документация 80 | 81 | К сожалению, какая-либо официальная документация API Хабрахабра отсутствует, а 82 | настоящая библиотека была написана командой первоклассных телепатов. 83 | 84 | Однако, кое-что все таки получить можно. Если выполнить следующую команду, то вы 85 | сможете узнать назначение практически всех методов библиотеки. 86 | 87 | ```bash 88 | pydoc habrahabr 89 | ``` 90 | 91 | ## Лицензия 92 | 93 | Библиотека доступна на условиях лицензии Apache версии 2.0 94 | 95 | http://www.apache.org/licenses/LICENSE-2.0 96 | -------------------------------------------------------------------------------- /habrahabr.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2013 Kafeman 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import json 18 | import urllib 19 | 20 | 21 | ENDPOINT = 'https://api.habrahabr.ru/v1' 22 | OAUTH_URL = 'https://habrahabr.ru/auth/o/login/' 23 | 24 | 25 | def _fetch_url(url, method, data, headers): 26 | if data is not None: 27 | data = urllib.urlencode(data) 28 | request = urllib.request.Request(url, data, headers) 29 | request.get_method = lambda: method 30 | response = urllib.request.urlopen(request) 31 | return response.read() 32 | 33 | 34 | class Service: 35 | """Абстрактный сервис для наследования.""" 36 | def __init__(self, **settings): 37 | """Инициализировать сервис.""" 38 | if 'client' not in settings or 'token' not in settings: 39 | raise ValueError('Нужно указать ID клиента и токен') 40 | self.settings = settings 41 | 42 | def request(self, resource, method='GET', data=None): 43 | """Послать запрос на сервер Хабрахабра и распарсить ответ.""" 44 | url = self.settings.get('endpoint') + resource 45 | headers = { 'client': self.settings.get('client'), 46 | 'token': self.settings.get('token') } 47 | resp = _fetch_url(url, method, data, headers) 48 | return json.loads(resp) 49 | 50 | 51 | class CommentService(Service): 52 | """Сервис для работы с комментариями.""" 53 | def get(self, post_id): 54 | """Получить комментарии к посту.""" 55 | return self.request('/comments/%d' % post_id) 56 | 57 | def add(self, post_id, text, parent_id=0): 58 | """Добавить комментарий к посту.""" 59 | data = {'text': text, 'parent_id': parent_id} 60 | return self.request('/comments/%d' % post_id, method='PUT', data=data) 61 | 62 | def vote(self, comment_id, vote): 63 | """Проголосовать за комментарий.""" 64 | if vote not in [-1, 1]: 65 | raise ValueError('Неверный формат голоса') 66 | data = {'vote': vote} 67 | return self.request('/comments/%d/vote' % comment_id, method='PUT', data=data) 68 | 69 | def vote_plus(self, comment_id): 70 | """Проголосовать за комментарий положительно.""" 71 | return self.vote(comment_id, 1) 72 | 73 | def vote_minus(self, comment_id): 74 | """Проголосовать за комментарий отрицательно.""" 75 | return self.vote(comment_id, -1) 76 | 77 | 78 | class CompanyService(Service): 79 | """Сервис для работы с компаниями.""" 80 | def get_posts(self, alias, page=1): 81 | """Получить посты из корпоративного блога компании.""" 82 | return self.request('/company/%s?page=%d' % (alias, page)) 83 | 84 | def get_info(self, alias): 85 | """Получить информацию о компании.""" 86 | return self.request('/company/%s/info' % alias) 87 | 88 | 89 | class FeedService(Service): 90 | """Сервис для работы с лентой пользователя.""" 91 | def get_habred(self, page=1): 92 | """Получить захабренные посты.""" 93 | return self.request('/feed/habred?page=%d' % page) 94 | 95 | def get_unhabred(self, page=1): 96 | """Получить отхабренные посты.""" 97 | return self.request('/feed/unhabred?page=%d' % page) 98 | 99 | def get_new(self, page=1): 100 | """Получить новые посты.""" 101 | return self.request('/feed/new?page=%d' % page) 102 | 103 | 104 | class HubService(Service): 105 | """Сервис для работы с хабами.""" 106 | def get_info(self, alias): 107 | """Получить информацию о хабе.""" 108 | return self.request('/hub/%s/info' % alias) 109 | 110 | def get_habred(self, alias, page=1): 111 | """Получить захабренные посты.""" 112 | return self.request('/hub/%s/habred?page=%d' % (alias, page)) 113 | 114 | def get_unhabred(self, alias, page=1): 115 | """Получить отхабренные посты.""" 116 | return self.request('/hub/%s/unhabred?page=%d' % (alias, page)) 117 | 118 | def get_new(self, alias, page=1): 119 | """Получить новые посты.""" 120 | return self.request('/hub/%s/new?page=%d' % (alias, page)) 121 | 122 | def get_all(self, page=1): 123 | """Получить все хабы.""" 124 | return self.request('/hubs?page=%d' % page) 125 | 126 | def get_categories(self): 127 | """Получить категории хабов.""" 128 | return self.request('/hubs/categories') 129 | 130 | def get_by_category(self, category, page=1): 131 | """Получить хабы, относящиеся к указанной категории.""" 132 | return self.request('/hubs/categories/%s?page=%d' % (category, page)) 133 | 134 | def subscribe(self, alias): 135 | """Подписать пользователя на хаб.""" 136 | return self.request('/hub/%s' % alias, method='PUT') 137 | 138 | def unsubscribe(self, alias): 139 | """Отписать пользователя от хаба.""" 140 | return self.request('/hub/%s' % alias, method='DELETE') 141 | 142 | 143 | class PollService(Service): 144 | """Сервис для работы с опросами.""" 145 | def get(self, poll_id): 146 | """Получить опрос.""" 147 | return self.request('/polls/%d' % poll_id) 148 | 149 | def vote(self, poll_id, answer_id): 150 | """Проголосовать за один или несколько ответов.""" 151 | pass 152 | 153 | 154 | class PostService(Service): 155 | """Сервис для работы с постами.""" 156 | def get(self, post_id): 157 | """Получить информацию о посте по его ID.""" 158 | return self.request('/post/%d' % post_id) 159 | 160 | def vote(self, post_id, vote): 161 | """Проголосовать за пост. 162 | 163 | Этот метод отключен по-умолчанию. Для включения обратитесь в службу 164 | технической поддержки Хабрахабра. 165 | """ 166 | if vote not in [-1, 0, 1]: 167 | raise ValueError('Неверный формат голоса') 168 | data = {'vote': vote} 169 | return self.request('/post/%d/vote' % post_id, method='PUT', data=data) 170 | 171 | def vote_plus(self, post_id): 172 | """Проголосовать за пост положительно. 173 | 174 | Этот метод отключен по-умолчанию. Для включения обратитесь в службу 175 | технической поддержки Хабрахабра. 176 | """ 177 | return self.vote(post_id, 1) 178 | 179 | def vote_minus(self, post_id): 180 | """Проголосовать за пост отрицательно. 181 | 182 | Этот метод отключен по-умолчанию. Для включения обратитесь в службу 183 | технической поддержки Хабрахабра. 184 | """ 185 | return self.vote(post_id, -1) 186 | 187 | def vote_neutral(self, post_id): 188 | """Проголосовать за пост нейтрально. 189 | 190 | Этот метод отключен по-умолчанию. Для включения обратитесь в службу 191 | технической поддержки Хабрахабра. 192 | """ 193 | return self.vote(post_id, 0) 194 | 195 | def add_to_favorite(self, post_id): 196 | """Добавить пост в избранное.""" 197 | return self.request('/post/%d/favorite' % post_id, method='PUT') 198 | 199 | def remove_from_favorite(self, post_id): 200 | """Удалить пост из избранного.""" 201 | return self.request('/post/%d/favorite' % post_id, method='DELETE') 202 | 203 | def increment_view_counter(self, post_id): 204 | """Увеличить счетчик просмотров.""" 205 | return self.request('/post/%d/viewcount' % post_id, method='PUT') 206 | 207 | def get_meta(self, post_ids): 208 | """Получить мета-информацию постов. 209 | 210 | В настоящий момент можно запрашивать не более 30 постов за раз. 211 | """ 212 | ids = ','.join(post_ids) 213 | return self.request('/posts/meta?ids=%s' % ids) 214 | 215 | 216 | class SearchService(Service): 217 | """Сервис для поиска.""" 218 | def search_posts(self, query, page=1): 219 | """Искать в постах.""" 220 | return self.request('/search/posts/%s?page=%d' % (query, page)) 221 | 222 | def search_users(self, query, page=1): 223 | """Искать в пользователях.""" 224 | return self.request('/search/users/%s?page=%d' % (query, page)) 225 | 226 | def search_hubs(self, query): 227 | """Искать в хабах.""" 228 | return self.request('/hubs/search/%s' % query) 229 | 230 | 231 | class SettingsService(Service): 232 | """Сервис для настроек.""" 233 | def accept_agreement(self, query, page=1): 234 | """Принять соглашение.""" 235 | return self.request('/settings/agreement', method='PUT') 236 | 237 | 238 | class TrackerService(Service): 239 | """Сервис для работы с трекером пользователя.""" 240 | def push(self, title, text): 241 | """Отправить уведомление в раздел "Приложения".""" 242 | data = {'title': title, 'text': text} 243 | return self.request('/tracker', method='PUT', data=data) 244 | 245 | def get_counters(self): 246 | """Получить количество новых уведомлений. 247 | 248 | Уведомления не отмечаются как просмотренные. 249 | """ 250 | return self.request('/tracker/counters') 251 | 252 | def get_posts(self): 253 | """Получить уведомления из раздела "Посты". 254 | 255 | Уведомления не отмечаются как просмотренные.""" 256 | return self.request('/tracker/posts') 257 | 258 | def get_subscribers(self): 259 | """Получить уведомления из раздела "Подписчики". 260 | 261 | Уведомления не отмечаются как просмотренные.""" 262 | return self.request('/tracker/subscribers') 263 | 264 | def get_mentions(self): 265 | """Получить уведомления из раздела "Упоминания". 266 | 267 | Уведомления не отмечаются как просмотренные.""" 268 | return self.request('/tracker/mentions') 269 | 270 | def get_apps(self): 271 | """Получить уведомления из раздела "Приложения". 272 | 273 | Уведомления не отмечаются как просмотренные.""" 274 | return self.request('/tracker/apps') 275 | 276 | 277 | class UserService(Service): 278 | """Сервис для работы с пользователями.""" 279 | def get(self, username=None): 280 | """Получить информацию о пользователе или список из 100 пользователей с 281 | самым высоким рейтингом.""" 282 | if username: 283 | return self.request('/users/%s' % username) 284 | return self.request('/users') 285 | 286 | def get_current_user(self): 287 | """Получить информацию о текущем пользователе.""" 288 | return self.get('me') 289 | 290 | def get_comments(self, username, page=1): 291 | """Получить комментарии пользователя.""" 292 | return self.request('/users/%s/comments?page=%d' % (username, page)) 293 | 294 | def get_posts(self, username): 295 | """Получить посты пользователя.""" 296 | return self.request('/users/%s/posts?page=%d' % (username, page)) 297 | 298 | def get_hubs(self, username): 299 | """Получить хабы на которые подписан пользователь.""" 300 | return self.request('/users/%s/hubs' % username) 301 | 302 | def get_companies(self, username, page=1): 303 | """Получить компании, в которых работает пользователь.""" 304 | return self.request('/users/%s/companies' % username) 305 | 306 | def get_followers(self, username, page=1): 307 | """Получить подписчиков пользователя.""" 308 | return self.request('/users/%s/followers?page=%d' % (username, page)) 309 | 310 | def get_followed(self, username, page=1): 311 | """Получить пользователей, которые подписаны на данного пользователя.""" 312 | return self.request('/users/%s/followed?page=%d' % (username, page)) 313 | 314 | def vote_plus(self, username): 315 | """Проголосовать за карму положительно. 316 | 317 | Этот метод отключен по-умолчанию. Для включения обратитесь в службу 318 | технической поддержки Хабрахабра. 319 | """ 320 | return self.request('/users/%s/vote' % username, method='PUT') 321 | 322 | def vote_minus(self, username): 323 | """Проголосовать за карму отрицательно. 324 | 325 | Этот метод отключен по-умолчанию. Для включения обратитесь в службу 326 | технической поддержки Хабрахабра. 327 | """ 328 | return self.request('/users/%s/vote' % username, method='DELETE') 329 | 330 | def get_favorite_comments(self, username, page=1): 331 | """Получить избранные комментарии пользователя.""" 332 | return self.request('/users/%s/favorites/comments?page=%d' % (username, page)) 333 | 334 | def get_favorite_posts(self, username, page=1): 335 | """Получить избранные посты пользователя.""" 336 | return self.request('/users/%s/favorites/posts?page=%d' % (username, page)) 337 | 338 | 339 | class Api: 340 | """Базовый класс для работы с API Хабрахабра.""" 341 | def __init__(self, **settings): 342 | """Инициализирует клиента API.""" 343 | self._settings = settings 344 | if 'endpoint' not in self._settings: 345 | self._settings['endpoint'] = ENDPOINT 346 | # TODO(kafeman): Сделать "ленивую" инициализацию, чтобы сервисы 347 | # создавались только при необходимости и кешировались 348 | self.comments = CommentService(**self._settings) 349 | self.companies = CompanyService(**self._settings) 350 | self.feed = FeedService(**self._settings) 351 | self.hubs = HubService(**self._settings) 352 | self.polls = PollService(**self._settings) 353 | self.posts = PostService(**self._settings) 354 | self.search = SearchService(**self._settings) 355 | self.settings = SettingsService(**self._settings) 356 | self.tracker = TrackerService(**self._settings) 357 | self.users = UserService(**self._settings) 358 | 359 | def get_authorization_url(self, redirect_uri, response_type='code'): 360 | """Составить URL для запроса доступа.""" 361 | data = { 'redirect_uri': redirect_uri, 362 | 'response_type': response_type, 363 | 'client_id': self._settings.get('client') } 364 | return OAUTH_URL + '?' + urllib.urlencode(data) 365 | --------------------------------------------------------------------------------