├── .gitignore ├── README.md ├── README.rst ├── douban_client ├── __init__.py ├── api │ ├── __init__.py │ ├── album.py │ ├── base.py │ ├── book.py │ ├── comment.py │ ├── discussion.py │ ├── doumail.py │ ├── error.py │ ├── event.py │ ├── guess.py │ ├── miniblog.py │ ├── movie.py │ ├── music.py │ ├── note.py │ ├── online.py │ ├── photo.py │ ├── review.py │ ├── subject.py │ └── user.py └── client.py ├── examples ├── auth_with_code.py ├── auth_with_password.py └── auth_with_token.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── douban.png ├── framework.py ├── run.py ├── test_api_album.py ├── test_api_book.py ├── test_api_discussion.py ├── test_api_doumail.py ├── test_api_error.py ├── test_api_event.py ├── test_api_guess.py ├── test_api_miniblog.py ├── test_api_movie.py ├── test_api_music.py ├── test_api_note.py ├── test_api_online.py ├── test_api_photo.py └── test_api_user.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | tests/local_config.py 4 | douban_client/scope.py 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | 19 | # Installer logs 20 | pip-log.txt 21 | 22 | # Unit test / coverage reports 23 | .coverage 24 | .tox 25 | 26 | #Translations 27 | *.mo 28 | 29 | #Mr Developer 30 | .mr.developer.cfg 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## douban-client 2 | 3 | douban-client 是对豆瓣 API v2 接口进行了一个简单封装,主要包括了 OAuth 2.0 认证、图片上传以及接口方面的调用。 4 | 5 | 目前已完成的接口有: 6 | ``` 7 | * 用户 User 8 | * 广播 Miniblog 9 | * 豆邮 Doumail 10 | * 日记 Note 11 | * 相册 Album 12 | * 图片 Photo 13 | * 读书 Book 14 | * 电影 Movie 15 | * 音乐 Music 16 | * 线上活动 Online 17 | * 同城活动 Event 18 | * 论坛 Discussion 19 | * 回复 Comment 20 | ``` 21 | 22 | ### 安装 23 | ``` 24 | pip install douban-client 25 | ``` 26 | 27 | 或 28 | ``` 29 | easy_install douban-client 30 | ``` 31 | 32 | ### 使用说明 33 | 34 | #### OAuth 2.0 认证 35 | ``` 36 | from douban_client import DoubanClient 37 | 38 | API_KEY = 'your api key' 39 | API_SECRET = 'your api secret' 40 | 41 | # 在 OAuth 2.0 中, 42 | # 获取权限需要指定相应的 scope,请注意!! 43 | # scope 权限可以在申请应用的 "API 权限" 查看。 44 | 45 | SCOPE = 'douban_basic_common,shuo_basic_r,shuo_basic_w' 46 | 47 | client = DoubanClient(API_KEY, API_SECRET, your_redirect_uri, SCOPE) 48 | 49 | # 以下方式 2 选 1: 50 | # 1. 引导用户授权 51 | print 'Go to the following link in your browser:' 52 | print client.authorize_url 53 | code = raw_input('Enter the verification code:') 54 | client.auth_with_code(code) 55 | 56 | # 2. 如果有之前有 token,则可以 57 | client.auth_with_token(token) 58 | 59 | # Token Code 60 | token_code = client.token_code 61 | 62 | # Refresh Token 63 | # 请注意:`refresh_token_code` 值仅可在授权完成时获取(即在 `auth_with_code`, `auth_with_password` 之后) 64 | refresh_token_code = client.refresh_token_code 65 | client.refresh_token(refresh_token_code) # refresh token 66 | 67 | ``` 68 | 69 | 至此,已经完成 OAuth 2.0 授权。 70 | 71 | #### Douban API v2 说明 72 | ``` 73 | 1. 豆瓣Api V2认证统一使用OAuth2 74 | 2. 数据返回格式统一使用json,GData不再使用 75 | 3. 需要授权的Api,需要加access_token的Header,并且使用https协议,限制具体见OAuth2文档 76 | 4. 不需要授权公开api可以使用http,参数里面如果不包含apikey的话,限制单ip每分钟10次 77 | 5. Api里面的通配符,:id代表纯数字, :name代表由数字+字母+[-_.]这些特殊字符 78 | 6. 使用HTTP Status Code表示状态 79 | 7. 列表参数使用start和count 80 | 8. POST/PUT 时中文使用UTF-8编码 81 | 9. 时间格式:yyyy-MM-dd HH:mm:ss, 如"2007-06-28 11:16:11" 82 | ``` 83 | 84 | #### 接口说明 85 | 86 | 默认参数(参考豆瓣官方文档): 87 | ``` 88 | start: 0 89 | count: 20 90 | ``` 91 | 所有返回数据以豆瓣官方文档为准,各接口末尾处均有相应链接入口。 92 | 93 | 94 | __用户 User__ 95 | ``` 96 | # 以下 id 指用户数字 id 97 | 当前用户 client.user.me 98 | 指定用户 client.user.get(id) 99 | 搜索用户 client.user.search(q) # q: 搜索的关键词 100 | 101 | # 此处是将广播关系接口放置到用户 102 | 关注用户 client.user.follow(id) 103 | 取消关注 client.user.unfollow(id) 104 | 粉丝信息 client.user.followers(id, start, count) 105 | 关注信息 client.user.following(id, start, count) 106 | 加入黑名单 client.user.block(id) 107 | ``` 108 | 109 | 110 | 111 | 112 | 113 | 114 | __广播 Miniblog__ 115 | ``` 116 | # 以下 id 指广播数字 id 117 | 当前用户Timeline client.miniblog.home_timeline(count) 118 | 指定用户Timeline client.miniblog.user_timeline(user_id, count) 119 | 120 | 获取一条广播 client.miniblog.get(id) 121 | 新写一条广播 client.miniblog.new(text) 122 | 新写图片广播 client.miniblog.new(text, image=open('/path/pic.png')) 123 | 删除一条广播 client.miniblog.delete(id) 124 | 125 | 推荐网址 client.miniglog.rec(title='', url='', desc='', image='http://url.jpg') 126 | 127 | 获取某广播回复列表 client.miniblog.comments(id) 128 | 回复某条广播 client.miniblog.comment.new(id, text) 129 | 获取某条广播回复 client.miniblog.comment.get(comment_id) 130 | 删除某条广播回复 client.miniblog.comment.delete(comment_id) 131 | 132 | 赞广播 client.miniblog.like(id) 133 | 取消赞 client.miniblog.unlike(id) 134 | 赞某广播用户列表 client.miniblog.likers(id) 135 | 136 | 转发广播 client.miniblog.reshare(id) 137 | 转发某广播的用户列表 client.miniblog.resharers(id) 138 | 139 | ``` 140 | 141 | 142 | 143 | 144 | __豆邮 Doumail__ 145 | ``` 146 | # 以下 id 指豆邮数字 id 147 | # 豆邮发送过多会需要验证,请注意 148 | 获取一封豆邮 client.doumail.get(id) 149 | 新写一封豆邮 client.doumail.new(title, content, receiver_id) 150 | 151 | 标记一封豆邮 client.doumail.read(id) 152 | 批量标记豆邮 client.doumail.reads(ids) # ids 为 list 153 | 154 | 删除一封豆邮 client.doumail.delete(id) 155 | 批量删除豆邮 client.doumail.deletes(ids) # ids: [id, id, id] 156 | 157 | 豆邮收件箱列表 client.doumail.inbox(start, count) 158 | 豆邮发件箱列表 client.doumail.outbox(start, count) 159 | 未读豆邮列表 client.doumail.unread(start, count) 160 | 161 | ``` 162 | 163 | 164 | 165 | 166 | __日记 Note__ 167 | ``` 168 | # 以下 id 指日记数字 id 169 | # format: html_full, html_short, abstract, text,默认为text 170 | 获取一篇日记 client.note.get(id, format='text') 171 | 新写一篇日记 client.note.new(title, content) 172 | 更新一篇日记 client.note.update(title, content) 173 | 删除一篇日记 client.note.delete(id) 174 | 175 | 喜欢一篇日记 client.note.like(id) 176 | 取消喜欢一篇日记 client.note.unlike(id) 177 | 178 | 获取用户日记列表 client.note.list(user_id, start, count) 179 | 获取用户喜欢的日记列表 client.note.liked_list(user_id, start, count) 180 | 181 | 获取回复列表 client.note.comments(id, start, count) 182 | 新加一条回复 client.note.comment.new(id, content) 183 | 获取一条回复 client.note.comment.get(comment_id) 184 | 删除一条回复 client.note.comment.delete(comment_id) 185 | 186 | ``` 187 | 188 | 189 | 190 | 191 | __相册 Album__ 192 | ``` 193 | # 以下 id 指相册数字 id 194 | # desc 描述文字 195 | 获取一个相册 client.album.get(id) 196 | 新建一个相册 client.album.new(title, desc) 197 | 更新一个相册 client.album.update(id, title, desc) 198 | 删除一个相册 client.album.delete(id) 199 | 200 | 获取用户相册列表 client.album.list(user_id, start, count) 201 | 用户喜欢相册列表 client.album.liked_list(user_id, start, count) 202 | 获取相册图片列表 client.album.photos(id, start, count) 203 | 204 | 喜欢一个相册 client.album.like(id) 205 | 取消喜欢相册 client.album.unlike(id) 206 | 207 | ``` 208 | 209 | 210 | 211 | 212 | __图片 Photo__ 213 | ``` 214 | # 以下 id 指图片数字 id 215 | 获取一张图片 client.photo.get(id) 216 | 上传一张图片 client.photo.new(album_id, image, desc) # image = open('/path/pic.png') 217 | 更新图片描述 client.photo.update(id, desc) # desc 为描述文字 218 | 删除一条图片 client.photo.delete(id) 219 | 220 | 喜欢一张图片 client.photo.like(id) 221 | 取消喜欢图片 client.photo.unlike(id) 222 | 223 | 获取回复列表 client.photo.comments(id, start, count) 224 | 新加一条回复 client.photo.comment.new(id, content) 225 | 获取一条回复 client.photo.comment.get(comment_id) 226 | 删除一条回复 client.photo.comment.delete(comment_id) 227 | ``` 228 | 229 | 230 | 231 | 232 | __读书 Book__ 233 | ``` 234 | # 以下 id 指图书条目数字 id 235 | # q: 关键词, tag: 标签 236 | 获取一本图书信息 client.book.get(id) 237 | 通过isbn获取信息 client.book.isbn(isbn_number) 238 | 搜索图书信息 client.book.search(q, tag, start, count) 239 | 240 | 获取图书标签 client.book.tags(id) 241 | 获取用户标签 client.book.tagged_list(user_id) 242 | 243 | 发表一条书评 client.book.review.new(id, title, content) 244 | 更新一条书评 client.book.review.update(review_id, title, content) 245 | 删除一条书评 client.book.review.delete(review_id) 246 | 247 | ``` 248 | 249 | 250 | 251 | 252 | __电影 Movie__ 253 | ``` 254 | # 以下 id 指电影条目数字 id 255 | # q: 关键词, tag: 标签 256 | 获取一部电影信息 client.movie.get(id) 257 | 获取影人信息 client.movie.celebrity(celebrity_id) 258 | 通过imdb获取电影 client.movie.imdb(imdb_number) 259 | 搜索电影信息 client.movie.search(q, tag, start, count) 260 | 261 | 获取电影标签 client.movie.tags(id) 262 | 获取用户标签 client.movie.tagged_list(user_id) 263 | 264 | 发表一条影评 client.movie.review.new(id, title, content) 265 | 更新一条影评 client.movie.review.update(review_id, title, content) 266 | 删除一条影评 client.movie.review.delete(review_id) 267 | 268 | ``` 269 | 270 | 271 | 272 | 273 | __音乐 Music__ 274 | ``` 275 | # 以下 id 指音乐条目数字 id 276 | # q: 关键词, tag: 标签 277 | 获取音乐信息 client.music.get(id) 278 | 搜索音乐信息 client.music.search(q, tag, start, count) 279 | 280 | 获取音乐标签 client.music.tags(id) 281 | 获取用户标签 client.music.tagged_list(user_id) 282 | 283 | 发表一条乐评 client.music.review.new(id, title, content) 284 | 更新一条乐评 client.music.review.update(review_id, title, content) 285 | 删除一条乐评 client.music.review.delete(review_id) 286 | 287 | ``` 288 | 289 | 290 | 291 | 292 | __线上活动 Online__ 293 | ``` 294 | # 以下 id 指线上活动数字 id 295 | # begin_time, end_time 格式为 '%Y-%m-%d %H:%M:%S' 296 | # cate 可选值: day, week, latest 297 | 获取一条线上活动 client.online.get(id) 298 | 发表一条线上活动 client.online.new(title, desc, begin_time, end_time) 299 | 更新一条线上活动 client.online.update(title, desc, begin_time, end_time) 300 | 删除一条线上活动 client.online.delete(id) 301 | 302 | 参加一条线上活动 client.online.join(id) 303 | 取消参加线上活动 client.online.quit(id) 304 | 305 | 喜欢一条线上活动 client.online.like(id) 306 | 取消喜欢线上活动 client.online.unlike(id) 307 | 308 | 获取线上活动图片列表 client.online.photos(id, start, count) 309 | 上传图片到线上活动 client.online.upload(id, image) # image = open('xxx.jpg') 310 | 311 | 获取线上活动讨论列表 client.online.discussions(id, start, count) 312 | 在线上活动新发讨论 client.online.discussion.new(id, title, content) 313 | 314 | 获取参加线上活动成员列表 client.online.participants(id, start, count) 315 | 316 | 获取线上活动列表 client.online.list(cate, start, end) 317 | 获取参加过的活动 client.online.joined(user_id, start, count) 318 | 获取创建过的活动 client.online.created(user_id, start, count) 319 | 320 | ``` 321 | 322 | 323 | 324 | 325 | __同城活动 Event__ 326 | ``` 327 | # 以下 id 指同城活动 id 328 | # q: 关键词, loc: 城市 329 | # day_type: future, week, weekend, today, tomorrow 330 | # type: all,music, film, drama, commonweal, salon, \ 331 | # exhibition, party, sports, travel, others 332 | 获取同城活动 client.event.get(id) 333 | 搜索同城活动 client.event.search(q, loc, start, count) 334 | 335 | 参加同城活动 client.event.join(id) 336 | 取消参加活动 client.event.quit(id) 337 | 338 | 对同城活动感兴趣 client.event.wish(id) 339 | 取消同城活动兴趣 client.event.unwish(id) 340 | 341 | 某同城活动参加者 client.event.participants(id, start, count) 342 | 某同城活动感兴趣者 client.event.wishers(id, start, count) 343 | 344 | 获取用户创建过的同城活动 client.event.owned(user_id, start, count) 345 | 获取用户参加过的同城活动 client.event.participated(user_id, start, count) 346 | 获取用户感兴趣的同城活动 client.event.wished(user_id, start, count) 347 | 348 | 获取同城活动列表 client.event.list(loc, day_type, type, start, count) 349 | ``` 350 | 351 | 352 | 353 | 354 | __论坛 Discussion__ 355 | ``` 356 | # 以下 id 指论坛帖子 id 357 | # target 指相应产品线(如 online, review 等) 358 | # target_id 指相应产品 id 359 | 获取帖子 client.discussion.get(id) 360 | 发表帖子 client.discussion.new(target, target_id, title, content) 361 | 更新帖子 client.discussion.update(id, title, content) 362 | 删除帖子 client.discussion.delete(id) 363 | 364 | 获取帖子列表 client.discussion.list(target, target_id) 365 | 366 | 获取回复列表 client.discussion.comments(id, start, count) 367 | 新加一条回复 client.discussion.comment.new(id, content) 368 | 获取某条回复 client.discussion.comment.get(comment_id) 369 | 删除某条回复 client.discussion.comment.delete(comment_id) 370 | ``` 371 | 372 | 373 | 374 | 已实现的接口中单元测试覆盖 90%+,如果文档中有没有说明的可以参考下: 375 | 376 | ### Changelog 377 | 378 | __v0.0.6 [2013-12-18]__ 379 | * 兼容 Python 3.x 380 | * 接口变更: 381 | ``` 382 | + note: upload_photo 383 | - user: block, friendships, follow_in_common 384 | - movie: celebrity_works 385 | - miniblog: mentions 386 | ``` 387 | 388 | __v0.0.5 [2013-04-26]__ 389 | * 修复文档中关于授权部分的错误 390 | * 修复 `refresh_token` 错误 391 | * DoubanClient 实例对象增加 `token_code`, `refresh_token_code` 属性 392 | 393 | __v0.0.4 [2013-04-07]__ 394 | * 增加 refresh token 方法 395 | 396 | __v0.0.3 [2012-10-23]__ 397 | * 解决 py-oauth2 与 python-oauth2 命名冲突 398 | * 同步更新同城活动 api 399 | 400 | __v0.0.2 [2012-09-07]__ 401 | * 与豆瓣官网同步,调整 people -> user 402 | * 获取创建线上活动接口变更 online.owned -> online.created 403 | * 去除已被删除的豆瓣猜接口 404 | 405 | __v0.0.1 [2012-09-06]__ 406 | * 根据豆瓣 API v2 文档,发布第一个版本 407 | 408 | ### 联系 409 | * 使用 douban-client 过程中遇到 bug, 可以到 [Issues](https://github.com/liluo/douban-client/issues) 反馈 410 | * 比较紧急的问题可以在 Douban 或者 Twitter @liluoliluo 411 | * 欢迎提 pull request 412 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | douban-client 2 | ============= 3 | 4 | Python client library for Douban APIs (OAuth 2.0) 5 | -------------------------------------------------------------------------------- /douban_client/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .client import DoubanClient 3 | 4 | VERSION = __version__ = '0.0.6' 5 | -------------------------------------------------------------------------------- /douban_client/api/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .user import User 4 | from .doumail import Doumail 5 | from .discussion import Discussion 6 | from .note import Note 7 | from .album import Album 8 | from .photo import Photo 9 | from .online import Online 10 | from .event import Event 11 | from .guess import Guess 12 | from .miniblog import Miniblog 13 | from .book import Book 14 | from .movie import Movie 15 | from .music import Music 16 | 17 | 18 | class DoubanAPI(object): 19 | 20 | def __repr__(self): 21 | return '' 22 | 23 | @property 24 | def user(self): 25 | return User(self.access_token) 26 | 27 | @property 28 | def doumail(self): 29 | return Doumail(self.access_token) 30 | 31 | @property 32 | def discussion(self): 33 | return Discussion(self.access_token) 34 | 35 | @property 36 | def note(self): 37 | return Note(self.access_token) 38 | 39 | @property 40 | def album(self): 41 | return Album(self.access_token) 42 | 43 | @property 44 | def photo(self): 45 | return Photo(self.access_token) 46 | 47 | @property 48 | def online(self): 49 | return Online(self.access_token) 50 | 51 | @property 52 | def event(self): 53 | return Event(self.access_token) 54 | 55 | @property 56 | def guess(self): 57 | return Guess(self.access_token) 58 | 59 | @property 60 | def miniblog(self): 61 | return Miniblog(self.access_token) 62 | 63 | @property 64 | def book(self): 65 | return Book(self.access_token) 66 | 67 | @property 68 | def movie(self): 69 | return Movie(self.access_token) 70 | 71 | @property 72 | def music(self): 73 | return Music(self.access_token) 74 | -------------------------------------------------------------------------------- /douban_client/api/album.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .base import DoubanAPIBase, DEFAULT_START, DEFAULT_COUNT 4 | 5 | 6 | class Album(DoubanAPIBase): 7 | 8 | def __repr__(self): 9 | return '' 10 | 11 | def get(self, id): 12 | return self._get('/v2/album/%s' % id) 13 | 14 | def new(self, title, desc='', order='desc', privacy='public'): 15 | return self._post('/v2/albums', 16 | title=title, desc=desc, 17 | order=order, privacy=privacy) 18 | 19 | def update(self, id, title='', desc='', order='desc', privacy='public'): 20 | return self._put('/v2/album/%s' % id, 21 | title=title, desc=desc, 22 | order=order, privacy=privacy) 23 | 24 | def delete(self, id): 25 | return self._delete('/v2/album/%s' % id) 26 | 27 | def list(self, user_id, start=DEFAULT_START, count=DEFAULT_COUNT): 28 | return self._get('/v2/album/user_created/%s' % user_id, 29 | start=start, count=count) 30 | 31 | def liked_list(self, user_id, start=DEFAULT_START, count=DEFAULT_COUNT): 32 | return self._get('/v2/album/user_liked/%s' % user_id, 33 | start=start, count=count) 34 | 35 | def photos(self, id, start=DEFAULT_START, count=DEFAULT_COUNT, order='', sortby='time'): 36 | return self._get('/v2/album/%s/photos' % id, 37 | start=start, count=count, order=order, sortby=sortby) 38 | 39 | def like(self, id): 40 | return self._post('/v2/album/%s/like' % id) 41 | 42 | def unlike(self, id): 43 | return self._delete('/v2/album/%s/like' % id) 44 | -------------------------------------------------------------------------------- /douban_client/api/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pyoauth2 import AccessToken 4 | 5 | from .error import DoubanAPIError, DoubanOAuthError 6 | 7 | DEFAULT_START = 0 8 | DEFAULT_COUNT = 20 9 | 10 | 11 | def check_execption(func): 12 | def _check(*arg, **kws): 13 | resp = func(*arg, **kws) 14 | if resp.status >= 400: 15 | if resp.status == 403: 16 | raise DoubanOAuthError(401, 'UNAUTHORIZED') 17 | else: 18 | raise DoubanAPIError(resp) 19 | body = resp.body 20 | if body: 21 | return resp.parsed 22 | return body 23 | return _check 24 | 25 | 26 | class DoubanAPIBase(object): 27 | 28 | def __init__(self, access_token): 29 | self.access_token = access_token 30 | if not isinstance(self.access_token, AccessToken): 31 | raise DoubanOAuthError(401, 'UNAUTHORIZED') 32 | 33 | def __repr__(self): 34 | return '' 35 | 36 | @check_execption 37 | def _get(self, url, **opts): 38 | return self.access_token.get(url, **opts) 39 | 40 | @check_execption 41 | def _post(self, url, **opts): 42 | return self.access_token.post(url, **opts) 43 | 44 | @check_execption 45 | def _put(self, url, **opts): 46 | return self.access_token.put(url, **opts) 47 | 48 | @check_execption 49 | def _patch(self, url, **opts): 50 | return self.access_token.patch(url, **opts) 51 | 52 | @check_execption 53 | def _delete(self, url, **opts): 54 | return self.access_token.delete(url, **opts) 55 | -------------------------------------------------------------------------------- /douban_client/api/book.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .subject import Subject 4 | 5 | 6 | class Book(Subject): 7 | 8 | target = 'book' 9 | 10 | def __repr__(self): 11 | return '' 12 | 13 | def isbn(self, isbn_id): 14 | return self._get('/v2/book/isbn/%s' % isbn_id) 15 | -------------------------------------------------------------------------------- /douban_client/api/comment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .base import DoubanAPIBase, DEFAULT_START, DEFAULT_COUNT 4 | 5 | 6 | class Comment(DoubanAPIBase): 7 | 8 | def __init__(self, access_token, target): 9 | self.access_token = access_token 10 | self.target = target 11 | 12 | def __repr__(self): 13 | return '' 14 | 15 | def list(self, target_id, start=DEFAULT_START, count=DEFAULT_COUNT): 16 | return self._get('/v2/%s/%s/comments' % (self.target, target_id), 17 | start=start, count=count) 18 | 19 | def new(self, target_id, content): 20 | return self._post('/v2/%s/%s/comments' % (self.target, target_id), 21 | content=content) 22 | 23 | def get(self, target_id, id): 24 | return self._get('/v2/%s/%s/comment/%s' % (self.target, target_id, id)) 25 | 26 | def delete(self, target_id, id): 27 | return self._delete('/v2/%s/%s/comment/%s' % (self.target, target_id, id)) 28 | -------------------------------------------------------------------------------- /douban_client/api/discussion.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .base import DoubanAPIBase, DEFAULT_START, DEFAULT_COUNT 4 | from .comment import Comment 5 | 6 | 7 | class Discussion(DoubanAPIBase): 8 | 9 | target = 'discussion' 10 | 11 | def __repr__(self): 12 | return '' 13 | 14 | def get(self, id): 15 | return self._get('/v2/discussion/%s' % id) 16 | 17 | def new(self, target, target_id, title, content): 18 | return self._post('/v2/%s/%s/discussions' % (target, target_id), 19 | title=title, content=content) 20 | 21 | def list(self, target, target_id, start=DEFAULT_START, count=DEFAULT_COUNT): 22 | return self._get('/v2/%s/%s/discussions' % (target, target_id), 23 | start=start, count=count) 24 | 25 | def update(self, id, title, content): 26 | return self._put('/v2/discussion/%s' % id, 27 | title=title, content=content) 28 | 29 | def delete(self, id): 30 | return self._delete('/v2/discussion/%s' % id) 31 | 32 | def comments(self, id, start=DEFAULT_START, count=DEFAULT_COUNT): 33 | return Comment(self.access_token, self.target).list(id, start=start, count=count) 34 | 35 | @property 36 | def comment(self): 37 | return Comment(self.access_token, self.target) 38 | -------------------------------------------------------------------------------- /douban_client/api/doumail.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .base import DoubanAPIBase, DEFAULT_START, DEFAULT_COUNT 4 | 5 | 6 | class Doumail(DoubanAPIBase): 7 | 8 | def __repr__(self): 9 | return '' 10 | 11 | def get(self, id): 12 | return self._get('/v2/doumail/%s' % id) 13 | 14 | def inbox(self, start=DEFAULT_START, count=DEFAULT_COUNT): 15 | return self._get('/v2/doumail/inbox', start=start, count=count) 16 | 17 | def outbox(self, start=DEFAULT_START, count=DEFAULT_COUNT): 18 | return self._get('/v2/doumail/outbox', start=start, count=count) 19 | 20 | def unread(self, start=DEFAULT_START, count=DEFAULT_COUNT): 21 | return self._get('/v2/doumail/inbox/unread', start=start, count=count) 22 | 23 | def read(self, id): 24 | return self._put('/v2/doumail/%s' % id, key='key') 25 | 26 | def reads(self, ids): 27 | if isinstance(ids, (list, tuple)): 28 | ids = ','.join(ids) 29 | return self._put('/v2/doumail/read', ids=ids) 30 | 31 | def delete(self, id): 32 | return self._delete('/v2/doumail/%s' % id) 33 | 34 | def deletes(self, ids): 35 | if isinstance(ids, (tuple, list)): 36 | ids = ','.join(ids) 37 | return self._post('/v2/doumail/delete', ids=ids) 38 | 39 | def new(self, title, content, receiver_id, captcha_token=None, captcha_string=None): 40 | return self._post('/v2/doumails', 41 | title=title, content=content, receiver_id=receiver_id, 42 | captcha_toke=captcha_token, captcha_string=captcha_string) 43 | -------------------------------------------------------------------------------- /douban_client/api/error.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class DoubanBaseError(Exception): 5 | def __str__(self): 6 | return "***%s (%s)*** %s" % (self.status, self.reason, self.msg) 7 | 8 | 9 | class DoubanOAuthError(DoubanBaseError): 10 | def __init__(self, status, reason, msg={}): 11 | self.status = status 12 | self.reason = reason 13 | self.msg = {} 14 | 15 | 16 | class DoubanAPIError(DoubanBaseError): 17 | 18 | def __init__(self, resp): 19 | self.status = resp.status 20 | self.reason = resp.reason 21 | self.msg = resp.parsed 22 | -------------------------------------------------------------------------------- /douban_client/api/event.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .base import DoubanAPIBase, DEFAULT_START, DEFAULT_COUNT 4 | 5 | 6 | class Event(DoubanAPIBase): 7 | 8 | def __repr__(self): 9 | return '' 10 | 11 | def get(self, id): 12 | return self._get('/v2/event/%s' % id) 13 | 14 | def list(self, loc, day_type=None, type=None, start=DEFAULT_START, count=DEFAULT_COUNT): 15 | return self._get('/v2/event/list', 16 | loc=loc, day_type=day_type, type=type, start=start, count=count) 17 | 18 | def search(self, q, loc, start=DEFAULT_START, count=DEFAULT_COUNT): 19 | return self._get('/v2/event/search', q=q, loc=loc) 20 | 21 | def join(self, id, participate_date=''): 22 | data = dict(participate_date=participate_date) if participate_date else {} 23 | return self._post('/v2/event/%s/participants' % id, **data) 24 | 25 | def quit(self, id, participate_date=''): 26 | data = dict(participate_date=participate_date) if participate_date else {} 27 | return self._delete('/v2/event/%s/participants' % id, **data) 28 | 29 | def wish(self, id): 30 | return self._post('/v2/event/%s/wishers' % id) 31 | 32 | def unwish(self, id): 33 | return self._delete('/v2/event/%s/wishers' % id) 34 | 35 | def participants(self, id, start=DEFAULT_START, count=DEFAULT_COUNT): 36 | return self._get('/v2/event/%s/participants' % id, 37 | start=start, count=count) 38 | 39 | def wishers(self, id, start=DEFAULT_START, count=DEFAULT_COUNT): 40 | return self._get('/v2/event/%s/wishers' % id, 41 | start=start, count=count) 42 | 43 | def owned(self, user_id, start=DEFAULT_START, count=DEFAULT_COUNT): 44 | return self._get('/v2/event/user_created/%s' % user_id, 45 | start=start, count=count) 46 | 47 | def participated(self, user_id, start=DEFAULT_START, count=DEFAULT_COUNT): 48 | return self._get('/v2/event/user_participated/%s' % user_id, 49 | start=start, count=count) 50 | 51 | def wished(self, user_id, start=DEFAULT_START, count=DEFAULT_COUNT): 52 | return self._get('/v2/event/user_wished/%s' % user_id, 53 | start=start, count=count) 54 | -------------------------------------------------------------------------------- /douban_client/api/guess.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .base import DoubanAPIBase, DEFAULT_START, DEFAULT_COUNT 4 | 5 | 6 | class Guess(DoubanAPIBase): 7 | 8 | def __repr__(self): 9 | return '' 10 | -------------------------------------------------------------------------------- /douban_client/api/miniblog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .base import DoubanAPIBase, DEFAULT_COUNT 4 | 5 | 6 | class Miniblog(DoubanAPIBase): 7 | 8 | def __repr__(self): 9 | return '' 10 | 11 | def get(self, id): 12 | return self._get('/shuo/v2/statuses/%s' % id) 13 | 14 | def new(self, text, image=None): 15 | files = dict(image=image) if image else dict() 16 | return self._post('/shuo/v2/statuses/', text=text, files=files) 17 | 18 | def rec(self, title='', url='', desc='', image=''): 19 | return self._post('/shuo/v2/statuses/', 20 | rec_title=title, rec_url=url, 21 | rec_desc=desc, rec_image=image) 22 | 23 | def delete(self, id): 24 | return self._delete('/shuo/v2/statuses/%s' % id) 25 | 26 | def home_timeline(self, count=DEFAULT_COUNT, since_id=None, until_id=None, category=None): 27 | return self._get('/shuo/v2/statuses/home_timeline', 28 | count=count, since_id=since_id, 29 | until_id=until_id, category=category) 30 | 31 | def user_timeline(self, user_id, since_id=None, until_id=None): 32 | return self._get('/shuo/v2/statuses/user_timeline/%s' % user_id, 33 | since_id=since_id, until_id=until_id) 34 | 35 | def like(self, id): 36 | return self._post('/shuo/v2/statuses/%s/like' % id) 37 | 38 | def unlike(self, id): 39 | return self._delete('/shuo/v2/statuses/%s/like' % id) 40 | 41 | def likers(self, id): 42 | return self._get('/shuo/v2/statuses/%s/like' % id) 43 | 44 | def reshare(self, id): 45 | return self._post('/shuo/v2/statuses/%s/reshare' % id) 46 | 47 | def unreshare(self, id): 48 | return self._delete('/shuo/v2/statuses/%s/reshare' % id) 49 | 50 | def reshareders(self, id): 51 | return self._get('/shuo/v2/statuses/%s/reshare' % id) 52 | 53 | def comments(self, id): 54 | return self._get('/shuo/v2/statuses/%s/comments' % id) 55 | 56 | @property 57 | def comment(self): 58 | return MiniblogComment(self.access_token) 59 | 60 | 61 | class MiniblogComment(DoubanAPIBase): 62 | 63 | def new(self, miniblog_id, text): 64 | return self._post('/shuo/v2/statuses/%s/comments' % miniblog_id, text=text) 65 | 66 | def get(self, id): 67 | return self._get('/shuo/v2/statuses/comment/%s' % id) 68 | 69 | def delete(self, id): 70 | return self._delete('/shuo/v2/statuses/comment/%s' % id) 71 | -------------------------------------------------------------------------------- /douban_client/api/movie.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .subject import Subject 4 | 5 | 6 | class Movie(Subject): 7 | 8 | target = 'movie' 9 | 10 | def __repr__(self): 11 | return '' 12 | 13 | def celebrity(self, celebrity_id): 14 | return self._get('/v2/movie/celebrity/%s' % celebrity_id) 15 | 16 | def imdb(self, imdb_id): 17 | return self._get('/v2/movie/imdb/%s' % imdb_id) 18 | -------------------------------------------------------------------------------- /douban_client/api/music.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .subject import Subject 4 | 5 | 6 | class Music(Subject): 7 | 8 | target = 'music' 9 | 10 | def __repr__(self): 11 | return '' 12 | -------------------------------------------------------------------------------- /douban_client/api/note.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .base import DoubanAPIBase, DEFAULT_START, DEFAULT_COUNT 4 | from .comment import Comment 5 | 6 | 7 | class Note(DoubanAPIBase): 8 | 9 | target = 'note' 10 | 11 | def __repr__(self): 12 | return '' 13 | 14 | def new(self, title, content, privacy='public', can_reply='true'): 15 | return self._post('/v2/notes', 16 | title=title, content=content, 17 | privacy=privacy, can_reply=can_reply) 18 | 19 | def get(self, id, format='text'): 20 | return self._get('/v2/note/%s' % id, format=format) 21 | 22 | def update(self, id, title, content, privacy='public', can_reply='true'): 23 | return self._put('/v2/note/%s' % id, 24 | title=title, content=content, 25 | privacy=privacy, can_reply=can_reply) 26 | 27 | def upload_photo(self, id, pid, image, content, layout=None, desc=None): 28 | kwargs = { 29 | 'pids': 'p_%s' % pid, 30 | 'content': content, 31 | 'layout_%s' % pid: layout, 32 | 'desc_%s' % pid: desc 33 | } 34 | files = { 35 | 'image_%s' % pid: image 36 | } 37 | return self._post('/v2/note/%s' % id, files=files, **kwargs) 38 | 39 | def delete(self, id): 40 | return self._delete('/v2/note/%s' % id) 41 | 42 | def like(self, id): 43 | return self._post('/v2/note/%s/like' % id) 44 | 45 | def unlike(self, id): 46 | return self._delete('/v2/note/%s/like' % id) 47 | 48 | def list(self, user_id, start=DEFAULT_START, count=DEFAULT_COUNT): 49 | return self._get('/v2/note/user_created/%s' % user_id, 50 | start=start, count=count) 51 | 52 | def liked_list(self, user_id, start=DEFAULT_START, count=DEFAULT_COUNT): 53 | return self._get('/v2/note/user_liked/%s' % user_id, 54 | start=start, count=count) 55 | 56 | def comments(self, id, start=DEFAULT_START, count=DEFAULT_COUNT): 57 | return Comment(self.access_token, self.target).list(id, start=start, count=count) 58 | 59 | @property 60 | def comment(self): 61 | return Comment(self.access_token, self.target) 62 | -------------------------------------------------------------------------------- /douban_client/api/online.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .base import DoubanAPIBase, DEFAULT_START, DEFAULT_COUNT 4 | 5 | 6 | class Online(DoubanAPIBase): 7 | 8 | def __repr__(self): 9 | return '' 10 | 11 | def get(self, id): 12 | return self._get('/v2/online/%s' % id) 13 | 14 | def new(self, title, desc, begin_time, end_time, 15 | related_url='', cascade_invite='false', tags=''): 16 | return self._post('/v2/onlines', 17 | title=title, desc=desc, tags=tags, 18 | begin_time=begin_time, end_time=end_time, 19 | related_url=related_url, cascade_invite=cascade_invite) 20 | 21 | def update(self, id, title, desc, begin_time, end_time, 22 | related_url='', cascade_invite='false', tags=''): 23 | return self._put('/v2/online/%s' % id, 24 | title=title, desc=desc, tags=tags, 25 | begin_time=begin_time, end_time=end_time, 26 | related_url=related_url, cascade_invite=cascade_invite) 27 | 28 | def delete(self, id): 29 | return self._delete('/v2/online/%s' % id) 30 | 31 | def join(self, id): 32 | return self._post('/v2/online/%s/participants' % id) 33 | 34 | def quit(self, id): 35 | return self._delete('/v2/online/%s/participants' % id) 36 | 37 | def photos(self, id, start=DEFAULT_START, count=DEFAULT_COUNT, order='', sortby='time'): 38 | return self._get('/v2/online/%s/photos' % id, 39 | start=start, count=count, order=order, sortby=sortby) 40 | 41 | def upload(self, id, image, desc=''): 42 | return self._post('/v2/online/%s/photos' % id, 43 | desc=desc, files={'image': image}) 44 | 45 | def like(self, id): 46 | return self._post('/v2/online/%s/like' % id) 47 | 48 | def unlike(self, id): 49 | return self._delete('/v2/online/%s/like' % id) 50 | 51 | def participants(self, id, start=DEFAULT_START, count=DEFAULT_COUNT): 52 | return self._get('/v2/online/%s/participants' % id, 53 | start=start, count=count) 54 | 55 | def discussions(self, id, start=DEFAULT_START, count=DEFAULT_COUNT): 56 | return self._get('/v2/online/%s/discussions' % id, 57 | start=start, count=count) 58 | 59 | @property 60 | def discussion(self): 61 | return OnlineDiscussion(self.access_token) 62 | 63 | def list(self, cate='day', start=DEFAULT_START, count=DEFAULT_COUNT): 64 | # cate: day, week, latest 65 | return self._get('/v2/onlines', cate=cate, start=start, count=count) 66 | 67 | def created(self, user_id, start=DEFAULT_START, count=DEFAULT_COUNT): 68 | return self._get('/v2/online/user_created/%s' % user_id, 69 | start=start, count=count) 70 | 71 | def joined(self, user_id, start=DEFAULT_START, count=DEFAULT_COUNT): 72 | return self._get('/v2/online/user_participated/%s' % user_id, 73 | start=start, count=count) 74 | 75 | 76 | class OnlineDiscussion(DoubanAPIBase): 77 | 78 | def new(self, target_id, title, content): 79 | return self._post('/v2/online/%s/discussions' % target_id, 80 | title=title, content=content) 81 | -------------------------------------------------------------------------------- /douban_client/api/photo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .base import DoubanAPIBase, DEFAULT_START, DEFAULT_COUNT 4 | from .comment import Comment 5 | 6 | 7 | class Photo(DoubanAPIBase): 8 | 9 | target = 'photo' 10 | 11 | def __repr__(self): 12 | return '' 13 | 14 | def get(self, id): 15 | return self._get('/v2/photo/%s' % id) 16 | 17 | def new(self, album_id, image, desc=''): 18 | return self._post('/v2/album/%s' % album_id, 19 | desc=desc, files={'image': image}) 20 | 21 | def update(self, id, desc): 22 | return self._put('/v2/photo/%s' % id, desc=desc) 23 | 24 | def delete(self, id): 25 | return self._delete('/v2/photo/%s' % id) 26 | 27 | def like(self, id): 28 | return self._post('/v2/photo/%s/like' % id) 29 | 30 | def unlike(self, id): 31 | return self._delete('/v2/photo/%s/like' % id) 32 | 33 | def comments(self, id, start=DEFAULT_START, count=DEFAULT_COUNT): 34 | return Comment(self.access_token, self.target).list(id, start=start, count=count) 35 | 36 | @property 37 | def comment(self): 38 | return Comment(self.access_token, self.target) 39 | -------------------------------------------------------------------------------- /douban_client/api/review.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .base import DoubanAPIBase 4 | 5 | 6 | class Review(DoubanAPIBase): 7 | 8 | def __init__(self, access_token, target): 9 | self.access_token = access_token 10 | self.target = target 11 | 12 | def new(self, target_id, title, content, rating=''): 13 | data = {self.target: target_id, 14 | 'title': title, 15 | 'content': content, 16 | 'rating': rating, } 17 | return self._post('/v2/%s/reviews' % self.target, **data) 18 | 19 | def update(self, id, title, content, rating=''): 20 | data = {self.target: id, 21 | 'title': title, 22 | 'content': content, 23 | 'rating': rating, } 24 | return self._put('/v2/%s/review/%s' % (self.target, id), **data) 25 | 26 | def delete(self, id): 27 | return self._delete('/v2/%s/review/%s' % (self.target, id)) 28 | -------------------------------------------------------------------------------- /douban_client/api/subject.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .base import DoubanAPIBase, DEFAULT_START, DEFAULT_COUNT 4 | from .review import Review 5 | 6 | 7 | class Subject(DoubanAPIBase): 8 | 9 | target = None 10 | 11 | def get(self, id): 12 | return self._get('/v2/%s/%s' % (self.target, id)) 13 | 14 | def search(self, q='', tag='', start=DEFAULT_START, count=DEFAULT_COUNT): 15 | return self._get('/v2/%s/search' % self.target, 16 | q=q, tag=tag, start=start, count=count) 17 | 18 | def tags(self, id): 19 | return self._get('/v2/%s/%s/tags' % (self.target, id)) 20 | 21 | def tagged_list(self, id): 22 | return self._get('/v2/%s/user_tags/%s' % (self.target, id)) 23 | 24 | @property 25 | def review(self): 26 | return Review(self.access_token, self.target) 27 | -------------------------------------------------------------------------------- /douban_client/api/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .base import DoubanAPIBase, DEFAULT_START, DEFAULT_COUNT 4 | 5 | 6 | class User(DoubanAPIBase): 7 | 8 | def __repr__(self): 9 | return '' 10 | 11 | def get(self, id): 12 | return self._get('/v2/user/%s' % id) 13 | 14 | @property 15 | def me(self): 16 | return self.get('~me') 17 | 18 | def search(self, q, start=DEFAULT_START, count=DEFAULT_COUNT): 19 | return self._get('/v2/user', q=q, start=start, count=count) 20 | 21 | def follow(self, id): 22 | return self._post('/shuo/v2/friendships/create', user_id=id) 23 | 24 | def unfollow(self, id): 25 | return self._post('/shuo/v2/friendships/destroy', user_id=id) 26 | 27 | def following(self, id, start=DEFAULT_START, count=DEFAULT_COUNT): 28 | page = start / count 29 | return self._get('/shuo/v2/users/%s/following' % id, page=page, count=count) 30 | 31 | def followers(self, id, start=DEFAULT_START, count=DEFAULT_COUNT): 32 | page = start / count 33 | return self._get('/shuo/v2/users/%s/followers' % id, page=page, count=count) 34 | -------------------------------------------------------------------------------- /douban_client/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pyoauth2 import Client, AccessToken 4 | from .api import DoubanAPI 5 | 6 | 7 | class DoubanClient(DoubanAPI): 8 | 9 | API_HOST = 'https://api.douban.com' 10 | AUTH_HOST = 'https://www.douban.com' 11 | TOKEN_URL = AUTH_HOST + '/service/auth2/token' 12 | AUTHORIZE_URL = AUTH_HOST + '/service/auth2/auth' 13 | 14 | def __init__(self, key, secret, redirect='', scope=''): 15 | self.redirect_uri = redirect 16 | self.scope = scope 17 | self.client = Client(key, secret, 18 | site=self.API_HOST, 19 | authorize_url=self.AUTHORIZE_URL, 20 | token_url=self.TOKEN_URL) 21 | self.access_token = AccessToken(self.client, '') 22 | 23 | def __repr__(self): 24 | return '' 25 | 26 | @property 27 | def authorize_url(self): 28 | return self.client.auth_code.authorize_url(redirect_uri=self.redirect_uri, scope=self.scope) 29 | 30 | def auth_with_code(self, code): 31 | self.access_token = self.client.auth_code.get_token(code, redirect_uri=self.redirect_uri) 32 | 33 | def auth_with_token(self, token): 34 | self.access_token = AccessToken(self.client, token) 35 | 36 | def auth_with_password(self, username, password, **opt): 37 | self.access_token = self.client.password.get_token(username=username, password=password, 38 | redirect_uri=self.redirect_uri, **opt) 39 | 40 | @property 41 | def token_code(self): 42 | return self.access_token and self.access_token.token 43 | 44 | @property 45 | def refresh_token_code(self): 46 | return getattr(self.access_token, 'refresh_token', None) 47 | 48 | def refresh_token(self, refresh_token): 49 | access_token = AccessToken(self.client, token='', refresh_token=refresh_token) 50 | self.access_token = access_token.refresh() 51 | -------------------------------------------------------------------------------- /examples/auth_with_code.py: -------------------------------------------------------------------------------- 1 | from six.moves import input 2 | from douban_client import DoubanClient 3 | 4 | KEY = '' 5 | SECRET = '' 6 | CALLBACK = '' 7 | 8 | SCOPE = 'douban_basic_common,community_basic_user' 9 | client = DoubanClient(KEY, SECRET, CALLBACK, SCOPE) 10 | 11 | print client.authorize_url 12 | code = input('Enter the verification code:') 13 | 14 | client.auth_with_code(code) 15 | print client.user.me 16 | -------------------------------------------------------------------------------- /examples/auth_with_password.py: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | 3 | """ 4 | auth with password 5 | 6 | 注意:auth_with_password 需要先申请 xAuth 权限 7 | 8 | 关于 xAuth 权限申请可咨询: api-master[at]douban.com 9 | 或者到 http://www.douban.com/group/dbapi/ 寻求帮助 10 | 11 | """ 12 | 13 | from douban_client import DoubanClient 14 | 15 | KEY = '' 16 | SECRET = '' 17 | CALLBACK = '' 18 | SCOPE = 'douban_basic_common,community_basic_user' 19 | 20 | client = DoubanClient(KEY, SECRET, CALLBACK, SCOPE) 21 | client.auth_with_password('user_email', 'user_password') 22 | 23 | print client.user.me 24 | -------------------------------------------------------------------------------- /examples/auth_with_token.py: -------------------------------------------------------------------------------- 1 | from douban_client import DoubanClient 2 | 3 | KEY = '' 4 | SECRET = '' 5 | CALLBACK = '' 6 | TOKEN = 'your token' 7 | 8 | SCOPE = 'douban_basic_common,community_basic_user' 9 | client = DoubanClient(KEY, SECRET, CALLBACK, SCOPE) 10 | 11 | client.auth_with_token(TOKEN) 12 | print client.user.me 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | py-oauth2>=0.0.8 2 | six>=1.4.1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup, find_packages 5 | 6 | setup(name='douban-client', 7 | version='0.0.6', 8 | keywords=('Douban', 'OAuth2', 'Douban API'), 9 | description='Python client library for Douban APIs (OAuth 2.0)', 10 | long_description='See http://liluo.github.com/douban-client', 11 | license='MIT License', 12 | 13 | url='http://liluo.github.com/douban-client', 14 | author='liluo', 15 | author_email='i@liluo.org', 16 | 17 | packages=find_packages(), 18 | include_package_data=True, 19 | platforms='any', 20 | install_requires=['py-oauth2>=0.0.8', 'six>=1.4.1'], 21 | classifiers=[ 22 | 'Environment :: Web Environment', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python', 27 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 28 | 'Topic :: Software Development :: Libraries :: Python Modules' 29 | ],) 30 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/douban/douban-client/10994824be2a407e0bf19a37c5b229f0911a4320/tests/__init__.py -------------------------------------------------------------------------------- /tests/douban.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/douban/douban-client/10994824be2a407e0bf19a37c5b229f0911a4320/tests/douban.png -------------------------------------------------------------------------------- /tests/framework.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | 6 | from six import print_ 7 | from six.moves import input, reduce 8 | 9 | 10 | TEST_DIR = os.path.dirname(os.path.realpath(__file__)) 11 | ROOT_DIR = os.path.dirname(TEST_DIR) 12 | sys.path.insert(0, ROOT_DIR) 13 | 14 | from unittest import main, TestCase 15 | from douban_client import DoubanClient 16 | from douban_client.api.error import DoubanAPIError 17 | 18 | try: 19 | from local_config import KEY, SECRET, CALLBACK, SCOPE, TOKEN 20 | except ImportError: 21 | KEY = '' 22 | SECRET = '' 23 | CALLBACK = '' 24 | 25 | SCOPE_MAP = { 'basic': ['douban_basic_common', 'community_basic_user'], } 26 | SCOPE = ','.join(reduce(lambda x, y: x + y, SCOPE_MAP.values())) 27 | TOKEN = '' 28 | 29 | def get_client(): 30 | client = DoubanClient(KEY, SECRET, CALLBACK, SCOPE) 31 | 32 | token = TOKEN 33 | 34 | if token: 35 | client.auth_with_token(token) 36 | else: 37 | print_('Go to the following link in your browser:') 38 | print_(client.authorize_url) 39 | 40 | code = input('Enter the verification code and hit ENTER when you\'re done:') 41 | client.auth_with_code(code) 42 | print_('token code:', client.token_code) 43 | print_('refresh token code:', client.refresh_token_code) 44 | return client 45 | 46 | client = get_client() 47 | 48 | class DoubanClientTestBase(TestCase): 49 | def setUp(self): 50 | pass 51 | 52 | @property 53 | def client(self): 54 | return client 55 | -------------------------------------------------------------------------------- /tests/run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | 6 | TEST_DIR = os.path.dirname(os.path.realpath(__file__)) 7 | ROOT_DIR = os.path.dirname(TEST_DIR) 8 | sys.path.insert(0, ROOT_DIR) 9 | 10 | from unittest import main, TestSuite, findTestCases 11 | 12 | def get_test_module_names(): 13 | file_names = os.listdir(os.curdir) 14 | for fn in file_names: 15 | if fn.startswith('test') and fn.endswith('.py'): 16 | yield 'tests.' + fn[:-3] 17 | 18 | def suite(): 19 | alltests = TestSuite() 20 | 21 | for module_name in get_test_module_names(): 22 | module = __import__(module_name, fromlist=[module_name]) 23 | alltests.addTest(findTestCases(module)) 24 | 25 | return alltests 26 | 27 | 28 | if __name__ == '__main__': 29 | main(defaultTest='suite') 30 | -------------------------------------------------------------------------------- /tests/test_api_album.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from uuid import uuid4 4 | from framework import DoubanClientTestBase, main 5 | 6 | class TestApiAlbum(DoubanClientTestBase): 7 | def setUp(self): 8 | super(TestApiAlbum, self).setUp() 9 | self.user_id = '40774605' 10 | self.album_id = '50201880' 11 | 12 | def test_get_album(self): 13 | ret = self.client.album.get(self.album_id) 14 | 15 | self.assertEqual(self.album_id, ret['id']) 16 | self.assertTrue('liked' in ret) 17 | 18 | def test_new_album(self): 19 | ret = self.client.album.new('test', desc='ddddddddddddd') 20 | 21 | self.assertTrue('id' in ret) 22 | self.assertTrue('privacy' in ret) 23 | self.assertTrue('size' in ret) 24 | self.assertTrue('author' in ret) 25 | 26 | def test_update_album(self): 27 | new_title = uuid4().hex 28 | self.client.album.update(self.album_id, new_title, 'new_desc') 29 | ret = self.client.album.get(self.album_id) 30 | self.assertEqual(new_title, ret['title']) 31 | 32 | def test_delete_album(self): 33 | aid = self.client.album.new('test', desc='abcdefg')['id'] 34 | ret = self.client.album.delete(aid) 35 | 36 | self.assertEqual({}, ret) 37 | 38 | def test_album_list_by_user(self): 39 | ret = self.client.album.list(self.user_id) 40 | 41 | self.assertTrue(isinstance(ret, dict)) 42 | self.assertTrue('albums' in ret) 43 | self.assertTrue(isinstance(ret['albums'], list)) 44 | 45 | def test_liked_album(self): 46 | ret = self.client.album.liked_list(self.user_id) 47 | 48 | self.assertTrue(isinstance(ret, dict)) 49 | self.assertTrue('albums' in ret) 50 | self.assertTrue(isinstance(ret['albums'], list)) 51 | 52 | def test_get_photos(self): 53 | ret = self.client.album.photos(self.album_id) 54 | 55 | self.assertTrue('start' in ret) 56 | self.assertTrue('count' in ret) 57 | self.assertTrue('photos' in ret) 58 | self.assertTrue(isinstance(ret['photos'], list)) 59 | 60 | def test_like_album(self): 61 | ret = self.client.album.like(self.album_id) 62 | 63 | self.assertEqual({}, ret) 64 | 65 | def test_unlike_album(self): 66 | ret = self.client.album.unlike(self.album_id) 67 | 68 | self.assertEqual({}, ret) 69 | 70 | 71 | if __name__ == '__main__': 72 | main() 73 | -------------------------------------------------------------------------------- /tests/test_api_book.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from uuid import uuid4 4 | from framework import DoubanClientTestBase, main 5 | 6 | class TestApiBook(DoubanClientTestBase): 7 | def setUp(self): 8 | super(TestApiBook, self).setUp() 9 | self.user_id = '40774605' 10 | self.book_id = '1126080' 11 | self.review_id = '1084441' 12 | self.isbn = '9787540457297' 13 | 14 | def test_get_book(self): 15 | ret = self.client.book.get(self.book_id) 16 | 17 | self.assertTrue(isinstance(ret, dict)) 18 | self.assertTrue('author' in ret) 19 | self.assertTrue('title' in ret) 20 | self.assertTrue('summary' in ret) 21 | 22 | def test_get_book_by_isbn(self): 23 | ret= self.client.book.isbn(self.isbn) 24 | 25 | self.assertTrue(isinstance(ret, dict)) 26 | self.assertTrue('author' in ret) 27 | self.assertTrue('title' in ret) 28 | self.assertTrue('summary' in ret) 29 | 30 | def test_search_book(self): 31 | ret = self.client.book.search('坦白') 32 | 33 | self.assertTrue(isinstance(ret, dict)) 34 | self.assertTrue(isinstance(ret['books'], list)) 35 | self.assertTrue('start' in ret) 36 | self.assertTrue('count' in ret) 37 | self.assertTrue('total' in ret) 38 | 39 | # def test_book_reviews(self): 40 | # ret = self.client.book.reviews(self.book_id) 41 | 42 | # self.assertTrue(isinstance(ret, dict)) 43 | # self.assertTrue(isinstance(ret['reviews'], list)) 44 | # self.assertTrue('start' in ret) 45 | # self.assertTrue('count' in ret) 46 | # self.assertTrue('total' in ret) 47 | 48 | def test_book_tags(self): 49 | ret = self.client.book.tags(self.book_id) 50 | 51 | self.assertTrue(isinstance(ret, dict)) 52 | self.assertTrue(isinstance(ret['tags'], list)) 53 | self.assertTrue('start' in ret) 54 | self.assertTrue('count' in ret) 55 | self.assertTrue('total' in ret) 56 | 57 | def test_get_book_tagged_list(self): 58 | ret = self.client.book.tagged_list('40774605') 59 | 60 | self.assertTrue(isinstance(ret, dict)) 61 | self.assertTrue(isinstance(ret['tags'], list)) 62 | self.assertTrue('start' in ret) 63 | self.assertTrue('count' in ret) 64 | self.assertTrue('total' in ret) 65 | 66 | def test_new_update_delete_review(self): 67 | 68 | # new 69 | title = content = uuid4().hex 70 | content = content * 10 71 | ret = self.client.book.review.new(self.book_id, title, content) 72 | 73 | self.assertTrue(isinstance(ret, dict)) 74 | self.assertEqual(content, ret['content']) 75 | self.assertTrue('author' in ret) 76 | 77 | review_id = ret['id'] 78 | 79 | # update 80 | content = content * 2 81 | ret = self.client.book.review.update(review_id, title, content) 82 | self.assertEqual(content, ret['content']) 83 | 84 | # delete 85 | ret = self.client.book.review.delete(review_id) 86 | self.assertEqual('OK', ret) 87 | 88 | 89 | # def test_get_book_review(self): 90 | # ret = self.client.book.review.get(self.review_id) 91 | 92 | # self.assertTrue(isinstance(ret, dict)) 93 | # self.assertEqual(ret['id'], self.review_id) 94 | # self.assertTrue('rating' in ret) 95 | # self.assertTrue('author' in ret) 96 | 97 | 98 | if __name__ == '__main__': 99 | main() 100 | -------------------------------------------------------------------------------- /tests/test_api_discussion.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from uuid import uuid4 4 | from framework import DoubanClientTestBase, main 5 | 6 | class TestApiDiscussion(DoubanClientTestBase): 7 | def setUp(self): 8 | super(TestApiDiscussion, self).setUp() 9 | self.user_id = '40774605' 10 | self.discussion_id = '48752833' 11 | self.target = 'online' 12 | self.target_id = '10903196' 13 | self.comment_id = '12939812' 14 | 15 | tmp = uuid4().hex 16 | 17 | self.title = tmp 18 | self.content = tmp 19 | self.comment_content = uuid4().hex 20 | self.comment_update_content = uuid4().hex 21 | 22 | def _add_discussion(self): 23 | return self.client.discussion.new(self.target, self.target_id, self.title, self.content) 24 | 25 | 26 | def test_get_discussion(self): 27 | ret = self.client.discussion.get(self.discussion_id) 28 | 29 | self.assertEqual(self.discussion_id, ret['id']) 30 | self.assertTrue('author' in ret) 31 | self.assertTrue('content' in ret) 32 | 33 | def test_update_discussion(self): 34 | content = title = uuid4().hex 35 | ret = self.client.discussion.update(self.discussion_id, title, content) 36 | 37 | self.assertTrue(title, ret['title']) 38 | self.assertTrue(content, ret['content']) 39 | 40 | def test_new_discussion(self): 41 | ret = self._add_discussion() 42 | 43 | self.assertTrue(self.title, ret['title']) 44 | self.assertTrue(self.content, ret['content']) 45 | self.assertTrue(self.target in ret['alt']) 46 | self.assertTrue(self.target_id in ret['alt']) 47 | 48 | def test_delete_discussion(self): 49 | dis = self._add_discussion() 50 | ret = self.client.discussion.delete(dis['id']) 51 | 52 | self.assertEqual({}, ret) 53 | 54 | def test_discussion_list(self): 55 | ret = self.client.discussion.list(self.target, self.target_id) 56 | 57 | self.assertTrue(isinstance(ret['discussions'], list)) 58 | 59 | def test_discussion_comments(self): 60 | ret = self.client.discussion.comments(self.discussion_id) 61 | 62 | self.assertTrue(isinstance(ret['comments'], list)) 63 | 64 | def test_get_discussion_comment(self): 65 | ret = self.client.discussion.comment.get(self.discussion_id, self.comment_id) 66 | 67 | self.assertEqual(self.comment_id, ret['id']) 68 | self.assertTrue('content' in ret) 69 | 70 | def test_new_delete_discussion_comment(self): 71 | # new 72 | ret = self.client.discussion.comment.new(self.discussion_id, self.comment_content) 73 | 74 | self.assertTrue('id' in ret) 75 | self.assertTrue('content' in ret) 76 | 77 | # delete 78 | comment_id = ret['id'] 79 | ret = self.client.discussion.comment.delete(self.discussion_id, comment_id) 80 | 81 | self.assertEqual({}, ret) 82 | 83 | 84 | if __name__ == '__main__': 85 | main() 86 | -------------------------------------------------------------------------------- /tests/test_api_doumail.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from uuid import uuid4 4 | from framework import DoubanClientTestBase, DoubanAPIError, main 5 | 6 | class TestApiDoumail(DoubanClientTestBase): 7 | def setUp(self): 8 | super(TestApiDoumail, self).setUp() 9 | self.user_id = '51789002' 10 | self.doumail_id = '263891152' 11 | self.doumail_ids = ['265897597', '265897596', '265897595'] 12 | 13 | def _new_doumail(self): 14 | title = content = uuid4().hex 15 | try: 16 | ret = self.client.doumail.new(title, content, self.user_id) 17 | except DoubanAPIError as e: 18 | ret = None 19 | return ret 20 | 21 | def test_get_doumail(self): 22 | ret = self.client.doumail.get(self.doumail_id) 23 | 24 | self.assertEqual(self.doumail_id, ret['id']) 25 | self.assertEqual(self.user_id, ret['receiver']['id']) 26 | 27 | def test_doumail_inbox(self): 28 | ret = self.client.doumail.inbox() 29 | 30 | self.assertTrue('start' in ret) 31 | self.assertTrue(isinstance(ret['mails'], list)) 32 | 33 | def test_doumail_outbox(self): 34 | ret = self.client.doumail.outbox() 35 | 36 | self.assertTrue('start' in ret) 37 | self.assertTrue(isinstance(ret['mails'], list)) 38 | 39 | def test_doumail_unread(self): 40 | ret = self.client.doumail.unread() 41 | 42 | self.assertTrue('start' in ret) 43 | self.assertTrue(isinstance(ret['mails'], list)) 44 | 45 | def test_new_doumail(self): 46 | ret = self._new_doumail() 47 | 48 | self.assertEqual({}, ret) 49 | 50 | def test_read_doumail(self): 51 | ret = self.client.doumail.read(self.doumail_id) 52 | 53 | self.assertEqual('R', ret['status']) 54 | 55 | def test_reads_doumail(self): 56 | ret = self.client.doumail.reads(self.doumail_ids) 57 | 58 | self.assertTrue(isinstance(ret, dict)) 59 | self.assertTrue(isinstance(ret['mails'], list)) 60 | 61 | def test_delete_doumail(self): 62 | doumail = self.client.doumail.inbox() 63 | doumail_id = doumail['mails'][0]['id'] 64 | ret = self.client.doumail.delete(doumail_id) 65 | 66 | self.assertEqual({}, ret) 67 | 68 | def test_deletes_doumail(self): 69 | doumail = self.client.doumail.inbox() 70 | doumail_ids = [m['id'] for m in doumail['mails']][:2] 71 | ret = self.client.doumail.deletes(ids=doumail_ids) 72 | 73 | self.assertEqual({}, ret) 74 | 75 | 76 | if __name__ == '__main__': 77 | main() 78 | -------------------------------------------------------------------------------- /tests/test_api_error.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from framework import DoubanClientTestBase, DoubanAPIError, main 4 | 5 | class TestApiError(DoubanClientTestBase): 6 | pass 7 | 8 | 9 | if __name__ == '__main__': 10 | main() 11 | -------------------------------------------------------------------------------- /tests/test_api_event.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from uuid import uuid4 4 | from datetime import datetime 5 | from framework import DoubanClientTestBase, main 6 | 7 | class TestApiEvent(DoubanClientTestBase): 8 | 9 | def setUp(self): 10 | self.event_id = '17087697' 11 | self.user_id = '40774605' 12 | self.loc = '108288' 13 | self.participate_date = datetime.now().strftime('%Y-%m-%d') 14 | 15 | def test_get_event(self): 16 | ret = self.client.event.get(self.event_id) 17 | 18 | self.assertTrue(isinstance(ret, dict)) 19 | self.assertEqual(self.event_id, ret['id']) 20 | self.assertTrue('loc_id' in ret) 21 | self.assertTrue('loc_name' in ret) 22 | 23 | def test_get_event_participants(self): 24 | ret = self.client.event.participants(self.event_id) 25 | 26 | self.assertTrue(isinstance(ret, dict)) 27 | self.assertTrue(isinstance(ret['users'], list)) 28 | self.assertTrue('total' in ret) 29 | 30 | def test_get_event_wishers(self): 31 | ret = self.client.event.wishers(self.event_id) 32 | 33 | self.assertTrue(isinstance(ret, dict)) 34 | self.assertTrue(isinstance(ret['users'], list)) 35 | self.assertTrue('total' in ret) 36 | 37 | def test_get_user_owned_events(self): 38 | ret = self.client.event.owned(self.user_id) 39 | 40 | self.assertTrue(isinstance(ret, dict)) 41 | self.assertTrue(isinstance(ret['events'], list)) 42 | 43 | def test_get_user_participated_events(self): 44 | ret = self.client.event.participated(self.user_id) 45 | 46 | self.assertTrue(isinstance(ret, dict)) 47 | self.assertTrue(isinstance(ret['events'], list)) 48 | 49 | def test_get_user_wished_events(self): 50 | ret = self.client.event.wished(self.user_id) 51 | 52 | self.assertTrue(isinstance(ret, dict)) 53 | self.assertTrue(isinstance(ret['events'], list)) 54 | 55 | def test_event_list(self): 56 | ret = self.client.event.list(self.loc) 57 | 58 | self.assertTrue(isinstance(ret, dict)) 59 | self.assertTrue(isinstance(ret['events'], list)) 60 | 61 | def test_search_event(self): 62 | ret = self.client.event.search('北京', self.loc) 63 | 64 | self.assertTrue(isinstance(ret, dict)) 65 | self.assertTrue(isinstance(ret['events'], list)) 66 | 67 | def test_join_event(self): 68 | ret = self.client.event.join(self.event_id) 69 | 70 | self.assertEqual({}, ret) 71 | 72 | def test_quit_event(self): 73 | ret = self.client.event.quit(self.event_id) 74 | 75 | self.assertEqual({}, ret) 76 | 77 | def test_wish_event(self): 78 | ret = self.client.event.wish(self.event_id) 79 | 80 | self.assertEqual({}, ret) 81 | 82 | def test_unwish_event(self): 83 | ret = self.client.event.unwish(self.event_id) 84 | 85 | self.assertEqual({}, ret) 86 | 87 | 88 | if __name__ == '__main__': 89 | main() 90 | -------------------------------------------------------------------------------- /tests/test_api_guess.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from framework import DoubanClientTestBase, main 4 | 5 | class TestApiGuess(DoubanClientTestBase): 6 | def setUp(self): 7 | super(TestApiGuess, self).setUp() 8 | self.user_id = '40774605' 9 | 10 | # def test_guess_notes(self): 11 | # ret = self.client.guess.notes(self.user_id) 12 | 13 | # self.assertTrue(ret.has_key('start')) 14 | # self.assertTrue(ret.has_key('count')) 15 | # self.assertTrue(ret.has_key('notes')) 16 | # self.assertTrue(isinstance(ret['notes'], list)) 17 | 18 | # def test_guess_albums(self): 19 | # ret = self.client.guess.albums(self.user_id) 20 | 21 | # self.assertTrue(ret.has_key('start')) 22 | # self.assertTrue(ret.has_key('count')) 23 | # self.assertTrue(ret.has_key('albums')) 24 | # self.assertTrue(isinstance(ret['albums'], list)) 25 | 26 | # def test_guess_onlines(self): 27 | # ret = self.client.guess.onlines(self.user_id) 28 | 29 | # self.assertTrue(ret.has_key('start')) 30 | # self.assertTrue(ret.has_key('count')) 31 | # self.assertTrue(ret.has_key('onlines')) 32 | # self.assertTrue(isinstance(ret['onlines'], list)) 33 | 34 | 35 | if __name__ == '__main__': 36 | main() 37 | -------------------------------------------------------------------------------- /tests/test_api_miniblog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from uuid import uuid4 4 | from framework import DoubanClientTestBase, DoubanAPIError, main 5 | 6 | 7 | class TestApiMiniblog(DoubanClientTestBase): 8 | 9 | def setUp(self): 10 | super(TestApiMiniblog, self).setUp() 11 | self.user_id = '40774605' 12 | self.miniblog_id = '999242853' 13 | self.comment = uuid4().hex 14 | self.comment_id = '140907103' 15 | self.rec_title = 'rec from douban-client' 16 | self.rec_url = 'https://github.com/douban/douban-client' 17 | self.rec_desc = 'Python client library for Douban APIs (OAuth 2.0) ' 18 | self.rec_image = 'http://img3.douban.com/view/photo/photo/public/p1850826843.jpg' 19 | 20 | def _gen_text(self): 21 | return 'test miniblog %s by douban-client'% uuid4().hex 22 | 23 | def _new_miniblog(self, upload=False): 24 | image = upload and open('douban.png', 'rb') 25 | ret = self.client.miniblog.new(self._gen_text(), image=image) 26 | if image: 27 | image.close() 28 | return ret 29 | 30 | def test_get_miniblog(self): 31 | ret = self.client.miniblog.get(self.miniblog_id) 32 | self.assertTrue(isinstance(ret, dict)) 33 | 34 | def test_home_timeline(self): 35 | ret = self.client.miniblog.home_timeline() 36 | self.assertTrue(isinstance(ret, list)) 37 | 38 | def test_user_timeline(self): 39 | ret = self.client.miniblog.user_timeline(self.user_id) 40 | self.assertTrue(isinstance(ret, list)) 41 | self.assertTrue(all([self.user_id == r['user']['id'] for r in ret])) 42 | 43 | def test_new_miniblog(self): 44 | ret = self._new_miniblog() 45 | self.assertTrue(isinstance(ret, dict)) 46 | self.assertTrue('id' in ret) 47 | 48 | def test_new_miniblog_with_image(self): 49 | ret = self._new_miniblog(upload=True) 50 | self.assertTrue('id' in ret) 51 | 52 | def test_delete_miniblog(self): 53 | mb = self._new_miniblog() 54 | mid = mb['id'] 55 | self.client.miniblog.delete(mid) 56 | func = self.client.miniblog.get 57 | self.assertRaises(DoubanAPIError, func, mid) 58 | 59 | def test_like_unlike_likers_miniblog(self): 60 | mb = self._new_miniblog() 61 | mid = mb['id'] 62 | ret = self.client.miniblog.like(mid) 63 | self.assertTrue(ret['liked']) 64 | 65 | ret = self.client.miniblog.unlike(mid) 66 | self.assertFalse(ret['liked']) 67 | ret = self.client.miniblog.likers(mid) 68 | self.assertTrue(isinstance(ret, list)) 69 | 70 | def test_reshare_unreshare_resharers_miniblog(self): 71 | mid = self.miniblog_id 72 | # reshare 73 | self.client.miniblog.reshare(mid) 74 | ret = self.client.miniblog.get(mid) 75 | reshared_count = ret['reshared_count'] 76 | self.assertTrue(reshared_count > 0) 77 | 78 | # unreshare 79 | # 这个豆瓣广播还没有实现接口 80 | # self.client.miniblog.unreshare(mid) 81 | # ret = self.client.miniblog.get(mid) 82 | # 83 | #self.assertEqual(reshared_count-1, ret['reshared_count']) 84 | 85 | # reshareders 86 | ret = self.client.miniblog.reshareders(mid) 87 | self.assertTrue(isinstance(ret, list)) 88 | 89 | def test_get_miniblog_comments(self): 90 | ret = self.client.miniblog.comments(self.miniblog_id) 91 | self.assertTrue(isinstance(ret, list)) 92 | self.assertTrue(all(['user' in r for r in ret])) 93 | 94 | def test_new_delete_miniblog_comment(self): 95 | # new 96 | ret = self.client.miniblog.comment.new(self.miniblog_id, self.comment) 97 | self.assertEqual(self.comment, ret['text']) 98 | # delete 99 | comment_id = ret['id'] 100 | ret = self.client.miniblog.comment.delete(comment_id) 101 | self.assertEqual(self.comment, ret['text']) 102 | 103 | def test_get_miniblog_comment(self): 104 | ret = self.client.miniblog.comment.get(self.comment_id) 105 | self.assertEqual('456', ret['text']) 106 | 107 | def test_miniblog_rec(self): 108 | ret = self.client.miniblog.rec(title=self.rec_title, url=self.rec_url, 109 | desc=self.rec_desc, image=self.rec_image) 110 | self.assertTrue('title' in ret) 111 | self.assertEqual(len(ret['attachments']), 1) 112 | 113 | 114 | if __name__ == '__main__': 115 | main() 116 | -------------------------------------------------------------------------------- /tests/test_api_movie.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from uuid import uuid4 4 | from framework import DoubanClientTestBase, main 5 | 6 | class TestApiMovie(DoubanClientTestBase): 7 | def setUp(self): 8 | super(TestApiMovie, self).setUp() 9 | self.user_id = '40774605' 10 | self.movie_id = '1296357' 11 | self.review_id = '5565362' 12 | self.imdb = 'tt1345836' 13 | self.celebrity_id = '1053585' 14 | 15 | def test_get_movie(self): 16 | ret = self.client.movie.get(self.movie_id) 17 | 18 | self.assertTrue(isinstance(ret, dict)) 19 | self.assertTrue('author' in ret) 20 | self.assertTrue('title' in ret) 21 | self.assertTrue('summary' in ret) 22 | 23 | def test_get_celebrity(self): 24 | ret = self.client.movie.celebrity(self.celebrity_id) 25 | 26 | self.assertTrue(isinstance(ret, dict)) 27 | self.assertTrue('name' in ret) 28 | self.assertTrue('avatars' in ret) 29 | self.assertTrue('works'in ret) 30 | 31 | def test_get_movie_by_imdb(self): 32 | ret= self.client.movie.imdb(self.imdb) 33 | 34 | self.assertTrue(isinstance(ret, dict)) 35 | self.assertTrue('author' in ret) 36 | self.assertTrue('title' in ret) 37 | self.assertTrue('summary' in ret) 38 | 39 | def test_search_movie(self): 40 | ret = self.client.movie.search('蝙蝠侠') 41 | 42 | self.assertTrue(isinstance(ret, dict)) 43 | self.assertTrue(isinstance(ret['subjects'], list)) 44 | self.assertTrue('start' in ret) 45 | self.assertTrue('count' in ret) 46 | self.assertTrue('total' in ret) 47 | 48 | # def test_movie_reviews(self): 49 | # ret = self.client.movie.reviews(self.movie_id) 50 | 51 | # self.assertTrue(isinstance(ret, dict)) 52 | # self.assertTrue(isinstance(ret['reviews'], list)) 53 | # self.assertTrue('start' in ret) 54 | # self.assertTrue('count' in ret) 55 | # self.assertTrue('total' in ret) 56 | 57 | def test_movie_tags(self): 58 | ret = self.client.movie.tags(self.movie_id) 59 | 60 | self.assertTrue(isinstance(ret, dict)) 61 | self.assertTrue(isinstance(ret['tags'], list)) 62 | self.assertTrue('start' in ret) 63 | self.assertTrue('count' in ret) 64 | self.assertTrue('total' in ret) 65 | 66 | def test_get_movie_tagged_list(self): 67 | ret = self.client.movie.tagged_list('40774605') 68 | 69 | self.assertTrue(isinstance(ret, dict)) 70 | self.assertTrue(isinstance(ret['tags'], list)) 71 | self.assertTrue('start' in ret) 72 | self.assertTrue('count' in ret) 73 | self.assertTrue('total' in ret) 74 | 75 | def test_new_update_delete_review(self): 76 | 77 | # new 78 | title = content = uuid4().hex 79 | content = content * 10 80 | ret = self.client.movie.review.new(self.movie_id, title, content) 81 | 82 | self.assertTrue(isinstance(ret, dict)) 83 | self.assertEqual(content, ret['content']) 84 | self.assertTrue('author' in ret) 85 | 86 | review_id = ret['id'] 87 | 88 | # update 89 | content = content * 2 90 | ret = self.client.movie.review.update(review_id, title, content) 91 | self.assertEqual(content, ret['content']) 92 | 93 | # delete 94 | ret = self.client.movie.review.delete(review_id) 95 | self.assertEqual('OK', ret) 96 | 97 | 98 | # def test_get_movie_review(self): 99 | # ret = self.client.movie.review.get(self.review_id) 100 | 101 | # self.assertTrue(isinstance(ret, dict)) 102 | # self.assertEqual(ret['id'], self.review_id) 103 | # self.assertTrue('rating' in ret) 104 | # self.assertTrue('author' in ret) 105 | 106 | 107 | if __name__ == '__main__': 108 | main() 109 | -------------------------------------------------------------------------------- /tests/test_api_music.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from uuid import uuid4 4 | from framework import DoubanClientTestBase, main 5 | 6 | class TestApiMusic(DoubanClientTestBase): 7 | def setUp(self): 8 | super(TestApiMusic, self).setUp() 9 | self.user_id = '40774605' 10 | self.music_id = '1419262' 11 | self.review_id = '5572975' 12 | 13 | def test_get_music(self): 14 | ret = self.client.music.get(self.music_id) 15 | 16 | self.assertTrue(isinstance(ret, dict)) 17 | self.assertTrue('author' in ret) 18 | self.assertTrue('title' in ret) 19 | self.assertTrue('summary' in ret) 20 | 21 | def test_search_music(self): 22 | ret = self.client.music.search('坦白') 23 | 24 | self.assertTrue(isinstance(ret, dict)) 25 | self.assertTrue(isinstance(ret['musics'], list)) 26 | self.assertTrue('start' in ret) 27 | self.assertTrue('count' in ret) 28 | self.assertTrue('total' in ret) 29 | 30 | # def test_music_reviews(self): 31 | # ret = self.client.music.reviews(self.music_id) 32 | 33 | # self.assertTrue(isinstance(ret, dict)) 34 | # self.assertTrue(isinstance(ret['reviews'], list)) 35 | # self.assertTrue('start' in ret) 36 | # self.assertTrue('count' in ret) 37 | # self.assertTrue('total' in ret) 38 | 39 | def test_music_tags(self): 40 | ret = self.client.music.tags(self.music_id) 41 | 42 | self.assertTrue(isinstance(ret, dict)) 43 | self.assertTrue(isinstance(ret['tags'], list)) 44 | self.assertTrue('start' in ret) 45 | self.assertTrue('count' in ret) 46 | self.assertTrue('total' in ret) 47 | 48 | def test_get_music_tagged_list(self): 49 | ret = self.client.music.tagged_list('40774605') 50 | 51 | self.assertTrue(isinstance(ret, dict)) 52 | self.assertTrue(isinstance(ret['tags'], list)) 53 | self.assertTrue('start' in ret) 54 | self.assertTrue('count' in ret) 55 | self.assertTrue('total' in ret) 56 | 57 | def test_new_update_delete_review(self): 58 | 59 | # new 60 | title = content = uuid4().hex 61 | content = content * 10 62 | ret = self.client.music.review.new(self.music_id, title, content) 63 | 64 | self.assertTrue(isinstance(ret, dict)) 65 | self.assertEqual(content, ret['content']) 66 | self.assertTrue('author' in ret) 67 | 68 | review_id = ret['id'] 69 | 70 | # update 71 | content = content * 2 72 | ret = self.client.music.review.update(review_id, title, content) 73 | self.assertEqual(content, ret['content']) 74 | 75 | # delete 76 | ret = self.client.music.review.delete(review_id) 77 | self.assertEqual('OK', ret) 78 | 79 | 80 | # def test_get_music_review(self): 81 | # ret = self.client.music.review.get(self.review_id) 82 | 83 | # self.assertTrue(isinstance(ret, dict)) 84 | # self.assertEqual(ret['id'], self.review_id) 85 | # self.assertTrue('rating' in ret) 86 | # self.assertTrue('author' in ret) 87 | 88 | 89 | if __name__ == '__main__': 90 | main() 91 | -------------------------------------------------------------------------------- /tests/test_api_note.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from uuid import uuid4 4 | from framework import DoubanClientTestBase, main 5 | 6 | 7 | class TestApiNote(DoubanClientTestBase): 8 | def setUp(self): 9 | super(TestApiNote, self).setUp() 10 | self.user_id = '64129916' 11 | self.note_id = '321263424' 12 | self.comment_id = '36366425' 13 | self.comment_content = uuid4().hex 14 | self.title = 'test note title' 15 | self.content = 'test note content' 16 | self.update_content = 'test note was updated' 17 | 18 | def _new_note(self): 19 | return self.client.note.new(self.title, self.content) 20 | 21 | def test_get_note_list(self): 22 | ret = self.client.note.list(self.user_id) 23 | 24 | self.assertTrue('start' in ret) 25 | self.assertTrue('count' in ret) 26 | self.assertTrue('notes' in ret) 27 | self.assertTrue(isinstance(ret['notes'], list)) 28 | 29 | def test_get_note(self): 30 | ret = self.client.note.get(self.note_id) 31 | 32 | self.assertEqual(ret['id'], self.note_id) 33 | self.assertTrue('title' in ret) 34 | self.assertTrue('summary' in ret) 35 | self.assertTrue('content' in ret) 36 | 37 | def test_new_note(self): 38 | ret = self._new_note() 39 | self.assertEqual(ret['title'], self.title) 40 | self.assertTrue('content' in ret) 41 | 42 | def test_update_note(self): 43 | ret = self._new_note() 44 | self.assertTrue('id' in ret) 45 | note_id = ret.get('id') 46 | self.assertTrue(note_id) 47 | ret = self.client.note.update(note_id, self.title, self.update_content) 48 | 49 | # TODO 50 | # 这个地方很奇怪,更新成功,但是应该返回结果类型是 unicode,说好的 JSON 呢 51 | # self.assertEqual(ret['title'], self.title) 52 | # self.assertEqual(ret['content'], self.update_content) 53 | 54 | self.assertTrue(self.update_content in ret) 55 | 56 | def test_upload_note_photo(self): 57 | note = self._new_note() 58 | self.assertTrue('id' in note) 59 | note_id = note.get('id') 60 | self.assertTrue(note_id) 61 | 62 | pid = 1 63 | content = self.update_content 64 | layout = 'L' 65 | desc = 'desc for image%s' % pid 66 | with open('douban.png', 'rb') as image: 67 | ret = self.client.note.upload_photo(note_id, pid, image, content, layout, desc) 68 | self.assertTrue('content' in ret) 69 | 70 | def test_delete_note(self): 71 | note = self._new_note() 72 | ret = self.client.note.delete(note['id']) 73 | self.assertEqual(ret, {}) 74 | 75 | def test_get_liked(self): 76 | ret = self.client.note.liked_list(self.user_id) 77 | self.assertTrue('start' in ret) 78 | self.assertTrue('count' in ret) 79 | self.assertTrue('notes' in ret) 80 | self.assertTrue(isinstance(ret['notes'], list)) 81 | 82 | def test_like(self): 83 | ret = self.client.note.like(self.note_id) 84 | self.assertEqual(ret, {}) 85 | 86 | def test_unlike(self): 87 | ret = self.client.note.unlike(self.note_id) 88 | self.assertEqual(ret, {}) 89 | 90 | def test_note_comments(self): 91 | ret = self.client.note.comments(self.note_id) 92 | self.assertTrue(isinstance(ret['comments'], list)) 93 | 94 | def test_get_note_comment(self): 95 | ret = self.client.note.comment.get(self.note_id, self.comment_id) 96 | self.assertEqual(self.comment_id, ret['id']) 97 | self.assertTrue('content' in ret) 98 | 99 | def test_new_delete_note_comment(self): 100 | # new 101 | ret = self.client.note.comment.new(self.note_id, self.comment_content) 102 | self.assertTrue('id' in ret) 103 | self.assertTrue('content' in ret) 104 | # delete 105 | comment_id = ret['id'] 106 | ret = self.client.note.comment.delete(self.note_id, comment_id) 107 | self.assertEqual({}, ret) 108 | 109 | 110 | if __name__ == '__main__': 111 | main() 112 | -------------------------------------------------------------------------------- /tests/test_api_online.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from uuid import uuid4 4 | from datetime import datetime, timedelta 5 | from framework import DoubanClientTestBase, main 6 | 7 | t2s = lambda t: t.strftime('%Y-%m-%d %H:%M') 8 | now = datetime.now() 9 | begin_time = t2s(now) 10 | end_time = t2s(now + timedelta(days=1)) 11 | 12 | class TestApiOnline(DoubanClientTestBase): 13 | def setUp(self): 14 | super(TestApiOnline, self).setUp() 15 | self.user_id = '40774605' 16 | self.online_id = '11182611' 17 | self._title = 'api douban-client test' 18 | self._desc = 'api test, desc abcdefg hijklmn opq rst uvw xyz, now you see, i can create online.' 19 | self.discussion_title = uuid4().hex 20 | self.discussion_content = uuid4().hex 21 | 22 | 23 | def _add_online(self): 24 | return self.client.online.new(self._title, self._desc, begin_time, end_time) 25 | 26 | 27 | def test_get_online(self): 28 | ret = self.client.online.get(self.online_id) 29 | 30 | self.assertEqual(self.online_id, ret['id']) 31 | self.assertEqual('http://www.douban.com/online/%s/'%self.online_id, ret['alt']) 32 | 33 | def test_new_online(self): 34 | ret = self._add_online() 35 | 36 | self.assertEqual(self._title, ret['title']) 37 | self.assertEqual(self._desc, ret['desc']) 38 | 39 | def test_update_online(self): 40 | online = self._add_online() 41 | new_title = self._title + 'new' 42 | new_desc = self._desc + 'new' 43 | ret = self.client.online.update(online['id'], new_title, new_desc, begin_time, end_time) 44 | 45 | self.assertEqual(new_title, ret['title']) 46 | self.assertEqual(new_desc, ret['desc']) 47 | 48 | def test_delete_online(self): 49 | online = self._add_online() 50 | ret = self.client.online.delete(online['id']) 51 | 52 | self.assertEqual({}, ret) 53 | 54 | def test_join_online(self): 55 | ret = self.client.online.join(self.online_id) 56 | 57 | self.assertEqual({}, ret) 58 | 59 | def test_quit_online(self): 60 | ret = self.client.online.quit(self.online_id) 61 | 62 | self.assertEqual({}, ret) 63 | 64 | def test_like_online(self): 65 | ret = self.client.online.like(self.online_id) 66 | 67 | self.assertEqual({}, ret) 68 | 69 | def test_unlike_online(self): 70 | ret = self.client.online.unlike(self.online_id) 71 | 72 | self.assertEqual({}, ret) 73 | 74 | def test_get_online_participants(self): 75 | ret = self.client.online.participants(self.online_id) 76 | 77 | self.assertTrue('total' in ret) 78 | self.assertTrue(isinstance(ret['users'], list)) 79 | 80 | def test_get_online_discussions(self): 81 | ret = self.client.online.discussions(self.online_id) 82 | 83 | self.assertTrue(isinstance(ret['discussions'], list)) 84 | 85 | def test_online_list(self): 86 | ret = self.client.online.list(cate='day') 87 | 88 | self.assertTrue('total' in ret) 89 | self.assertTrue(isinstance(ret['onlines'], list)) 90 | 91 | def test_new_online_discussion(self): 92 | online_id = 10903196 93 | ret = self.client.online.discussion.new(online_id, self.discussion_title, self.discussion_content) 94 | 95 | self.assertTrue(self.discussion_title, ret['title']) 96 | self.assertTrue(self.discussion_content, ret['content']) 97 | 98 | def test_created_onlines(self): 99 | ret = self.client.online.created(self.user_id) 100 | 101 | self.assertTrue('total' in ret) 102 | self.assertTrue(isinstance(ret['onlines'], list)) 103 | 104 | def test_joined_onlines(self): 105 | ret = self.client.online.joined(self.user_id) 106 | 107 | self.assertTrue('total' in ret) 108 | self.assertTrue(isinstance(ret['onlines'], list)) 109 | 110 | 111 | 112 | if __name__ == '__main__': 113 | main() 114 | -------------------------------------------------------------------------------- /tests/test_api_photo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from uuid import uuid4 4 | from framework import DoubanClientTestBase, main 5 | 6 | class TestApiPhoto(DoubanClientTestBase): 7 | def setUp(self): 8 | super(TestApiPhoto, self).setUp() 9 | self.user_id = '40774605' 10 | self.album_id = '50201880' 11 | self.photo_id = '1692008281' 12 | self.comment_id = '113934719' 13 | self.comment_content = uuid4().hex 14 | 15 | def _add_photo(self): 16 | with open('douban.png', 'rb') as image: 17 | return self.client.photo.new(self.album_id, image) 18 | 19 | def test_get_photo(self): 20 | ret = self.client.photo.get(self.photo_id) 21 | 22 | self.assertEqual(self.photo_id, ret['id']) 23 | 24 | def test_new_photo(self): 25 | ret = self._add_photo() 26 | 27 | self.assertEqual(self.album_id, ret['album_id']) 28 | self.assertTrue('id' in ret) 29 | self.assertTrue('desc' in ret) 30 | self.assertTrue('alt' in ret) 31 | 32 | def test_delete_photo(self): 33 | photo = self._add_photo() 34 | ret = self.client.photo.delete(photo['id']) 35 | 36 | self.assertEqual({}, ret) 37 | 38 | def test_update_photo(self): 39 | desc = 'hmm' 40 | ret = self.client.photo.update(self.photo_id, desc) 41 | self.assertTrue(desc.startswith(ret['desc'])) 42 | 43 | def test_like_photo(self): 44 | ret = self.client.photo.like(self.photo_id) 45 | self.assertEqual({}, ret) 46 | 47 | def test_unlike_photo(self): 48 | ret = self.client.photo.unlike(self.photo_id) 49 | self.assertEqual({}, ret) 50 | 51 | def test_photo_comments(self): 52 | ret = self.client.photo.comments(self.photo_id) 53 | 54 | self.assertTrue(isinstance(ret['comments'], list)) 55 | 56 | def test_get_photo_comment(self): 57 | ret = self.client.photo.comment.get(self.photo_id, self.comment_id) 58 | 59 | self.assertEqual(self.comment_id, ret['id']) 60 | self.assertTrue('content' in ret) 61 | 62 | def test_new_delete_photo_comment(self): 63 | # new 64 | ret = self.client.photo.comment.new(self.photo_id, self.comment_content) 65 | 66 | self.assertTrue('id' in ret) 67 | self.assertTrue('content' in ret) 68 | 69 | # delete 70 | comment_id = ret['id'] 71 | ret = self.client.photo.comment.delete(self.photo_id, comment_id) 72 | 73 | 74 | if __name__ == '__main__': 75 | main() 76 | -------------------------------------------------------------------------------- /tests/test_api_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from framework import DoubanClientTestBase, main 4 | 5 | class TestApiUser(DoubanClientTestBase): 6 | 7 | def setUp(self): 8 | super(TestApiUser, self).setUp() 9 | self.user_id = '70920446' 10 | 11 | def test_get_user(self): 12 | ret = self.client.user.get('liluoliluo') 13 | self.assertEqual(ret['uid'], 'liluoliluo') 14 | 15 | def test_get_me(self): 16 | ret = self.client.user.me 17 | self.assertTrue('id' in ret) 18 | 19 | def test_search(self): 20 | q = '落' 21 | ret = self.client.user.search(q) 22 | 23 | self.assertTrue('start' in ret) 24 | self.assertTrue('count' in ret) 25 | self.assertTrue('total' in ret) 26 | 27 | def test_follow(self): 28 | ret = self.client.user.follow(self.user_id) 29 | 30 | self.assertTrue(ret['following']) 31 | 32 | def test_unfollow(self): 33 | self.client.user.follow(self.user_id) 34 | ret = self.client.user.unfollow(self.user_id) 35 | 36 | self.assertFalse(ret['following']) 37 | 38 | self.assertTrue(isinstance(ret, dict)) 39 | self.assertTrue('uid' in ret) 40 | 41 | def test_followers(self): 42 | ret = self.client.user.followers(self.user_id) 43 | 44 | self.assertTrue(isinstance(ret, list)) 45 | self.assertTrue(all(['uid' in r for r in ret])) 46 | 47 | # def test_following_followers_of(self): 48 | # ret = self.client.user.following_followers_of('51789002') 49 | 50 | 51 | # def test_suggestions(self): 52 | # ret = self.client.user.suggestions(self.user_id) 53 | 54 | # self.assertTrue(isinstance(ret, list)) 55 | # self.assertTrue(all(['uid' in r for r in ret])) 56 | 57 | 58 | 59 | if __name__ == '__main__': 60 | main() 61 | --------------------------------------------------------------------------------