├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── TODO.md ├── dev-requirements.txt ├── examples └── vine_search.py ├── fixtures └── cassettes │ ├── follow_notifications.yml │ ├── get_post.yml │ ├── login-custom-device-token.yml │ ├── login.yml │ ├── signup.yml │ ├── tag_timeline.yml │ ├── unfollow_notifications.yml │ └── vm_friends_inbox.yml ├── setup.py ├── tests ├── __init__.py └── test_vinepy.py └── vinepy ├── __init__.py ├── api.py ├── endpoints.py ├── errors.py ├── models.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 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 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | # Logs 38 | *.log 39 | 40 | # Virtualenv 41 | .venv 42 | vinepy3 43 | 44 | # Backups 45 | *.bak 46 | 47 | # Coverage 48 | htmlcov 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - '2.6' 5 | - '2.7' 6 | # - '3.2' 7 | - '3.3' 8 | - '3.4' 9 | - pypy 10 | deploy: 11 | provider: pypi 12 | user: davoclavo 13 | password: 14 | secure: ib4iaqycgcF3ijSeUAvOkPWUJe3K0Y0Iha23GaUXfCkYykIEBBTc830co418dNXHA1dDBCpopfFxnAmplZlWzCakqXFQqZttaRMSQ4jgp6tsbUdLbAd4Zgitobr04oWs3xb3fGKCkCx8c+ZrfadSHmKLj63XKhtIeClfEt/LdsQ= 15 | install: 16 | - pip install . 17 | - pip install -r dev-requirements.txt 18 | - pip install coveralls 19 | script: 20 | - PYTHONPATH=. nose2 -v --with-cov 21 | after_success: coveralls 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 David Gomez Urquiza 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | vinepy 2 | ====== 3 | 4 | *Python wrapper for the [Vine](https://vine.co) API* 5 | 6 | [![](https://travis-ci.org/davoclavo/vinepy.svg?branch=master)](https://travis-ci.org/davoclavo/vinepy) 7 | [![](https://img.shields.io/coveralls/davoclavo/vinepy.svg)](https://coveralls.io/r/davoclavo/vinepy) 8 | [![](https://img.shields.io/pypi/v/vinepy.svg)](https://pypi.python.org/pypi/vinepy) 9 | [![](https://img.shields.io/badge/coolness-ultrasupercool-blue.svg)](http://i.imgur.com/oJ6ZZf8.gif) 10 | 11 | ## Installation 12 | 13 | From pip 14 | 15 | ``` 16 | pip install vinepy 17 | ``` 18 | 19 | From source 20 | 21 | ``` 22 | git clone https://github.com/davoclavo/vinepy.git 23 | cd vinepy 24 | pip install -r dev-requirements.txt 25 | python setup.py install 26 | ``` 27 | 28 | ## Requirements 29 | 30 | #### Usage 31 | 32 | * [requests](http://docs.python-requests.org/en/latest/) 33 | 34 | #### Development 35 | 36 | * [nose2 + coverage-plugin](https://github.com/nose-devs/nose2) 37 | * [vcrpy](https://github.com/kevin1024/vcrpy) 38 | 39 | 40 | ## Examples 41 | 42 | ```python 43 | import vinepy 44 | 45 | vine = vinepy.API(username='email@host.com', password='leinternetz') 46 | user = vine.user 47 | followers = user.followers() 48 | timeline = user.timeline() 49 | ``` 50 | 51 | ## Tests 52 | 53 | #### Quick run tests 54 | ```sh 55 | cd vinepy 56 | nose2 57 | ``` 58 | 59 | #### Coverage 60 | ```sh 61 | cd vinepy 62 | nose2 --with-coverage --coverage-report html 63 | open htmlcov/index.html 64 | ``` 65 | 66 | 67 | ## Acknowledgements 68 | 69 | * Inspired on [TweetPony](https://github.com/Mezgrman/TweetPony) 70 | * Based on the Vine API documentation by [neuegram](https://github.com/neuegram) and [starlock](https://github.com/starlock/vino/wiki/API-Reference) 71 | * Used [mitmproxy](http://mitmproxy.org/) to get the missing API endpoints 72 | * Thanks to [Vine](https://vine.co) for making such an amazing app. 73 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | TO DO 2 | ===== 3 | 4 | Refactors 5 | --------- 6 | 7 | - [x] Move methods from models to utils 8 | - [x] Python3 + PEP8 9 | - [ ] Docstrings 10 | - [ ] *args, **kwargs everywhere? 11 | - [ ] _attrs 12 | - [ ] VineErrors with better descriptions 13 | 14 | 15 | Endpoints 16 | --------- 17 | 18 | - [x] VMs 19 | - [ ] Loops 20 | - [x] Favorites 21 | - [ ] Delete account 22 | - [ ] Add a `public` attribute for public endpoints 23 | 24 | 25 | Tests 26 | ----- 27 | 28 | - [x] Nose2 29 | - [x] vcrpy 30 | - [x] Travis CI 31 | - [x] Automated PyPI deploy if build passes 32 | - [ ] Coveralls 33 | 34 | 35 | Examples 36 | -------- 37 | 38 | - [ ] Upload video 39 | - [ ] Send VM 40 | 41 | 42 | Ideas 43 | ----- 44 | 45 | - [ ] Parse Forsquare venue data and create an object that can interact with the Forsquare API 46 | - [ ] Generator for paginations 47 | - [ ] Nested endpoints 48 | - [ ] Request timed context, remember we are using a private api, we don't want them to get angry 49 | 50 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | nose2[coverage-plugin] 2 | vcrpy 3 | -------------------------------------------------------------------------------- /examples/vine_search.py: -------------------------------------------------------------------------------- 1 | import vinepy 2 | 3 | 4 | def main(): 5 | vine = vinepy.API(username='something@yourmail.com', password='password') 6 | # You can create a vine account 7 | # user = vine.signup(username='Your Name', email='something@yourmail.com', password='password') 8 | tag_timeline = vine.get_tag_timeline(tag_name='LNV') 9 | 10 | for post in tag_timeline: 11 | print(post.shareUrl) 12 | 13 | if __name__ == '__main__': 14 | main() 15 | -------------------------------------------------------------------------------- /fixtures/cassettes/follow_notifications.yml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: notify=1 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Accept-Language: [en;q=1] 8 | Connection: [keep-alive] 9 | Content-Length: ['8'] 10 | Content-Type: [application/x-www-form-urlencoded; charset=utf-8] 11 | Host: [api.vineapp.com] 12 | Proxy-Connection: [keep-alive] 13 | User-Agent: [iphone/172 (iPad; iOS 7.0.4; Scale/2.00)] 14 | X-Vine-Client: [ios/2.5.1] 15 | vine-session-id: [!!python/unicode 1169361810303164416-ee021a75-d3ca-4f39-aa14-863e41723743] 16 | method: POST 17 | uri: https://api.vineapp.com:443/users/948731399408640000/followers/notifications 18 | response: 19 | body: {string: !!python/unicode '{"code": "", "data": {}, "success": true, "error": 20 | ""}'} 21 | headers: 22 | cache-control: ['private, no-store, must-revalidate'] 23 | connection: [keep-alive] 24 | content-length: ['54'] 25 | content-type: [application/json] 26 | date: ['Tue, 10 Feb 2015 23:13:40 GMT'] 27 | expires: ['0'] 28 | pragma: [no-cache] 29 | strict-transport-security: [max-age=631138519] 30 | status: {code: 200, message: OK} 31 | - request: 32 | body: null 33 | headers: 34 | Accept: ['*/*'] 35 | Accept-Encoding: ['gzip, deflate'] 36 | Accept-Language: [en;q=1] 37 | Connection: [keep-alive] 38 | Content-Type: [application/x-www-form-urlencoded; charset=utf-8] 39 | Host: [api.vineapp.com] 40 | Proxy-Connection: [keep-alive] 41 | User-Agent: [iphone/172 (iPad; iOS 7.0.4; Scale/2.00)] 42 | X-Vine-Client: [ios/2.5.1] 43 | vine-session-id: [!!python/unicode 1169361810303164416-ee021a75-d3ca-4f39-aa14-863e41723743] 44 | method: GET 45 | uri: https://api.vineapp.com:443/users/profiles/948731399408640000 46 | response: 47 | body: {string: !!python/unicode '{"code": "", "data": {"followerCount": 195, "userId": 48 | 948731399408640000, "private": 0, "likeCount": 715, "postCount": 147, "explicitContent": 49 | 0, "blocked": 0, "verified": 0, "loopCount": 7671, "avatarUrl": "http://v.cdn.vine.co/r/avatars/98E427C03A1035848145776820224_1c1a5595066.4.7_AJeYR3l8u2pgTdFOq.rn9k3ziYHqfhul96J76ILSrFSVdIbDEbv6EeizF.XEklIf.jpg?versionId=11kNTERjSU_kj4CAcpQfGQoZrSqx3LSp", 50 | "authoredPostCount": 115, "description": "Soy lo m\u00e1s viejo que he sido 51 | y lo m\u00e1s joven que jam\u00e1s volver\u00e9 a ser", "location": "CDMX", 52 | "username": "Davoclavo", "vanityUrls": ["davoclavo"], "following": 1, "blocking": 53 | 0, "shareUrl": "https://vine.co/davoclavo", "profileBackground": "0x5082e5", 54 | "notifyPosts": 1, "followingCount": 288, "repostsEnabled": 1}, "success": 55 | true, "error": ""}'} 56 | headers: 57 | cache-control: ['private, no-store, must-revalidate'] 58 | connection: [keep-alive] 59 | content-length: ['802'] 60 | content-type: [application/json] 61 | date: ['Tue, 10 Feb 2015 23:13:41 GMT'] 62 | expires: ['0'] 63 | pragma: [no-cache] 64 | strict-transport-security: [max-age=631138519] 65 | status: {code: 200, message: OK} 66 | version: 1 67 | -------------------------------------------------------------------------------- /fixtures/cassettes/get_post.yml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Accept-Language: [en;q=1] 8 | Connection: [keep-alive] 9 | Content-Type: [application/x-www-form-urlencoded; charset=utf-8] 10 | Host: [api.vineapp.com] 11 | Proxy-Connection: [keep-alive] 12 | User-Agent: [iphone/172 (iPad; iOS 7.0.4; Scale/2.00)] 13 | X-Vine-Client: [ios/2.5.1] 14 | vine-session-id: [!!python/unicode 1169361810303164416-7508513e-8757-485a-9fba-9870d111e3e5] 15 | method: GET 16 | uri: https://api.vineapp.com:443/timelines/posts/1167619641938518016 17 | response: 18 | body: 19 | string: !!binary | 20 | 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 57 | headers: 58 | cache-control: ['private, no-store, must-revalidate'] 59 | connection: [keep-alive] 60 | content-encoding: [gzip] 61 | content-length: ['2086'] 62 | content-type: [application/json] 63 | date: ['Tue, 20 Jan 2015 16:53:51 GMT'] 64 | expires: ['0'] 65 | pragma: [no-cache] 66 | strict-transport-security: [max-age=631138519] 67 | status: {code: 200, message: OK} 68 | version: 1 69 | -------------------------------------------------------------------------------- /fixtures/cassettes/login-custom-device-token.yml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: device_token=a3352a79c3e29283a03a2e6eb89587648f5b2a291c709708816ec768d058ea45&username=bobtesty%40suremail.info&password=password 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Accept-Language: [en;q=1] 8 | Connection: [keep-alive] 9 | Content-Length: ['129'] 10 | Content-Type: [application/x-www-form-urlencoded; charset=utf-8] 11 | Host: [api.vineapp.com] 12 | Proxy-Connection: [keep-alive] 13 | User-Agent: [iphone/172 (iPad; iOS 7.0.4; Scale/2.00)] 14 | X-Vine-Client: [ios/2.5.1] 15 | method: POST 16 | uri: https://api.vineapp.com:443/users/authenticate 17 | response: 18 | body: {string: !!python/unicode '{"code": "", "data": {"username": "Bob Testy", 19 | "edition": "US", "userId": 1169361810303164416, "key": "1169361810303164416-cb76b64e-ce4f-4687-a8bb-0986e7b696db", 20 | "avatarUrl": "http://v.cdn.vine.co/avatars/default.png"}, "success": true, 21 | "error": ""}'} 22 | headers: 23 | cache-control: ['private, no-store, must-revalidate'] 24 | connection: [keep-alive] 25 | content-length: ['249'] 26 | content-type: [application/json] 27 | date: ['Sun, 25 Jan 2015 22:17:06 GMT'] 28 | expires: ['0'] 29 | pragma: [no-cache] 30 | strict-transport-security: [max-age=631138519] 31 | status: {code: 200, message: OK} 32 | version: 1 33 | -------------------------------------------------------------------------------- /fixtures/cassettes/login.yml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: username=bobtesty%40suremail.info&password=password&deviceToken=a3352a79c3e29053a03a2e6eb89587648f5b2a291c709708816ec768d058ea45 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Accept-Language: [en;q=1] 8 | Connection: [keep-alive] 9 | Content-Length: ['128'] 10 | Content-Type: [application/x-www-form-urlencoded; charset=utf-8] 11 | Host: [api.vineapp.com] 12 | Proxy-Connection: [keep-alive] 13 | User-Agent: [iphone/172 (iPad; iOS 7.0.4; Scale/2.00)] 14 | X-Vine-Client: [ios/2.5.1] 15 | method: POST 16 | uri: https://api.vineapp.com:443/users/authenticate 17 | response: 18 | body: {string: !!python/unicode '{"code": "", "data": {"username": "Bob Testy", 19 | "edition": "US", "userId": 1169361810303164416, "key": "1169361810303164416-7508513e-8757-485a-9fba-9870d111e3e5", 20 | "avatarUrl": "http://v.cdn.vine.co/avatars/default.png"}, "success": true, 21 | "error": ""}'} 22 | headers: 23 | cache-control: ['private, no-store, must-revalidate'] 24 | connection: [keep-alive] 25 | content-length: ['249'] 26 | content-type: [application/json] 27 | date: ['Tue, 20 Jan 2015 16:43:21 GMT'] 28 | expires: ['0'] 29 | pragma: [no-cache] 30 | strict-transport-security: [max-age=631138519] 31 | status: {code: 200, message: OK} 32 | version: 1 33 | -------------------------------------------------------------------------------- /fixtures/cassettes/signup.yml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: username=Bob+Testy&password=password&authenticate=1&email=bobtesty%40suremail.info 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Accept-Language: ['en;q=1, fr;q=0.9, de;q=0.8, ja;q=0.7, nl;q=0.6, it;q=0.5'] 8 | Connection: [keep-alive] 9 | Content-Length: ['82'] 10 | Content-Type: [application/x-www-form-urlencoded] 11 | User-Agent: [iphone/1.3.1 (iPhone; iOS 6.1.4; Scale/2.00)] 12 | X-Vine-Client: [ios/2.5.1] 13 | method: POST 14 | uri: https://api.vineapp.com:443/users 15 | response: 16 | body: {string: !!python/unicode '{"code": "", "data": {"username": "Bob Testy", 17 | "edition": "US", "userId": 1169361810303164416, "key": "1169361810303164416-bcbbe44b-f145-44ac-a408-a3988b447508", 18 | "avatarUrl": "http://v.cdn.vine.co/avatars/default.png"}, "success": true, 19 | "error": ""}'} 20 | headers: 21 | cache-control: ['private, no-store, must-revalidate'] 22 | connection: [keep-alive] 23 | content-length: ['249'] 24 | content-type: [application/json] 25 | date: ['Tue, 20 Jan 2015 16:36:35 GMT'] 26 | expires: ['0'] 27 | pragma: [no-cache] 28 | strict-transport-security: [max-age=631138519] 29 | status: {code: 200, message: OK} 30 | version: 1 31 | -------------------------------------------------------------------------------- /fixtures/cassettes/tag_timeline.yml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Accept-Language: ['en;q=1, fr;q=0.9, de;q=0.8, ja;q=0.7, nl;q=0.6, it;q=0.5'] 8 | Connection: [keep-alive] 9 | User-Agent: [iphone/1.3.1 (iPhone; iOS 6.1.4; Scale/2.00)] 10 | X-Vine-Client: [ios/2.5.1] 11 | vine-session-id: [!!python/unicode 967576278242844672-9c2f6444-66b8-4ec5-a03b-86afec71cae7] 12 | method: GET 13 | uri: https://api.vineapp.com:443/timelines/tags/LNV 14 | response: 15 | body: 16 | string: !!binary | 17 | H4sIAAAAAAAAA9V9+XfqSpLmv6J6/qGr5vj65b749EwfVq94xWu/OT4CCZANAoOwjXt6/vaJEN4A 18 | CcuCW91TVafefddyKjMVmRnx5Rdf/Mcfzb7n/7Ht/PHHpvOH50Yu/Pk/4C/HYQR/EpTCX7ths9Mf 19 | nkdDfI5SwYjSlFiqlFCSMIG/OvSb/aE3gif+/T/+6AYPvgd/JPCDp8Dz+2V31LkYdvH3O1E02P7z 20 | z6etphduPQWhv9Xs/zn8M35sdNdhSnjw8J9FWyhWSaFiSvhCxhmTVAtiJZHM3HHV4C6hVhCyRbbo 21 | FqVECmWVMsQyS4UxQm71BuLfnvzhKOiHe97/rIbnrw8yEMW7wx3WI43bjjp/aT80e9e357JAnnAQ 22 | rf54OHocu0P/0g/H/h6OIRx3u/Cj8cgfxv9OiYIhC6GlUdYILqSCHw+GwZMb+W9jxvGPZiaSE704 23 | kUpryWF0WnEttVRmYSLxraHbiz/QwaTrO2f90QifgnEFreBzkt0wiCYww/Hv/W/4m+bQh/7gz/9g 24 | hMpfhP6iqk7UNhHb1G6R+D9/fB2YJUowq7hUXCkGEy3efhoP5HOA9D/fRjidjsVRwM9ne17v9HsT 25 | h1D615hp0vxr3PJJa4VRMLINA6EsaRTweaiwBOZbaqG5sSJlGGRhGBaMmXLCjeYWvurCMEr+yB06 26 | hWZ/BKtkhd7zbWlSe8+04RYmkihluYTvkbH3hjDOraGGCcmZWPwI+1tnW+e9IOrk7rpG85EMep/Y 27 | ddgOrGRCU5g/ojX0IWvXKdNgQwz/yI0gC10vjEdREDqF4X2/H67QfSq2JdlmJHnmibCUCcup5ALm 28 | UpGs3YcxW6KNloIT2JAWul8Lum7oFIe4+lfpvdlmdJvL5N5TDbMP2562hhEJHyJr7zmhRHOtwfRp 29 | ktUfuUHQ+yV17p6bOjHbgmyL5J7Dzg3vhU1VwcRb6Elmi4cDgRklqJJcSskWLd5t9ruNibPvhw9B 30 | mH/bNHUqtyXdZslLlsImIy0hErqiYc3yzANQTCopDWcUbE8tGs6OO4Te9zvhqB86e3u5B2BxxxR8 31 | myTu+7BjMjAZRTnXsBAZz2w7FvdazRQuXWkTNv6TcQiOAOz6lLG8vWcE161Q2zxlx5QC9g+llBYU 32 | 5hI2n6y95xzcG20lnOdw5Jn//N/xOe4/Bf3x6MRt+5+Hf8NtPhTis/vdU3Lf/y3h/IMfh/5L9NYE 33 | TuYoeI1Pzmkf+v3BrHPAYM+ETxNPULffhDnB/sZ/0Q+rwfCj+1Fn3GuEbtBd7kvFj43+rFRZQQtV 34 | LKh3F0oRy4kRSjNyx5c6Tlv3g/YX52mne0S2mu2G1fp+a//c9vfurnmp0+jvFAuk3KvglPgvg24A 35 | nS/1w8iPx4UD6E3O/AGcmHvvH93zR81hMIigYRzCX3+cDP3RyIEzdRj99Yez0QbbcTZwMM5G6bjs 36 | bLSGPkz29C+6/a6z0YH9dIgfKX44CNvOBphNF6bN2XB77mv8Ny23Fz/hwZfysZHADz1nYwSfDdvo 37 | jUdB09kI/Wdn4/DoEp6DKUswUResxx0un+7pM6M/y9xUCqqoSpZSBr4pGiN8WdhbYZHcsUaLG95s 38 | wr9vSZj52Qk+fjhtlNjjiD5xfTpgfr12X9o9P5zY1wovu6HFrjX7vR5M7KztUL7oV0oKpxj4yYwr 39 | ygxsMnN+ZU5DX2j2G0OHvgZR4H/EBOEDNorzBpMYue0/8VPHXXPDuIV/p+CrUYvdiyaDeAeBx/CJ 40 | 4M1B5Ro2KimIIXDWEosPBlE3fjJuLN6BEt50CX+ceRODj8tk2ps4+9oytpPaMpjobMMwK5wsGwK1 41 | GvxsQuLjYuZF2Fbaez5XwczrOMRngqW+jgkCTjB8Lg4RC8RLM6/70mTaWw+PD2deJ8DahF4yOnRf 42 | JewysCFTeOPX18HaTX3Px5KefRtspdIsm0uh4ZXg4nOBe9zXt322mPbO6d4x80IJFqXUsuEZqxm4 43 | KZIaqZScsz9sLu1tbzvUzOsUxIQ61QgtAd8MHFJNOIclJ8zMbL63l/a+t41w5n3oWhmxbD7hKFcW 44 | DiOi4ZgwX9/33l6qfcb77czrDMyPTbdNotDdgDEKdPpMHOl82ua0ubS3xbv6zMssWKZdunVQWAcc 45 | PpvATXnm001bS195eHTM7lMEvgW4zUteB14FwhMQ2FgC/t3suosbTHvf9Iiaex98C9iAlw4PHCBl 46 | BTNCWDbz6d4aTHtffBTOvo7CtwCXbdnrIFwwsIuB/wAe18ynm7aX9jY4cWffxTi+a+meAv4JwW/G 47 | 47X+9V3YWuredXQ59yacRJ4+Ki4xloAIkMA6UGLW/rG1VGt88x0+X8VxUDzL1gVeLjWzJ1ncXOyN 48 | xqDYYf/54uxwif/xBp0N2fpBs8hchzvV05tas/MKR2/juSXKj6fR7V31okUe630cUQNc1k+4b+AP 49 | ey7O0RefaYSdfuvu05/H/lObt71a95q/e/IfCEvHHXb7vXiXXAwOYAY//tzqd7v9Z9yQ3l777mK+ 50 | DVwL9HLx4IujmXiKZr24XtRMnMf1T+LTuFc7bl6+nilKIJa+Obh+fur4ncer+1J95+Ks88fHeM78 51 | x7E/ij4mczYcEr8o/UVUndptKbcZ/RIOjWDi/KwzPhj2W0HXL4Kv1x6CGxm3T16U1yAt9vZAr//Z 52 | i2HswM+6nToBF4ZNgXOIhARMGHgeYh7OzBtfzTe7zO18W6Irw8/gXVZpwSqYdjQCJuAzcwphMkJy 53 | d5x64E95WjXs1AiMYHCsMNjpDYNoUEJ0OWcEJ9GlVyUV/3rSMw/efbsNHuzu7kl5Mih754WL26zw 54 | s5XgCAihhdQUwozpdHyHPseO2PzXAsu1xigBvTacW7YUfD4H78Z3ihB6+pMckTyYLvvFTJ0xRG4Z 55 | TwZShBHoPkrL8HpB8aw4xPxIFpFbWF1B6JT9Yt/PAzxPu2/rRCKASNMARDAAIzi42pIRblVW/BNO 56 | WqUI2BViYCQJeHZHI5j93X6j3e97efvPSZ3Yba63JU2Zfm0kiQMFQphWmfFbqoSKXQHFmIZJWATi 57 | +r6z4w6bQR7Y/L3vCGFB90UK9gxbsoRTVVqrOVEmc98NlZxZWOhCSpKAPdcginA7zlXQfHiGhZl7 58 | ABQnX4D5J6KgFixGGiONFpaiO5wVfGYQBWsBvi0XEmwvYe6HD895oTfCsNNMbfMUi+dw6ilNiDHY 59 | AaZTFuzCjRGDbyTxexkCQ03odcUbnY/BlagGL3vRt92fezxtNKJOYfFyGFDaaOAEJ7Dzc0vhS5is 60 | F0cQfCLMA2GMQLBncf3uHB+W70q7Z8e1yq//9a+/ipVCvXh8nRvOJXWKe2ganEvxFgvCKjgiGMSr 61 | EEJmHIeEAcD5Ziy8ycASWhjHpN9fyw2ewDswKrepTlwLGg4AsIrYfYODQGRdC5rBR2N4MU01uu+L 62 | 95BBeD/ZX+UegG5zlbYHEfARjKUMZhB9RkUyXyBJvPpAFwN3YDi+8jpK8+fgMkdpMwmIxiuA3wBE 63 | l3nVKl0ileq7M4VgolQQaUn1DkSnuFBzOOnhqC0m9tlUnsqtA9Xt7e3Vn25HpRe/zu7bZ8NOfiB6 64 | 4y1eXAEAprQidalMiMTLUAPBh4HdAbfn2GnUVjYt93WTw3jl3MDUpe53micXRUZ7g/3JqFi/dn1X 65 | DkTz7LE5vLxNBYDFomsHcwxHAZzhXINZwf66Hkd8odlZ+3pr4mcQ8HyQDlOeDlB9H6DnCZrX7uoP 66 | t8qFi93QDnaLTXHRPJUPRX4f7B7f7l+OK4E6zxk0dx4rh/L+9mPXed/Vbn0/dJ1CF/4XrBo449LE 67 | M9BSOM5+GjivfSLPK8VH/Xxsdy7dHn09GL+aQtXVN+PHnTN9V3u8zRE4M7pNxE8C56+znhI4c95s 68 | NloZA+eFSIysY3GSb5biusJiVuVFVWSmaPETUzhtEKM22lphzN351d1VpVi7o4LCJ5eKg39omJJE 69 | ++CXq7mv+1iIKo3nl444vr6MTqKdWu+hNCySzvHJeXFMS1HmiFgwKdGELBgu0gFIhog4mdcGe7UB 70 | jxppKHiOLo2Ii0M/9NzQKQXNZj/8PjB4/4X35xMDBTBTjVFmDPGksQ10fNcNrqaBTTirl7owuIQo 71 | c+h3J85TvhAH/CMex5diWybTyyT4OFJbcLKlhKWf1T0SApkJ4NBCgMPgUy+61+duzzkMiuMoCvL2 72 | nUmcdaFnN4evfYceKwvuCtHItcrad3C/Bd6qgFsKo05gSBx3PacGVlSDX3L24S/z0FSmQ9AYFnCe 73 | 4lVDZ8A71Bw2Y4LgqGQZQ2RhNQQEXOF/FbICf3ewFo8GwQqFpBueTDbLG6zBXsSZYRx8bYj6IXJN 74 | JCu6I6fc90fOeSf4fkBpcBFE/GIbI7U0uAUelBAzUytgZQqakTYkDVIUYQXDNgCRJ1v8HoWu/xKM 75 | nPspcSh3/xl+AU5SEAsMsyBYIWDg1sJRTrLCRUibtRa5ZjB0Jeli8H/ggk83CZ2Dt/vLnL2fwuSp 76 | vbdUGwS6BJF4DZO59xBQY+xCFEWG6eLsV4f+qFPv+OcR7PLD3P1XdaK3OcabqfYPAZSFfcUinyAN 77 | elkINglVwioYL5zaeFj+VrAujSi6ClgnLKLEkuLazR3IzJ2FPw6UrTRk/XEyDNCA+1zhn96VoUQj 78 | xsZIsnd1F/Sg0/Ox5MtV66k/3KUX5EZNRtcXR6Vhx2tDC/J5VCAnKwTJ9WLdAbt2+i2nN3FawXAU 79 | OTiO0dbWlrMxghUbs6/e6FXd8MnZ6He9wHc2IMTz+r0VI2xmaJVaogsa8SoOZmANsYoIDEfvBkHz 80 | 1/AX5ifADo/ngyZNPLO422gYnKO5eepdnzQOxpeB4k91fsRO9dlu9TEcX42frtv7rdv71JhbJTiP 81 | VuExBH2TCgkG885jXlOdb3YNMTd8w+wxN171WzB3Dkcn4kozMTe2lEpjmNrCLFsJXrX0VhzGCNaN 82 | 1CgGEejMVf97e9lpDNzCwJZyJvKSGOZRCwG/K9NJX0txC1gjqe+Jl84sQwniFrmELKGphm0d/Gxk 83 | o0KE8PVN09ZSCUof6/PzZfDr6hvSidBEw/GpICyZIZe9NZcLkVlnlBn1xuUj6b3cu/7B+V50Tejl 84 | 2aOqPV+Uq1vC7N/lA2PC9s0LY+3Dm3kw5txvuKMoAI8efEe/603deRzZld/oZQm2Vxz9M7zmy/Av 85 | r66vhlcvpdJk17/2dka7z4/ebf1EPNHzohyPH1YEjChsDUgr1BrWbnzRkAAYLQEVOkF7vaDCS+eZ 86 | PT0dPOzTHW6eXWYOCo+1/X222z0Na88t98eQkUbiPFU/gIxmbCMVMsL/ZISMEqAKiRdoSiDjHFnv 87 | ZE1Ui4Vmvzlt1gUribIsFFiZGzq1ADjiLTfgJoqYqe5qgmT9RsOPkUMuwfdj0EFwv8FGGNVizgqe 88 | T/rF48oR75RJeHBLy53w+ei2EbC6VwlLrfFpVmgJ3GwWWyLeARFEl7KwLXgCJB9fkmO+AccADux6 89 | Kba01gt/kwJqrHbh/3UsizGE2+3DDpgz2+y98xA7S77NUnAwRbhUlCkI5w21mmYmi2gpMYaT4DeC 90 | 6SwGQM2h+zpxmp2gm2fm3y/OofOwcaTkPBGJ5zRsm5LAFyBTam+m634jptRBilthQvR27456mCnR 91 | 7b+6YT93/0UcfaZd/FspFezFBHkfQkiZmemCHAEFcafEVDuSkCp3MOkGGaDUB3wsDjvSLvsprROO 92 | 8b9IwY+EjG9KGWbOQUCf+bIfbAd/kaKjCv9djP/L2DGnueXUgm43FwAwHUDMvYAB0MQBWGS8C7Aj 93 | SzkHry9z0pnisAIYFQomAL7e4idg4pd2jl8mzt/JE/lH7u7zGFBVqdmWGp/EFD4N61dnv+RXmFiA 94 | UDIsf2ESVsDBxA1D17lZgZ1AGdKkaDJTB7k2EpOoOKxb9oP0YsrA0GDmLeaescVNc6/p+3vhyM1w 95 | l7CEoWC3KUkDjSixTMQMAUxaw6u5jH03SoOlIUVBwtJNMPpaPwwe8idFx8w6zlKZabBnGMLBKzEU 96 | jEVkBozAkWR4X6K5lUjJyM+smDvxNn8KGMGk8/UDRlVhS0TwUoG/+00wpbDFEqWNeCNWpHhLc1DI 97 | zbilT4ePg8uwelG5lZ2dg6dG8eblqe3f71Ly8rACZFTo9vDHoyMfXlcJ++N2x2lgqt4QwiS3jYGi 98 | 72yEbtRxw9EEnCnn/zgbUSN6w5BO4JdvwFcrD91WNHI2js+OCrtvSXtqBB5U6DX70LKzcQ5Byl5U 99 | 7Q/rHf8yThecxaNWxJ4KRVkpWiPKJTzJ8CgzyuIGhiksd9xaTxm/qVssgd2x1Sv0t0ypPxBlT6ly 100 | 86R+f/Dc4SI6OC/dh2EzFWmiCcxd/NKcgVGDR8rR81kXvWOu2W+c/80MUFPCt5+Hnlg6bqLBaC0E 101 | AkTBfiuJnEFokppOwza+mtrM+zFk5elwCjVxKqfhsO1wO42HPjOivjaa9uYvZj2LTZmliBGciRbW 102 | K8ekAvgkcgZa+dpm2nvf188snsM3HZWe/yIFZlFTTfCNRKgZ+O2jwbQ3zq7TmfcqeK9Ohf0oonAa 103 | 9TcknAecT32xjxfPtZv2+nhbmHmrtpuOSUevuLEGX0bwHBQc5+bzpdPWssJ/BgzT5oT/luUVfd3e 104 | Zl5oKaafLSFKwX8J7E3g32rMedNf3zjTatqr5/fSmddD/LI8P0zCSS3ja0Eb3+/MpL8tNP0TBHma 105 | LsbpPwtDpshy+yaT61sUOQ8Gum5IxD8Qx+3+ziPsKPtbxwejvUrD64xLT2eF43Pl+SwnDlo7dE9u 106 | D/j9PA56KC7OKkeejx/7LQhaBWVESRMmIFzg5Gco49rncWdn6+z1tsJbl1tbxYv2cHIyEOrmoXI8 107 | rjwfHdb9HAAj5dviJ8lcM5O+Dk6aTPYxGASIcPzACawWuFAr+BgzzX7jY6wLYIQTtKirsPdOrUCj 108 | CWgCm6TQytxx3wMD4x5xvakVoJoShBoIbROJ1IZ5ZuLO8/Hgpdrt3YWNF650Nxqen8rhoXKfeP/5 109 | hm6lAYx/iJb1Ycto+qJBhEt932eNlsvl1DeFx0pgI+3+cPL2eKMljecZD7pgjEehy4Lq6eOfISlV 110 | jBIlMfeAWinj8/PblLAkPTKBPCei4ROJ+BZ1KUhZc6PIKXX8/OklOk7IgLAvMR/MIjApLQqsWaJF 111 | dprA/CgWotWG74NPp3L329QJ3YaINYUshvCEEQazwcCXlpkRAgGhhIa4wmqLeQSL0Mx50OuH5+4k 112 | t5QRscjpiWliKakjyIviENFwATEAycwqkQwZHbCWMJONJ2gxnY0bfuicu7BEc6OSlGDeiKBpCXgU 113 | 3B5YChI+Pyo0ZFfwghUjtKHwyQiDj7Y48bvusOtPnBM/DOGIilYQIYthSUq3RVoeW5wIRsCbEFyL 114 | zCRJGIJFDTWBZyQsmMUhVN1J1HHqvuetgElSPK4k3+aJOA04fCjPQpS1GjVaVFbDV5RaBfOPpKxE 115 | aKwCkbEDH2GYX8SLIScJ4dQU22Gwy6CCHvgFaAWZLR8+GCxb+CdKK5GEpKM146lpqXgr4algO8gp 116 | hmEsGs7Id7tOszsGF2noKJt7ADLGswX8L9l2YMdG/04xiJEweypj/2GRIPsTRsxg3STM/7nbzd3n 117 | mIsNG45ISVQzVljJGRUCJv4HcCrmaSkIPlF5DT5cblhy/qTb/CksCWHpb4AlZRkCL/DyiH3ztkSs 118 | rihiUdF3WDLZx5rDzS6PH+VLvX4+vKxcj9nr41gP7x7G14+6fgWOd3EFWPLMDcKJ47kT5yxodpwd 119 | CPichyD0XGfUCSLnr7FnuAf/7zH5BjYG4SgajhGic2MRskGnP3A2qoeUOBsnw743bvrDuJmNIsZR 120 | Zy5q362IOcqKguOA20p8aQAzSQjsToLB/oqZc9YSNyaCephQMzd3Z4HPhuSpe/YK4UzloBk+hfXR 121 | 1nFws/daei7R8ofLefTBfPadcv++nwpGJkiNCQysJKxCWAWYx7MuLHK+2W/ihM0cqWao0bIE+8uN 122 | 2ny1k0XMbwlygdoBVuEpqiFOVTMvnGk07c1Tm5yF/GDm5DJZJIk55AjVxHpd9Os735pLexta/izM 123 | R5bSxThyNmMwFTxMxdkMKBQ3lgotfllds29Um45OHx0myWAWZiwNCa7JDAw202jam+dW8ifiR5ZJ 124 | W8G+DjEaeBKoCsOJUrOsya+N5sKG1h3NCrV1+uS3gsujiijcXe1c7tbK0XUn2BqdHY3q9d5CcHoO 125 | 6zR63zZ2+1MVv5+jR/Ty/kAfXzduF1h0x4eV/jhy/n50WfrH329qf41hdI1/fA8jTXuJ29VwUnoT 126 | Nr84/8RkPgGm6ZPTE++PGv6Gc9Jv9qfRySz6pK2Co5pgNs80TyD+3fNoerr/cVL4mJ+C56GY5fuG 127 | NhvQQ+NzczEa8S0xegy2Qj/6M+i1/2xOn4Vd7O6J/dkYB10PevxnB2b4jrOtwVs+RGbwa92GcqYq 128 | 0Z06bVeEy472D0XwsnN51Wv1H+/3K1cXNz9XMlLbTMwyHL4Dv2ZsZi3suqQbNk5h5zfokiKkS9aS 129 | o5nQ7Den2rrQL2UKAv5HRCE2A9iZCIp4ctiYBJhBwzfGJ8yaKQYqUdZDcVQkAAeDC03nt4uXRvVg 130 | clWMgg4rHhSK9+PO9XW3OLx1B7dR+/koK7sOL0+tkdrGiTjSmCzkOpUkZUQwmQcOF4F8VIhlluJW 131 | 5x1ov+s6u/5918/NT2Mm1kbhqWpAWCAAGdicYsagyhyQzY9lIaA58vvhYdDuRDlTBWMlIxQT12lK 132 | 7mALHCJiJDpRjZlCmYmBCE4SCKAlU4lAypnf8B/cjnMYdLtBYxh47dxkF+TYcUw3FSmkEYLUD5Q7 133 | tmhoIvMHYBp1KWCdIDcviWG0PoKg2CbJQNxKBEH4ZERxJE8nMOyKfnesdW41dxTRQU7aNk/JMkXh 134 | FkQDwQfCDKvM9Qs4gnaGgQMMlmdsQpZvf9AJXOfSHbbd/ACoQMsXNlUDCFYsLDwKYQf4/SQ7/sYl 135 | 6rRCv+GfMP8J7K7X1zzCaR9AuY6TYdMEc1AYDw532LK1UJkFf4RScc0LChEH7L6LxlLqumMPJr2K 136 | fxV6uZKT30fASHzWJyOHsONRwuHkQRUf1BnJOgK0dYG8OhbzwhZGcOiPunmywt+BfjbdYlKU/xlK 137 | vHDMo4wl8DMDQJJi+j5Km1ImZAKfDpzLoXPlvsIhNfSd//uvfBUaqcFjKoXJqxBCM3jLYsAXyH5K 138 | Kc7xcLDS4pI1+SGs+eNu86cQljZKrh/CgtEp1MCy7N1lwq/GUaPPvGvnJztKcyjM3qVXLnJxoE7k 139 | 5dHWcTN8NGfqYULM3mB0Iffc/AjWru/03InT8B3KnLD/7MAKdVr9IfKhnAZETxsnfuQPT9zQcbvP 140 | 7mTkPMOp63TcJ99xnUHgN9/TODu+O4z+5myUg1HoT5yN1jgMJ29i+83OMBhFmBH/9uMuvmaa2dmE 141 | uMRL2tV+AnHxUqHKlC0bEms+ClhRuKzBd4Xt7I61jIWZb2mLEBeZm9ziTYOentb3Ch65DXsPjZ12 142 | szvyny58r92NDqeX1olIVsLlJ35lrP6CZFPL5IITmf/Ge7bZb3z+zQxI1vt3XYCzlijBg1dAUTFU 143 | odqAVbNq2x8NpuEgXvzpZyEQuumYpbwYpHVJVMDm4IPNICBTS0pX3Eb7m32Z3YRwYcnLmIZXIeMJ 144 | dQXmZO7j1lK5XHMK9zgxEOwvfVUuifuPhTRHpUIqF01FBSmfecFnI8u/FC7S2fcgnRNOq2W8KUvx 145 | 9hcBZkJmKyB8rvwfqG1jLQeWbiC5k2I/d50vL7PI0lqm3E+YkXDaCMpiPZOZSZ02mAuKW3No7bVK 146 | /dNCeB+54dE5uXlmYfHyPNrTj5eV9sUx6+bE2dzr4oBK5c3jbEfBQ7/rO6X+MPSnuP8qLC2IsmDv 147 | Nsq+35/+RDxszRPZ3dl56d/vek97O0d3Pe/6sf9SOyjcXtnenb68PeI/RqoESuuwHyFVX2c9BanS 148 | RpNmMyNSxRLyCiW4XxCtcqLw8kCsrdTLfLPL3LK1wVS2zFiBF8pxvSKIBzCWwVwGJim7402GZHDB 149 | PRmbAOMoIGkIFrvTBrxoMm8DpPXc3RqU9rfMsHt4evBy1R6/eI0b74Lf6M6JfU4naTVQ7oA2W1YJ 150 | VyKQ0VKc+DwjSaulkkha0JZQGvZXIsCsmcpC0mJJxX00g7MHYmULUROEHcvBrjO32fG7Ti3Af3Rz 151 | yxfTKdrF0qS7EaEwTFNYiMiszEwcWhjMItoVNB9Qj6aRX0sH06IEVqBLTiVN/DKZ+o4KC1qhkg2s 152 | lgTaytqlpESK4u8KUlKCoCKWpFaxhPD5AMKJEAbw5I7ysZ4+xJjItjAp/YcgVGP4j+RG857tkKn7 153 | OPkCxcwwWyJBeHytedQ2rWbnSnnU4E7iPSG1LEGSrxi0nav+MEk1J7PqeKwiJZOBIwlbqIEDFJVy 154 | fqAlCL3VyqKLLxVFG1ro+BpUx9MSMFdTHdeGGYpCOixJp97FQotn/iTsvyt35Ok/xwRSSlOYTjGw 155 | jmqClELIgqLAWalmEFAJOA4VeD0cPtniXlmOa93thU7M5AiGeTacd5iUUWSbkeTkYykEmKtGt51j 156 | BnJmkBeRMgKrFbpvkqD19ZLNZEr+/UpkM5RbA79DkxWUuheOvWXu1WYicYvK30Dc0uCuVm2lwirv 157 | Hhi4VdLQOO3iDfVK8bvmkBnDKuNXetTza/fNUiHaOSuS8zIEGb0DISh/PE+HveZQLhQzdFrgHDuR 158 | O2z7kdOBE9V1ev5oBJOFeJfjdrtOBBbfcsORs+HCL+Dz08dbsQzR/N++4Vc4rSuiVyUrOHzAIq/E 159 | +7nEAyyWrkER+Dtu/YbfavoW1hHdmp+jyXOzqVr3pPb6OFZ751611ry8Oz26Cl+D5v0kPF4kaMWd 160 | TwW1WIKzSOBIRbhWC2TBLMiO5LXf+WY3Vwe1kr7bTJCPx6pKJfugEwQRIcy/VkzEQuVfY/zE1tMQ 161 | htmHZzqhybKkwrgT4AYwTTCZVshZJGqu3axQyirphcuAlHfzn0krtKkgirHTVDeLApIGZ3jmRdhY 162 | PghlzWGf295p7NqTind7vN847IiHx6vBMQ0q/QN9frt/vreczVT2B+4wwsXlnEf9YU5mk7x3r6on 163 | 4VUC4jIbzvwONlMBAr3QOXGHD4tcJvCxJYpuEgtukHr/zQ8uU+1zdr5wmZD95FTdYNh1Q3Ashlm4 164 | TZ+BNcxJL4HdNOrAmfan9zHdI5zt5SyndPBozSbknd0+V6KzwZXwDobmvvigw/vDy/7xVv+6dnl3 165 | +XP0iMZXcPoH6NGMBS1cBeVVDcNrbYv6rzGhbV1g0Xyz35wG68KL4JAtFIoKooDpV8eaG5zLOG+J 166 | gCV5PuEtS5U7rTcg4lrrBuwAaSpYQm1ePO7ugPRenw9p7ViX7y5vL09Zt9WpHEfuuETDPV7NTGzS 167 | sLwkMUgKiEuAsM0MWI9d/GAKi4OBw0Rid9ksoHuzzvNt358459i111VCXY22mixajbgWODYQgOAJ 168 | x3hmx39+HAmhl1/KHa1QJHVgYaeUrDYKtskQ4OGcM7zAzdhrhtnABONipEMlBFxRB4IsHzavDpYv 169 | X4WUQrDMVgq+gKKeYOMMmZlTpClruCWQ7CnQIaVxYaH5/j+4kdPs+LnT2bDnMTCeIvnPNXgLyKYx 170 | CI+QzAlhaGEQHEvJU/SG9t2nwKmMoqHr5VYdwn2ZYJAr0pIhOQIMCI0wEouVZ+w9IomwD8USUShm 171 | udD7wqATBaFTdIfDFdIhsf8SOTUshYoFziC4oQrxEo3oXlazF/HiVgx2Six1kJCHunZ2SkqC1Urs 172 | FCSWaAjxk/CpB9fv+g2nGUD0nLvzPM7l5CkgCSxb8LOowQ4QsJ/s9cCUYigngRdfsPUkzP+6URKZ 173 | SIZbBSV5K3thLbEJAv8RFkIJnAc4qb7fet6e3po+nTYU9VY1hSVnF6KGtcFrSSWpNNlpfSglYjB7 174 | DSlyWPsiL81p/gDc/CngI6T+DYrzHIUkC2Ca6t2FwjscTeMC5u+V2VIcpzk0Y3ewc8oPfRlem9fi 175 | +Ygc1VrPV48VKm47UbM+EvmJTtopF27OneNdp3bj7ByXnY2o41Or5ccfusGT72z03CiadGBtTJyN 176 | tt+HGNsDS/K7zgacEr2OG4bOxrA/GvXcptcP3a7nbAwwxBnDJoaA0IpQkJSlMjMVRIIwixkRUmqI 177 | JHAGc5hLWEC+abIGTN6iPtjZ+UvpAWLVyeSKXIrrZ6UeT9tycnr+4EfudTtdHywB88FdHz5WXBkF 178 | t801JS8sNPuNl7+ZAfN5+3yzXA696bB0qg/FCvfUctQRMQjpfwUh3ttLQzy+mMvMO5lapktPUeMH 179 | BZXwUjeuPGETXho3mvbiT9Oc5WzZpSpgFk9v2Hcxw9yACzuD7HxpMu2tXxfBzHtRRV6lykdhJWNq 180 | LEZyOMdkep/48eKZVlMhtLcVN/NaJTcdveS1mH4vKRycAkursFlB+Y8W0145s7RnQTt4r0lHDj/f 181 | K6V9d5Q+hexnmk17+ftGMounYZbgkuRLzTF1myJtTUkxO9yPBjMTrvBQWyoK9i1OmAe9W3sQPgxP 182 | apPnfmvMq1G5+TJsNfbL/Oll391qTu5fjxo5ATnv9Wk88PTePCB33neq4M3+fS/6l5FTcz0v8BPy 183 | DGG14U+6k/Ctclt2VhShWKAqJuaLmHX5E2Br7bNbatz2L4Vu8caJ3h0OFWnuDNvN49uzq6er5qhf 184 | yYFsYRBif4Jsff0U68jgS6rGIjHkpNZYrAMj1oV0LTT7zRm4PqQLtgge50dMzYBhXSDJlFV4y6Rt 185 | g2njmaaamoEmyMKhXCpwQTjXQpI5M7C1k8bJfVCj7mmvdbHbPqi5fu002hmc3UTXTXKbGenCSxd8 186 | mTXoIetMtRd5wgebciHgdBc6ris2ryPwW3kdaQH0SryOL2P5PfSINCrWKvQIOIwQfCG4VS30+q+x 187 | aEny19hIX/w1Vorkz93jce6eTClhjhLr2nAEvKxhUmXHjaiE/2CZJExgTuAXnPkeuEz50/biRCxu 188 | UpA6LHFpJRcozvEzxAVj/phJhjVOEkR41i1AxVNk5VcSoDJYQl5YpAeZhCqR69NwSkaLVtNwshxt 189 | H4yaJVRUXC/gorbp2jWQLGWooIvC1En7jduD7XIFEugXkIXBqk2+HADHA6uMYm1XTCzMnPqpUeZF 190 | Co6yAyIp9bOAgv6VbjfoR7kpTchg1aiNn2b4TMYyIQS2HCEtt1mNB/Ya5F5CrI8KUAkkvsKok6Wo 191 | RVq/bZwpjIV1U9KcJR5OCFFDCMM0ybpXQnzHEJsX4CYIzBTOD23NnnabP4a2GP8NXCZlC4qW4TQs 192 | f/hMKINhMbHPvENbKZ7SHDrT7EdHw93u8+35sHH82NzbEfWdUuWgz3a6PLT3Z/mhrT3nfjyKnHY/ 193 | wjy80H92NlB0J/RVnM3XdEOnFUDgiZAWjtBx224Q/u1vf3M2zrAVVM6Pxal6wWjke5P++ClIrB/x 194 | PZD19AlkIf/fFNkvUdTFX6JcIr+KRct+gdNbKIoSl6asf0H88WtqjzB5JVEsW6Lmpq36oMkj7AV9 195 | KVuy7u70b+sBQxGvU3t3fGW8VFArSY8Wy0Bj8iVD2W44I9fk0M83+41Dv5kB1Hr7gLPBObiObFl6 196 | krS4hwvw11AzdAZfem8vDQhYkOcWmAmYimbN0naelglwv5nYLHSELKz0kVANGwoWmGYou2Bm0+Pe 197 | 28sKaSi96eh0wazcCl0Li+UTKYLJMemjU3DK4n0wqltKPScuPttqTjBlzXFee8s9vC7y6lWBs9Jh 198 | 4W4UPDWiw62t+/rBxX1l6OYEU56Jro2vni8XwBTfBQe1tbrgN0EdSyyya/Wb5FJmzaO1z+GL2HrY 199 | Uzdblh2T6mDnpXPRie4HlRPbOji5bt5380AmAtzYn0AmXyd8HYrfSXlFMElWMqTIG+RIrOnaYKHZ 200 | b3bYdUEmolTVlQIRhViDEulujGHhd+gDmMFMUUlNFea5ae36+NOmnNd8v24f8ePd0ePD1l7jsb+z 201 | 7983L8+Cy+Lr0+XFWfu9qGQGtATlDsAyYZsXCpkBWdASmpCrDic0RItcIMUATi22XKg76vh3R8HE 202 | zaM/EgMlSLBhaLRpgRe4VRarwMKwwBfMXgp+fhgJBBs39ooaq3Q9dvuTGSpI69HgAmiD+zpS+jPG 203 | XBLiBdxWYo6vIAlBS+gEzqMzdnLn3CHQQxBdJcnSHUnGlKnnXGptmJDgyGmrkhRzn3zX7fzL9v9Y 204 | oeso625TAl1KMG0IBcOZRnJKZrViSaWE/VrhLT5s7QmResHDqjy+cz7p9XMVUpv2fypNx9IMHpml 205 | qDkM0R9GfiyzwYOJEWMkym5aKZMk3iFSd53zaDxsd8DtzzsAOrUdss1TzJ6iPiVWstNw8sGumHUA 206 | whoCRyX4WwSTrxeNp1o73tt2RuO2O7xrkiZEK3nHwBhmPULkS5KTeMCIBeZEYd4jt1PpmExZgxDn 207 | S4x/mUJx2QRymY/xVhF8mdw8SiYQKpEqVWENgg6BlCAmsQgpyaqwBmECxeq8TOF1fBKz6fCvMThP 208 | 5nwX/kmaeZhZ7/J2uG+abZIilEUoJxRT2CQmME0ZLZmwccI1lhbAmkso0b8wgOn1chT5DnhSg44f 209 | 5scLJY4B+awpvEos8aJRSxkLaZof4G1SalTGwxKmSuSHThbOv82fQifwW3r90ImtmEJZVVCF+N13 210 | UkxjyMOoSvad7oIedHoOANDPWhTa0nusHr7Ui+ZkfL7Pr05fnyq3l6+Dp8puOm4yB5N0+s+Oh1nU 211 | QeS0fL/7bzHqsSJfh5YEr/KSLCsLXxM2ZTiPBMq4IT5EbRPME1aQy7f4Fr0T1Zd2YzCusej+pS7G 212 | xdI5a1V6NwP7Urq9abUq3fOg8ixILXg5Lx4X/U7ZnPgjGzXp1ZAenJ4W5mbG7shj/ix6qnNVs63G 213 | 6PbwqHiwc2mjh93LfvewlQqNJElwwy6GVszgPMTsozXleC00+43jvplDgjuGRdI1i74P8PME2euM 214 | DC6q/vGw3Ng/KZOT61pJBMOwt+/v7diT0Y0QVZszvh6eFIYNPZV7SfcpcURXfqOXrQ7WSqN+htd8 215 | GXanuyVr4nKf7w3vnl5e21u3UTQS/Rt/9/lGXpzvrxj+E8TiNAQSUhjyU85EHAJ2gvZ6Q8BwV7Xb 216 | 4+C6u8Ov1cVgr1isNnb2C6cHBjZE+3KUAwQgdNY9/hYE+GoVeTKCEriCsMgFyouCTwSxjDXrK/M1 217 | 2+w/KeivVDk4haYypcsQg44VnJBx0Sx1x3VD2CYRvnjT74PAScF2Dxu+jvXhwVmbY8scl/TB8H5c 218 | 2qf2hO28lMOnkV+5NWU9KVf7lcPMgT9YFkdZS5RCo0ZlUjpOUPsRBqW7GTp4Jvaelsb9EEs4h0Fx 219 | HEV5FDBjS2UyroCi03jVWH8Eok8kKaLKcdbb4oVxLN4We6PzMeyV1eBl7/s7v7nH027v4zwbKtKu 220 | AAmSeizhMcVVYmpgRn+QC6wwjQW/4EMn3X2v//o+TZF0let7hhfhinBqErQx1nsFDm55MoNilStw 221 | zEpTqINrZW5lhgXL/N7rWfTKod9i/V65kKZcrhQr2r5vbmCrVMRUnw9J0uQtbc773Le069WfX25r 222 | zdA9rHYfacFvWGp2G5PKLuu08t9nbnhYpdfxxr3B1lp8dKsqyigFZzcK/VIkYpmYpsSZuWOqxX0B 223 | YWgD+Y98bpRHKqiRm9LQXLJRrz4unTxWm7f7tePdcdA40S+PqT42mdt43z76qsfi2xOr+tDxDM94 224 | 0dDfZVeLKFdpDWbuSz6n+zltKzPDGEvvrlIwJ4+3vu4j/V77Bb5Ha82jw61+ceeszB6uTnvVi1vi 225 | HV4U69c5Pfbb4P7QVxfhvMe+3/edfXcYruoRGwzG4UuKd5mmn7CI1z2H9qZzrPv962jQuC/c7B5f 226 | e5UB3dVbp9FVNDg+ev2xM0xQ3Ez+iET8dcJTbsRaLW3Fu/rid87x/KL/Yy33X/+0265qmYpypQJf 227 | LP7CAsvBcoVcTosCCAQcmobw1FQAAWt0WsZJzDsAq2KMz31hdbElz0y1t6NHDcGi+/v9SE/uH6Jm 228 | 94YPmk+jNMf3D9Gk1LaY8gT1tWoQj3tY5+6NcZmzvi1jWNoQmtGGcvBusnjPsYudRDKG4SphCbxH 229 | zEc7v5VknKrdtwLJ+OtYFjy2Xj988CfeOGy3uuM3TY18A8B6pXKbJV9DGcKspJici+U/squZYWYv 230 | h6ABdeRVEm+u2N9xe+6duCisQjOeJtanpReDTyEQ+KPU0OwVQjg6Ixzzo+Csowkg/G+NXtJKV6wS 231 | vQjYLEicbpaQbrx+Wbnk6GUlWTlYBwY11VhS9eF1laqW2yTZklYpVa01RB085T7zAO+icvfaxOWe 232 | aUqCN8UNh0zr7jIb62Jn7baR0uKWLKXkSd1ed7SYLAW5WrSosSoTphomXMPW3TCEnu/3O+FohWg9 233 | rnpCSNoFFKVw8hKs2RvXTNaZ+284A2sDR05N18lC//eavr8XjtxcV+DvfGlq0XaSkR9KCZacMQaX 234 | ntb8B8VvlZZIDZQo34rEiVV4x18PwM2fXp4Zqn9D5RChJGNV2BH4hytmNTMk5pS/hekpDthcAHt4 235 | dnNCn2/87n31UlVao4ed85oIbhV7Nefe82BnhTC96IfeMbypEHp73e55p/980x9jhY/gYdyp9Z/8 236 | WABtozD2gv5lMBq73XLgDocd33Xmnvn7njN19R38Wv/YxEPirjSO8N/uDoJVC+DaKq1UKqUSxK8U 237 | wUZwlSQ4SwxLVZM7bnyXMtcTmmP9wvn5075pnghxNfAKaie6DS/Y5Om1FD2P9sJ22xMfPumH9huK 238 | s5Ux3B3+yygVHkiuHUI0FveA/RRrHK+rXuB8sz+FDxIizveAfH/stSE0Hc9VUsAtaKo1OAcI4NL+ 239 | FXh/YoYHrPxYyBV3XDYVpXpHBHC6gumOGbz5t/OPx5vVkp4lGNBnRjVWsSB6Wf8I5ww2MSYEOJZU 240 | 8ik5LrV/i4+nwSENWDNg9dAXL+h2R7BmJv3xPBCTfptJUWQD/BtMIBUat68ZbnTiikyFZmYX4Uwn 241 | UDN+iZYBfg9U/LaoU2nZnKjAXLtpr0/YF2b6EOsaLJHqjCXipaRWwOTbuSIqSY3nu+hdc1AsGs2W 242 | PW12L2qqfFN6PpYPduepXBsU9e5D5HaeFmLcNZXJla+64Ouyup4Hl3aGQdjuH+L/OX+NmWbuIsz0 243 | 9aGFzPWcApOH7si59N/K8M3hVCi8yjhmF/E4FJzVl3wHhP8ra+WmwmTrthfvrrb/OLyvXO/vNs7a 244 | p73o+G7sFnebLTr09shxKwdMRuRsHti3MNlX00mBySQxzJcZYTKRdIkMUTSmgCsMPxNyt3Nfxcw1 245 | u8zBWxeQZlmFgP9WslMb0FhaEYtacANn2B3nTdn0CfH41AawggJyaBWiUxABwtY+bwTN21Gr29Ot 246 | y1ZZu7Xz3fs7v9woNPvV/aeGOmb97FfIyFKKAxc+vdrbzMAdTwDBJAQ81lCL9dhZAl1oIXPUHzln 247 | 0FyuoptTMqqtExqHQclq8gLdYw0nMMY1KvOd5cJAFqKgdSJ4AjHq9SN4eO1KqI2J0P9cGCm5IOSK 248 | MBLWDcL7g6TqCuuHkWQyKrAKjKSxCilFMdsE3brCeIS6jbvuZPT3uvv6Dyf4Gvv8Lfd4FKYmS7It 249 | k2ExAz6jwfwZpmGVZCZZoM+tiLYwEchwTAD21geL0WQdh1VgMYOsbqlhW01Y2Wdj8Madc/dpBf1M 250 | SrC6q2BpdUbg9Rw1YCVWJpZvXIBsXArElAXWTsD6QL9fzUGksbRXooNwzOqG7RgcugR+zrrUHFSK 251 | 7ayk5gCrBeUEDE3cVtcNTqaI364CTsq4OkxMsV+hyMjc+bjMf9pMZJdr8xs0J1mhZIgsFKdFRtDF 252 | YowaghXn1DuPJc2xmkN47r3qzekZG5W7rcNe6VjqKmmEtB62zkV7t2Fe8iNkcEaNfNjWPQd8COch 253 | 8EZbzobngpMWOhvl+J+fMNnO0VkPC9r3w/ycl8/0+2JFF7VglV9gOaVfQtrCL1MAkytXYRGUFCnz 254 | QuEXyklO8+8LFVauqBItczM3O3ePRfvS79x5fqD372vDaPewOqhU6k+9q8PD171yKsKV5OODl6bj 255 | OmeKxFS/Nfn4881uroEgE3+dWUwEkZl05gpj0hjFNcP87gWOzLS5VCRmxhZm3wqjEakVV8HJgGUA 256 | K4BTVOcmTNCvr51rN+31n6Y382pEw+WSMiUUTgTOCVZCZSjoPqMJ8KXNrNQgTP9ROUuU5KYGrT1W 257 | U3bHnXRpdEkPzh/5ZEBelHw8uB7UvdHhQ3UKdP4cvglI+fB2eFxdYPMXu+6Lc9S/cjsJsA38sPkw 258 | /eHPmEJaG4yomLBkmtb3I73BdU/puDURVz01HHvmKTq95A1x/HS6dTXuHV1d3Fw26z/FQKaR2Ax5 259 | 9DsMZGb+16E3mFSGFXMVmFGSS27wOnU92+NCs99sj+tCQcolXalQXmJ0agZSayz8Y4kCl/6OG2r8 260 | lpC6QadmYFBnzsbplBTLT1I2r6FwU9i9Ou51W+zo4OoYdgh60OJjNR7dvZYu7m/Ge6l8IswRFnA4 261 | +KJBoGHe4NI0NZUZ+US6lcAnipNfwSnDuAhl7nkm1UII0he/O4aYWGwGcwhjifelYEohfEC3Av51 262 | nL/kgq6TOBefpsj/QZckBrBY9OG9NlemmGtuJL8xWuQpxUVWiRaxrj3GDDKBjlPEOxuIF6v9JlhX 263 | lL9Qh8G5Z2ybp4Rb0qCOr9Kx/BnJnFWthLZYUCsuRiRYQnmUaby7MwbHZ5ivvMt0ADZGTlhKXURK 264 | sIQaJsISIRnmAmQdgMTnLQTKCmKWpLTktQbsPIVOsUrAjqZD4dRE7atF4KfRmDiFUWeFOB1TqWVK 265 | HVawW1h7MO0cNSANSl5mjXWhy4YScCJh+pOIIEdBEyu8n/hRAM3kBxoY1hrhJIUEiEJaMT/TMiQM 266 | 0+yYG5ZehoWiUBeBqwSkZ41AQ5rM6ypAQyxGwSCAkAmTv2YWEU8rb7QKi0hjzUpw73SCEEgNvJUR 267 | Mj7cRm4OGoL/cpsocNvS0HP0HAWW04ZNM7MAi8JlYgiFnVND8ERMbh7R/MG3+VOYBCEe+huy8GNJ 268 | HVaqVD+cMAMfWsHMfuIkKa7XHBLgPbRqJVYePlTv3VqXHTwVG+3qVvG13Xd9crrXS8dJ5mCRv8CP 269 | 5034/5ZQ2ehCc2yOTWdnGPw1drnP3i6l/xozMOBpidcrt9tzh9GmUxwGUeSGE6c6xqKDK5KIsLw4 270 | FggsW3T9kHaMKe2UGqSv3THhUtfalhDNLQlzOjt1any5w3rFU/rKqlua07tr9bhvLvcfwyatwcdd 271 | IBEx8JGC5sPIoQ7yiVJBFrqQcfAWcnEW19dkyqxNtny+2W/CiM0f8IiWsXViGoj4/WSd7xgPH8BG 272 | Il9iBsQwdEl3FxMRlvV28emkzjbeLL01NfSZ/iasgs+isHzTsWZZZynmcxKCVAwLbhETSzu78HQe 273 | 6tGy/LB/DvMIb7yXsZ/+OcyjtcfPtyOXF9pa1XdrO8e9F1bq6qChL19Kg7F62fNOllOPYFYDH3di 274 | +OEwJ4j1wgfXtXJ9MA9iJczZdEdfxLR+P/sIjm8U1MFzkvF1so++rWYLp9boDgzXH0ZuEKIJ/9nD 275 | WY+mk56TkbR2O7ryjDk92Oc78kQ9B5evd+3wmYimbG2V+955IQ8cx9lsatO3cNxXS8ojY2GTKtvi 276 | VZdReOGGdwlqTToWC80u8w3XRkHiZSrLvFyZ5nlrZsFdkbBdYslt+OjIeIZDBckCWFRecQi9BByH 277 | BrZvoqnVc9/8sED7VzfBnnfVAcdv0HptHHjd8HL0vHV+22J7O5kZSLB/awsOd6yWKrMxkJLqkyFT 278 | RGLleLxdtt+IWPxWDkyKKORKHJivI/sn3Pyn0HhWuvnHsrGwy2AB0fxh1fxcfO9zJkRWFvXz1h5Y 279 | FXVFQBwg6ccCgxBYY1KTeleGT1lWc8HBU//Z7jYHslc8aHqN3Ul5VNPhgVc4atVg3xif5L9/XoN2 280 | gqWiUraamjJyzeCDIjhpUV5cUHXHlQcBj5EuhK4w3Hnp9stq/aq9VYJDolk5O2/cDc90n7bY8fBq 281 | 4F+9+qlRzX9v7YT5a0vobc56afkvLde8u19X7urRoHRw0O7tHLVv7MHwrEuqXrXWHbzWxjsvOd29 282 | KJTy9UIu6BnUXC8Y9UOn9l5mYHb7ffvxVvzjH95bYplccGCsYlPo/0f3lmue1Uc/aNLd6m1lz78o 283 | q+gZM+kG+zV62FUT73k0zuEnMbLNf6L5PfMJUq4tldcgLZbx2vL/b4WDKroc8M1Kb2gYjW+mIYZX 284 | BHcz31pXaFez1tQVJkYwbSEUQI1OhPX0/CeuufrySJ5eXoeuOSoXrgcX/YJVdjjwBrxImuXMbhHy 285 | MuFdBjZYSaVVWdQJwBQTnFjCYLMRWO9acyxTvNQxOvT7oTv0+s5Z3xsG7XGuW6GpyFfMzxYipZC1 286 | hYiC4w2DZZoKabN6QwvDWbwUCpoPE+d4GAW5hXbfUpx5ys2KBQ8EaT2WUWtI9osVyRECwRxOgVkf 287 | CRnC5X63iRK7vnEugyhPEfopN57EAmssjQVJEKWQMATFoE80uz65wEJQ0sQC4SzpSrHQbUCw6pxB 288 | JN11vTyXctMBcPwAUqVlxnNiJUO/0sZxVNbLFQnxhQDL05qDM24S+r/vNvsN58jv5rtamXZeYf15 289 | 6HzKhS6ZeoXEam05Q9nIjL1HxqeA0RL4XaESen/S6fsN3ymE3tB/zp1ZwfibuAhNiQII3qYzrHCD 290 | YtGSZu2/pcTG6mLIYNV6sf+n4yB0zjtBz+2Nh7mtn8m4sIDZpsmMAPjuCvOwKJYBJCJrRTGwNS0U 291 | VnNjMd6XUBDNHw8xDMt3lz7tu4prAepUiXUJx4+1qCgMm5DKzv+Facd4BPwg+EWasPWs8UpUJM77 292 | ileiSkpMh9M6IYXiEhwpvFM8RyJDHrt5FydgfJuIlKJoViB7GqNOxCvw6MnYfaNjKUeUtjcrCXvP 293 | H32bP79TlGhX6w59ZclWqdRVVXr3ohiDk+5N2Zsv9Z3mQsTOUfDw1Bl4xxNrC8eHVbnfCR9vnu6P 294 | Lk8uLs8mbv7Yt+g+QJDguE6377b+zdmoPwejaHIVdLsVN9qrO899t+NE7oOPcuC+O5o4yO+ZbDpu 295 | Z+Q0u8Fg9P479Y5fAh89fCuSVqvVimeVQnnFwJrbMlFFWSrGMKtEcWzYZJSQyHy546RpBVeu8pvx 296 | dM5JL/p3AXschA+Xrw/Pdye6MHmqDCPvvk1ea4939iU1sMa7tMXrQqXRxAmm+dD1FUWbb/Yb6818 297 | V/jxfWav3fR7RbG0myy87EO6ssH1CUEiW3qTtfB00rUb2lBsNbM3bimmNXuryd651Wn9RRIPSiDH 298 | 9Xe/7e/C02l3X3PrYPbeC+IJnloq7S0o5gZcSS0EnSr9fN7hzjW8vAPvi2rm/ViqzaRrH1AI1rB4 299 | iGLgTRNBZ+/+5trNSjrHK1Czih5lWnm4Xq8xfKsS/Pky+HWb/jKBMqSoBmYwX0qrmZd97Du5EKO1 300 | R74vtaq4v2adpzCQ0e7poOWOC6x8ryq1+0nj+K6cEzKqt4nfqHYv5iGjvV6xO/avOv2bqVzGKjKY 301 | sQ0jsqPZlA39E5Bo7RPZ3jkk6vHs0A1bk7OT3YdoOOns3Ucnl1vhuPnQC3KgREgT/NFt2tdZXwdK 302 | pJNKwwlwEZAUiuwj5Mys6XZtvtll58y6cCQG50OVmGqVvBlBLDSgUPcZy8ISDzZH7lHqvl2pgj+L 303 | sjXYSzxSuLFzRnA69r3edUBLpFbbmYiT287u88DWJ+Vg98Tjw0pWHAmcbmogatFYRg1mZqpo9e39 304 | WtJlqLWGEINFhtDczfIM/8Iw8ntB7vgfQzgby7cmhtDgChEjIZoxFrMyVeYqZfNjWIgkzjvwm13X 305 | 2fXvu34eZu57nSPEZmUqnV5oCX6ENpilhYJxmaM4i9fxDP5BUcFpMYo7gA3VKcG/DPPfAmKJuLhK 306 | U4o0AawrBoEEMUiJYCo7pZ4pLMEBsTdlEAolVLf7a8yUS/8at3zSGj0Hregh8O66/fHXv889qmli 307 | bargAoFdgxGINJXBrZlnv541DOIxgzX7YB0nSKbuu0+BUxlFQ9fLHZtOhVqE3hYpwnmSG3D1rCZY 308 | sxvjroy9R+dQwi5FsESRSPgmR/3IKe2v0m+2Tdi2SFwJWGoQAQnCYDXGSeGZuw2nLMr9CaOh+wkl 309 | +wYdVFUousPhCskB2H25zcQ2SWFJS6yYhugKRPl43GTvvyYYBHE8sGgCFLbW1IY0lvRKqQ2o1amE 310 | pjaBo75+RkIaprQKI0FS5K/CuJNkXs7BX3B23Dg5ZoXeY11evs1Skvnx+IINlGgIX8TUV8kEiBFM 311 | tSW4iVptTG46xcJ5+E1MnlCUwsjfUJRC6bJWlXJZVd7dKQ6riyOi9IWknuxEzUEj13fjRuX41lyH 312 | Kjx12fimvts+eOj6p4/3EwjCi/kBpRXxHlUisF9Xy8iogx1AaqtiQ1QsToolLuFStbTxEvCevaMJ 313 | 1eH5+OqcdboPlbZ63jol9V1xw6P29UvfpuI9CcpN05gFji9MMmNkXRWaF5rd/BHRIk9Iu3YnvDkR 314 | 9S1+VHlVd5MttfM6VJ2Di9vx7p5f16/DvXHOkPZ+z55U9oqP8yFt3R26zqEftDtOpdsNRiuHtbjB 315 | x0lEb1UofxLWrn0y74+OdreOe83G0Xm7c/D06l2MeLu4WyC7o+Z1oT7ME9bCyfCjnO2vM59a3kFa 316 | pTOGtYlRLZymWP0Nk6aY0uvK2Z5v9pvltLbAtqhRG65YMrEZKCS/gG9MUEWJ3XFpRZM3G01J3ygw 317 | WhPCrNDgm8HzQs9ThVm9ftTfeemrrZfT5+fOJGK16mS/dte8eyhunZ6qzHEtQVodpvViXinq0GTK 318 | tk6Ia9G4sSwJOPIs1qpfGtfuFo6OCrtOoVjey0uMeLNdmlKBGBYthIaU4nc2Ok4nyOYVLAxkwaU5 319 | 7pTdJ78EiyzIVf/2S5lBadJ0xRBrQL/SIvxMbdZsZXyYQWwL/qSAz0kXfUoIzcMw7638tN86HVKg 320 | mByOmnkcL/dgFrNWfsZIBD4USilwpZOqztUnXX/oXMZCdX8v9Xu+F7jhP75nHce/F/9aAuV4OiaL 321 | GcCo1JVSixjcJQ4bR0w84W+XLlnGxNHsKIkF7SXjizeulW7w6jb8qOPcuuFDbo6QjDXqdApTAo4a 322 | xlCjmzPwcq3ImjgusGaDMOBIKQarKQHmWTP/ezoYjaEK4j4pHyMn/1tAnGU1fD4k2WI5hYXRrFWR 323 | kqbGuisoUmoErvCqBje1xVj9tyog0m2ezEBbRQER8X2jJFbYXhzP+qNfmfJJVop+kb+DkYdK2mwh 324 | RnGu3Fdn1x/6zv/91zwl399JIcQggpKMQViF2nRIaRDGiB8goTDzxlLUhMby2iw3qWLh1PxxCMzB 325 | Of4NenbFapGXMdnq3fHC6vbGUE6k+kgnSHS35oJFUz8blS8DVRMP7smzLVfOT/YKJ6eTS/vYeb4c 326 | 9fNHwDvgnkG/nTPfbUaxTt1zB4Nb/9n3+ki22OjC1wshoIlgfGH8NzFloomH4MTZaI3DcOLU/LYL 327 | q78f+pPmOIqcDV6orRhci6rmulysVGIdA42KPFYj52xa4bHpt5otRSy4Aou52QVWiVhnp7Ild07O 328 | niM9bF3st83ezdbRWfG5ZE7+f8hSWEKXmJvtmetoDf6xWUqagANVSsNgA2FccNhAl5MQ5p9Ouxyf 329 | tZtZDoLedHh6NXdUpOTgosAmgop/8aH6SbyYbTbt7fNWOkvMgCBPLhGhgxMZ9h8U6QFPUM+WX1ho 330 | OCsJQdJlSnv5SQjThTf7KhTZS9UTtLA3MiNhXxQ0ruU0M7635tLeFi/vWUIHfB6dPpkEzlUtYV1C 331 | pCvkHJ9k2lrau972jE9mBZiNTaWuWNizecyQgQNIyqmyzMebsK1cpIp1R8vh0YnHSqx8EB1enMm9 332 | anlQG5mzm/3Dsyo9OVHdnADUILzR14f2Zh6AKg0no8jtOmdur78y+qRgZuFkBSf8rQbej9CnNU/k 333 | 7cnkrFMdDU96XnR8uGNGxzs9Wikcne4OCqL2aHOAT+Ba/oxT8XXWUwUDm81GKyP4lIxlwDlnNMSx 334 | FoPw9Sl9zDb7zXG0LvBJlkmR2WJVF6Z9QPqgiQvNKmLuuCd1k6kWOB9vGGR8lSLBH4MgBUwC7G/O 335 | DGggX2/2rtvRbqF0TSsDuXuxVRi4hXD/pXU3qD2loU+/RTAwVwFSlkjNMFpoLMQMQVlcR28phLWP 336 | RMSrIPQ6bi83wYHFKQImlaZO4FSGwwlWJCxYhIyzufULI0m4TO/5+TMbYqk9FO1KZdcLOBJQYp1j 337 | 1J01swHMEeIwIsFMBJioSCr9N+n2IaIKVkks4RSF9mSqSiMFfwvxefBxCf8BhAW/ZpFLA0e8RF75 338 | Yvd/d4QukvGfVSJ0vIXA6gs6Kbw9mICjgDyZQcPvdnN3X8fsgFTEBFESmFnBuEEet8ia6KOE4hCX 339 | oy6CTK7EuEZ2AN8mKVfrq7AD4AzW4F0ZkYRDrxsfkalQ+ir4CDiGTBAhE8sgF7r+i1PqD/th/nQT 340 | Hut+6pQcN8Q4YP44FzH2x2TmOqpKIWIK44YGiE3Ap7BUyhmWKzhwu34jd/9NnbG4AE9aDWoIGGAB 341 | MojMCMVqPBn7b1C5PIaDmKZJgPpaFQTl71AQBLNXuGYYzV2JdOEo3Px5tg8zfP3IFBhXWdKK0NUP 342 | rwzGCgEVA6P7IGck+2JzUMvwZrfWuj2pjW9PC7Wja7f63D7cPYi6QaPmi4NWLT80VR+Oo85Wsnjg 343 | vMLgD2uPbjr7fb/r7Pv+68TZ2L8+fNMa/HvxsgRP1/qjphu9CRBuwN+dbzplP6li7n/biqWZxQYT 344 | i5YyBSsHIjVkGxu2Jlb3QrPfrIZVK5bipateKjL4X1qxFD07u0xU8PdoIIKxz2Y27VcqtzdOpbxX 345 | v/3UZ/usq8o3cSNb2k3GOWyYDKVyuMKaMsu7ufD4d7M4sxzR/+4W47B7FmNES6Z8aYVaAocYIbHK 346 | Oyfo7C//3ouPf9fTMxc2nU3n6Ng5+h87OwWUkpT6k/P82Vl0I6HdZTCtxHINZJqThQVil6s0Ljyd 347 | BrJ9p5OolxVEySOTmEMtcnl5lJ/JRebBANcOWrx0B/T5unTsPnQau2FwOnoKHg814a5/f/JStoMF 348 | DGLtKo3X1Sq7rnb35vHC/4aVYpWmqDQKERTWvnn/xf/2Wo3J5rR+Wzqqq7MX+0JOH0eVI9MsFQ/p 349 | QblevuGte088teiPcVCD7I0ZnYVvcdCv1rSO4rHTO9t5SAxCbwi/OWUQR1C2Ls3jhWaXuSGZPf/Y 350 | veFM40fDalFcsgQEdt6yk98bKzv8MRo3m1PjjoZjH/2h4fBtQP/5/wBHFJcFnU0BAA== 351 | headers: 352 | cache-control: ['private, no-store, must-revalidate'] 353 | connection: [keep-alive] 354 | content-encoding: [gzip] 355 | content-type: [application/json] 356 | date: ['Tue, 20 Jan 2015 16:36:02 GMT'] 357 | expires: ['0'] 358 | pragma: [no-cache] 359 | strict-transport-security: [max-age=631138519] 360 | transfer-encoding: [chunked] 361 | status: {code: 200, message: OK} 362 | version: 1 363 | -------------------------------------------------------------------------------- /fixtures/cassettes/unfollow_notifications.yml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Accept-Language: [en;q=1] 8 | Connection: [keep-alive] 9 | Content-Length: ['0'] 10 | Content-Type: [application/x-www-form-urlencoded; charset=utf-8] 11 | Host: [api.vineapp.com] 12 | Proxy-Connection: [keep-alive] 13 | User-Agent: [iphone/172 (iPad; iOS 7.0.4; Scale/2.00)] 14 | X-Vine-Client: [ios/2.5.1] 15 | vine-session-id: [!!python/unicode 1169361810303164416-1ba6acf7-d897-45ae-8640-cd9484acf5b7] 16 | method: DELETE 17 | uri: https://api.vineapp.com:443/users/948731399408640000/followers/notifications 18 | response: 19 | body: {string: !!python/unicode '{"code": "", "data": {}, "success": true, "error": 20 | ""}'} 21 | headers: 22 | cache-control: ['private, no-store, must-revalidate'] 23 | connection: [keep-alive] 24 | content-length: ['54'] 25 | content-type: [application/json] 26 | date: ['Tue, 10 Feb 2015 23:13:31 GMT'] 27 | expires: ['0'] 28 | pragma: [no-cache] 29 | strict-transport-security: [max-age=631138519] 30 | status: {code: 200, message: OK} 31 | - request: 32 | body: null 33 | headers: 34 | Accept: ['*/*'] 35 | Accept-Encoding: ['gzip, deflate'] 36 | Accept-Language: [en;q=1] 37 | Connection: [keep-alive] 38 | Content-Type: [application/x-www-form-urlencoded; charset=utf-8] 39 | Host: [api.vineapp.com] 40 | Proxy-Connection: [keep-alive] 41 | User-Agent: [iphone/172 (iPad; iOS 7.0.4; Scale/2.00)] 42 | X-Vine-Client: [ios/2.5.1] 43 | vine-session-id: [!!python/unicode 1169361810303164416-1ba6acf7-d897-45ae-8640-cd9484acf5b7] 44 | method: GET 45 | uri: https://api.vineapp.com:443/users/profiles/948731399408640000 46 | response: 47 | body: {string: !!python/unicode '{"code": "", "data": {"followerCount": 195, "userId": 48 | 948731399408640000, "private": 0, "likeCount": 715, "postCount": 147, "explicitContent": 49 | 0, "blocked": 0, "verified": 0, "loopCount": 7671, "avatarUrl": "http://v.cdn.vine.co/r/avatars/98E427C03A1035848145776820224_1c1a5595066.4.7_AJeYR3l8u2pgTdFOq.rn9k3ziYHqfhul96J76ILSrFSVdIbDEbv6EeizF.XEklIf.jpg?versionId=11kNTERjSU_kj4CAcpQfGQoZrSqx3LSp", 50 | "authoredPostCount": 115, "description": "Soy lo m\u00e1s viejo que he sido 51 | y lo m\u00e1s joven que jam\u00e1s volver\u00e9 a ser", "location": "CDMX", 52 | "username": "Davoclavo", "vanityUrls": ["davoclavo"], "following": 1, "blocking": 53 | 0, "shareUrl": "https://vine.co/davoclavo", "profileBackground": "0x5082e5", 54 | "notifyPosts": 0, "followingCount": 288, "repostsEnabled": 1}, "success": 55 | true, "error": ""}'} 56 | headers: 57 | cache-control: ['private, no-store, must-revalidate'] 58 | connection: [keep-alive] 59 | content-length: ['802'] 60 | content-type: [application/json] 61 | date: ['Tue, 10 Feb 2015 23:13:31 GMT'] 62 | expires: ['0'] 63 | pragma: [no-cache] 64 | strict-transport-security: [max-age=631138519] 65 | status: {code: 200, message: OK} 66 | version: 1 67 | -------------------------------------------------------------------------------- /fixtures/cassettes/vm_friends_inbox.yml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Accept-Language: ['en;q=1, fr;q=0.9, de;q=0.8, ja;q=0.7, nl;q=0.6, it;q=0.5'] 8 | Connection: [keep-alive] 9 | User-Agent: [iphone/1.3.1 (iPhone; iOS 6.1.4; Scale/2.00)] 10 | X-Vine-Client: [ios/2.5.1] 11 | vine-session-id: [!!python/unicode 967576278242844672-9c2f6444-66b8-4ec5-a03b-86afec71cae7] 12 | method: GET 13 | uri: https://api.vineapp.com:443/users//conversations 14 | response: 15 | body: {string: !!python/unicode '{"code": 900, "data": "", "success": false, "error": 16 | "That record does not exist."}'} 17 | headers: 18 | cache-control: ['private, no-store, must-revalidate'] 19 | connection: [keep-alive] 20 | content-length: ['83'] 21 | content-type: [application/json] 22 | date: ['Tue, 20 Jan 2015 16:36:01 GMT'] 23 | expires: ['0'] 24 | pragma: [no-cache] 25 | strict-transport-security: [max-age=631138519] 26 | status: {code: 404, message: NOT FOUND} 27 | - request: 28 | body: null 29 | headers: 30 | Accept: ['*/*'] 31 | Accept-Encoding: ['gzip, deflate'] 32 | Accept-Language: [en;q=1] 33 | Connection: [keep-alive] 34 | Content-Type: [application/x-www-form-urlencoded; charset=utf-8] 35 | Host: [api.vineapp.com] 36 | Proxy-Connection: [keep-alive] 37 | User-Agent: [iphone/172 (iPad; iOS 7.0.4; Scale/2.00)] 38 | X-Vine-Client: [ios/2.5.1] 39 | vine-session-id: [!!python/unicode 1169361810303164416-7508513e-8757-485a-9fba-9870d111e3e5] 40 | method: GET 41 | uri: https://api.vineapp.com:443/users/1169361810303164416/conversations 42 | response: 43 | body: {string: !!python/unicode '{"code": "", "data": {"count": 0, "anchorStr": 44 | "0", "records": [], "previousPage": null, "backAnchor": "", "anchor": 0, "nextPage": 45 | null, "size": 20}, "success": true, "error": ""}'} 46 | headers: 47 | cache-control: ['private, no-store, must-revalidate'] 48 | connection: [keep-alive] 49 | content-length: ['180'] 50 | content-type: [application/json] 51 | date: ['Tue, 20 Jan 2015 16:47:04 GMT'] 52 | expires: ['0'] 53 | pragma: [no-cache] 54 | strict-transport-security: [max-age=631138519] 55 | status: {code: 200, message: OK} 56 | version: 1 57 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | version='0.6.3', 5 | name='vinepy', 6 | description='Python wrapper for the Vine Private API', 7 | license='MIT', 8 | author='David Gomez Urquiza', 9 | author_email='david.gurquiza@gmail.com', 10 | install_requires=['requests'], 11 | url='https://github.com/davoclavo/vinepy', 12 | keywords=['vine', 'library', 'api', 'wrapper'], 13 | packages=find_packages(), 14 | ) 15 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davoclavo/vinepy/e5f9ec39d1c5801244e04458be47e61d967e3060/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_vinepy.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | import vcr 5 | import vinepy 6 | 7 | from nose2.compat import unittest 8 | 9 | 10 | my_vcr = vcr.VCR( 11 | cassette_library_dir='fixtures/cassettes', 12 | record_mode='once', 13 | match_on=['uri', 'method'], 14 | # record_mode = 'all' # Re-record cassettes 15 | ) 16 | 17 | 18 | class TestAPI(unittest.TestCase): 19 | 20 | @my_vcr.use_cassette('login.yml') 21 | @classmethod 22 | def setUp(cls): 23 | cls.vine_name = 'Bob Testy' 24 | cls.vine_email = 'bobtesty@suremail.info' 25 | cls.vine_password = 'password' 26 | cls.api = vinepy.API( 27 | username=cls.vine_email, password=cls.vine_password) 28 | 29 | @my_vcr.use_cassette('signup.yml') 30 | def test_signup(self): 31 | api = vinepy.API().signup( 32 | username=self.vine_name, email=self.vine_email, password=self.vine_password) 33 | self.assertEqual(api.username, self.vine_name) 34 | 35 | @my_vcr.use_cassette('tag_timeline.yml') 36 | def test_get_tag_timeline(self): 37 | timeline = self.api.get_tag_timeline(tag_name='LNV') 38 | assert(timeline._attrs) 39 | 40 | @my_vcr.use_cassette('vm_friends_inbox.yml') 41 | def test_get_friends_inbox(self): 42 | conversations = self.api.get_conversations( 43 | user_id=self.api.user.userId) 44 | assert(conversations._attrs) 45 | 46 | @my_vcr.use_cassette('get_post.yml') 47 | def test_get_post(self): 48 | post_id = 1167619641938518016 49 | # Retrieves PostCollection 50 | post = self.api.get_post(post_id=post_id)[0] 51 | self.assertEqual(post.id, post_id) 52 | self.assertEqual( 53 | post.name, 'In-N-Out vs. Shake Shack: The Ultimate Battle') 54 | 55 | def test_custom_device_token(self): 56 | with my_vcr.use_cassette('login-custom-device-token.yml') as cassette: 57 | custom_device_token = 'a3352a79c3e29283a03a2e6eb89587648f5b2a291c709708816ec768d058ea45' 58 | api = vinepy.API( 59 | username=self.vine_email, password=self.vine_password, device_token=custom_device_token) 60 | self.assertIn(custom_device_token, cassette.requests[0].body) 61 | self.assertEqual(api.username, self.vine_email) 62 | 63 | def test_user_notifications(self): 64 | api = vinepy.API(username=self.vine_email, password=self.vine_password) 65 | user_id = 948731399408640000 66 | 67 | with my_vcr.use_cassette('unfollow_notifications.yml') as cassette: 68 | api.unfollow_notifications(user_id=user_id) 69 | user = api.get_user(user_id=user_id) 70 | self.assertTrue(user.is_following()) 71 | self.assertFalse(user.is_notifying()) 72 | 73 | with my_vcr.use_cassette('follow_notifications.yml') as cassette: 74 | api.follow_notifications(user_id=user_id) 75 | user = api.get_user(user_id=user_id) 76 | self.assertTrue(user.is_following()) 77 | self.assertTrue(user.is_notifying()) 78 | 79 | 80 | class TestModel(unittest.TestCase): 81 | # Model method tests 82 | 83 | def test_model_from_json(self): 84 | mock_json = {'id': 1, 85 | 'from_json': 'something' 86 | } 87 | model = vinepy.Model.from_json(mock_json) 88 | self.assertEqual(model['id'], mock_json['id']) 89 | # Does not replace an existing key 90 | self.assertNotEqual(model.from_json, mock_json['from_json']) 91 | 92 | # classname + 'Id' replaces 'id' key 93 | mock_json['modelId'] = 2 94 | model = vinepy.Model.from_json(mock_json) 95 | self.assertEqual(model.id, mock_json['modelId']) 96 | 97 | def test_model_from_id(self): 98 | _id = 123 99 | model = vinepy.Model.from_id(_id) 100 | self.assertEqual(_id, model.id) 101 | 102 | def test_model_repr(self): 103 | # No name attribute sets it to 104 | _id = 123 105 | model = vinepy.Model.from_json({'id': _id}) 106 | self.assertEqual(repr(model), "" % (_id, ''.encode('utf8'))) 107 | 108 | # Unicode name (emojis) 109 | _description = u'Lmaoo\U0001f602' 110 | model = vinepy.Post.from_json({'id': _id, 'description': _description}) 111 | self.assertEqual(repr(model), "" % (_id, _description.encode('utf8'))) 112 | 113 | 114 | class TestDecorator(unittest.TestCase): 115 | # Decorator tests 116 | 117 | def test_vine_json(self): 118 | pass 119 | 120 | def test_ensure_ownership(self): 121 | pass 122 | 123 | def test_chained(self): 124 | pass 125 | 126 | def test_inject_post(self): 127 | pass 128 | 129 | 130 | class TestUtils(unittest.TestCase): 131 | short_id = 'OjunvOxTpZ5' 132 | long_id = 1167619641938518016 133 | 134 | def test_post_long_id(self): 135 | long_id = vinepy.post_long_id(self.short_id) 136 | self.assertEqual(self.long_id, long_id) 137 | 138 | def test_post_short_id(self): 139 | short_id = vinepy.post_short_id(self.long_id) 140 | self.assertEqual(self.short_id, short_id) 141 | -------------------------------------------------------------------------------- /vinepy/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import * 2 | -------------------------------------------------------------------------------- /vinepy/api.py: -------------------------------------------------------------------------------- 1 | # add check if requests is installed 2 | import requests 3 | 4 | from .models import * 5 | from .endpoints import * 6 | from .errors import * 7 | 8 | from functools import partial 9 | from json import dumps 10 | import os 11 | import binascii 12 | 13 | 14 | class API(object): 15 | 16 | def __init__(self, username=None, password=None, device_token=None, DEBUG=False): 17 | self.username = username 18 | self.password = password 19 | self._session_id = None 20 | self.DEBUG = DEBUG 21 | self.device_token = device_token or binascii.b2a_hex(os.urandom(32)) 22 | 23 | self._make_dynamic_methods() 24 | 25 | if self.username and self.password: 26 | self.user = self.login( 27 | username=username, password=password, device_token=self.device_token) 28 | 29 | def _make_dynamic_methods(self): 30 | for endpoint in list(ENDPOINTS.keys()): 31 | def _inner(endpoint, *args, **kwargs): 32 | return self.api_call(endpoint, *args, **kwargs) 33 | _inner.__name__ = endpoint 34 | setattr(self, _inner.__name__, partial(_inner, endpoint)) 35 | 36 | def build_request_url(self, protocol, host, endpoint): 37 | url = '%s://%s/%s' % (protocol, host, endpoint) 38 | # encode url params 39 | return url 40 | 41 | def api_call(self, endpoint, *args, **kwargs): 42 | metadata = ENDPOINTS[endpoint] 43 | 44 | params = self.check_params(metadata, kwargs) 45 | 46 | response = self.do_request(metadata, params) 47 | 48 | if metadata['model'] is None: 49 | return response 50 | else: 51 | model = metadata['model'].from_json(response) 52 | model.connect_api(self) 53 | return model 54 | 55 | def check_params(self, metadata, kwargs): 56 | missing_params = [] 57 | url_params = [] 58 | 59 | # page, size and anchor are data_params for get requests 60 | 61 | for param in metadata['url_params']: 62 | p = kwargs.get(param) 63 | if p is None: 64 | missing_params.append(param) 65 | else: 66 | url_params.append(p) 67 | del kwargs[param] 68 | if missing_params: 69 | raise ParameterError( 70 | 'Missing URL parameters: [%s]' % ', '.join(missing_params)) 71 | 72 | # url_params shouldnt have default params, I guess 73 | data_params = kwargs 74 | if metadata.get('default_params', []) != []: 75 | default_params = dict(metadata['default_params']) 76 | data_params = dict(list(default_params.items()) + list(kwargs.items())) 77 | 78 | missing_params = [] 79 | for param in metadata['required_params']: 80 | p = data_params.get(param) 81 | if p is None: 82 | missing_params.append(param) 83 | if missing_params: 84 | raise ParameterError( 85 | 'Missing required parameters: [%s]' % ', '.join(missing_params)) 86 | 87 | # Check for unsupported params? 88 | 89 | return {'url': url_params, 'data': data_params} 90 | 91 | def do_request(self, metadata, params): 92 | headers = HEADERS.copy() 93 | if params['url'] != []: 94 | endpoint = metadata['endpoint'] % tuple(params['url']) 95 | else: 96 | endpoint = metadata['endpoint'] 97 | 98 | host = API_HOST 99 | # Upload methods, change host to specific host 100 | if metadata.get('host'): 101 | host = metadata['host'] 102 | 103 | url = self.build_request_url(PROTOCOL, host, endpoint) 104 | 105 | built_params = built_data = None 106 | built_data = data = params['data'] 107 | 108 | if metadata['request_type'] == 'get': 109 | built_params = data 110 | elif metadata['request_type'] == 'post': 111 | if metadata.get('json'): 112 | built_data = dumps(data) 113 | headers['Content-Type'] = 'application/json; charset=utf-8' 114 | elif data.get('filename'): 115 | if data['filename'].split('.')[-1] == 'mp4': 116 | headers['Content-Type'] = 'video/mp4' 117 | else: 118 | headers['Content-Type'] = 'image/jpeg' 119 | built_data = open(data['filename'], 'rb') 120 | 121 | if self._session_id: 122 | headers['vine-session-id'] = self._session_id 123 | 124 | if(self.DEBUG): 125 | # pip install mitmproxy 126 | # mitmproxy 127 | http_proxy = "http://localhost:8080" 128 | https_proxy = "http://localhost:8080" 129 | 130 | proxies = { 131 | "http": http_proxy, 132 | "https": https_proxy, 133 | } 134 | 135 | # cafile='~/.mitmproxy/mitmproxy-ca-cert.pem' 136 | cafile = False 137 | response = requests.request( 138 | metadata['request_type'], url, params=built_params, data=built_data, headers=headers, verify=cafile, proxies=proxies) 139 | print('REQUESTED: %s [%s]' % (url, response.status_code)) 140 | else: 141 | response = requests.request( 142 | metadata['request_type'], url, params=built_params, data=built_data, headers=headers) 143 | 144 | if response.headers.get('X-Upload-Key'): 145 | return response.headers['X-Upload-Key'] 146 | 147 | if response.status_code in [200, 400, 404, 420]: 148 | try: 149 | json = response.json() 150 | except: 151 | raise VineError( 152 | 1000, 'Vine replied with non-json content:\n' + response.text) 153 | 154 | if json['success'] is not True: 155 | raise VineError(json['code'], json['error']) 156 | return json['data'] 157 | else: 158 | raise VineError(response.status_code, response.text) 159 | 160 | def authenticate(self, user): 161 | self.user = user 162 | self._session_id = user.key 163 | self._user_id = user.id 164 | -------------------------------------------------------------------------------- /vinepy/endpoints.py: -------------------------------------------------------------------------------- 1 | from .models import * 2 | 3 | PROTOCOL = 'https' 4 | API_HOST = 'api.vineapp.com' 5 | MEDIA_HOST = 'media.vineapp.com' 6 | 7 | HEADERS = { 8 | 'Host': 'api.vineapp.com', 9 | 'Proxy-Connection': 'keep-alive', 10 | 'Accept': '*/*', 11 | 'X-Vine-Client': 'ios/2.5.1', 12 | 'Accept-Encoding': 'gzip, deflate', 13 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 14 | 'Accept-Language': 'en;q=1', 15 | 'Connection': 'keep-alive', 16 | 'User-Agent': 'iphone/172 (iPad; iOS 7.0.4; Scale/2.00)' 17 | } 18 | 19 | 20 | OPTIONAL_PARAMS = ['size', 'page', 'anchor'] 21 | 22 | ENDPOINTS = { 23 | 24 | # Auth 25 | 'login': { 26 | 'endpoint': 'users/authenticate', 27 | 'request_type': 'post', 28 | 'url_params': [], 29 | 'required_params': ['username', 'password'], 30 | 'optional_params': ['deviceToken'], 31 | 'default_params': [], 32 | 'model': User 33 | }, 34 | 'logout': { 35 | 'endpoint': 'users/authenticate', 36 | 'request_type': 'delete', 37 | 'url_params': [], 38 | 'required_params': [], 39 | 'optional_params': [], 40 | 'model': None 41 | }, 42 | 'signup': { 43 | 'endpoint': 'users', 44 | 'request_type': 'post', 45 | 'url_params': [], 46 | 'required_params': ['email', 'password', 'username'], 47 | 'optional_params': [], 48 | 'default_params': [('authenticate', 1)], 49 | 'model': User 50 | }, 51 | 52 | # Profile 53 | 'get_me': { 54 | 'endpoint': 'users/me', 55 | 'request_type': 'get', 56 | 'url_params': [], 57 | 'required_params': [], 58 | 'optional_params': [], 59 | 'model': User 60 | }, 61 | 'get_user': { 62 | 'endpoint': 'users/profiles/%s', 63 | 'request_type': 'get', 64 | 'url_params': ['user_id'], 65 | 'required_params': [], 66 | 'optional_params': [], 67 | 'model': User 68 | }, 69 | 'update_profile': { 70 | 'endpoint': 'users/%s', 71 | 'request_type': 'put', 72 | 'url_params': ['user_id'], 73 | 'required_params': [], 74 | 'optional_params': ['username', 'description', 'location', 'locale', 'email', 'private', 'phoneNumber', 'avatarUrl', 'profileBackground', 'acceptsOutOfNetworkConversations'], 75 | 'model': None 76 | }, 77 | 'set_explicit': { 78 | 'endpoint': 'users/%s/explicit', 79 | 'request_type': 'post', 80 | 'url_params': ['user_id'], 81 | 'required_params': [], 82 | 'optional_params': [], 83 | 'model': None 84 | }, 85 | 'unset_explicit': { 86 | 'endpoint': 'users/%s/explicit', 87 | 'request_type': 'delete', 88 | 'url_params': ['user_id'], 89 | 'required_params': [], 90 | 'optional_params': [], 91 | 'model': None 92 | }, 93 | 94 | # User actions 95 | 'follow': { 96 | 'endpoint': 'users/%s/followers', 97 | 'request_type': 'post', 98 | 'url_params': ['user_id'], 99 | 'required_params': [], 100 | 'optional_params': ['notify'], # notify=1 to follow notifications as well 101 | 'model': None 102 | }, 103 | 'follow_notifications': { 104 | 'endpoint': 'users/%s/followers/notifications', 105 | 'request_type': 'post', 106 | 'url_params': ['user_id'], 107 | 'required_params': ['notify'], 108 | 'optional_params': [], 109 | 'default_params': [('notify', 1)], 110 | 'model': None 111 | }, 112 | 'unfollow': { 113 | 'endpoint': 'users/%s/followers', 114 | 'request_type': 'delete', 115 | 'url_params': ['user_id'], 116 | 'required_params': [], 117 | 'optional_params': [], 118 | 'model': None 119 | }, 120 | 'unfollow_notifications': { 121 | 'endpoint': 'users/%s/followers/notifications', 122 | 'request_type': 'delete', 123 | 'url_params': ['user_id'], 124 | 'required_params': [], 125 | 'optional_params': [], 126 | 'model': None 127 | }, 128 | 'block': { 129 | 'endpoint': 'users/%s/blocked/%s', 130 | 'request_type': 'post', 131 | 'url_params': ['from_user_id', 'to_user_id'], 132 | 'required_params': [], 133 | 'optional_params': [], 134 | 'model': None 135 | }, 136 | 'unblock': { 137 | 'endpoint': 'users/%s/blocked/%s', 138 | 'request_type': 'delete', 139 | 'url_params': ['from_user_id', 'to_user_id'], 140 | 'required_params': [], 141 | 'optional_params': [], 142 | 'model': None 143 | }, 144 | 'get_pending_notifications_count': { 145 | 'endpoint': 'users/%s/pendingNotificationsCount', 146 | 'request_type': 'get', 147 | 'url_params': ['user_id'], 148 | 'required_params': [], 149 | 'optional_params': [], 150 | 'model': None 151 | }, 152 | 'get_notifications': { 153 | 'endpoint': 'users/%s/notifications', 154 | 'request_type': 'get', 155 | 'url_params': ['user_id'], 156 | 'required_params': [], 157 | 'optional_params': [], 158 | 'model': NotificationCollection 159 | }, 160 | 161 | # User lists 162 | 'get_followers': { 163 | 'endpoint': 'users/%s/followers', 164 | 'request_type': 'get', 165 | 'url_params': ['user_id'], 166 | 'required_params': [], 167 | 'optional_params': [], 168 | 'model': UserCollection 169 | }, 170 | 'get_following': { 171 | 'endpoint': 'users/%s/following', 172 | 'request_type': 'get', 173 | 'url_params': ['user_id'], 174 | 'required_params': [], 175 | 'optional_params': [], 176 | 'model': UserCollection 177 | }, 178 | 179 | 'get_conversations': { 180 | 'endpoint': 'users/%s/conversations', 181 | 'request_type': 'get', 182 | 'url_params': ['user_id'], 183 | 'required_params': [], 184 | 'optional_params': ['inbox'], 185 | 'model': ConversationCollection 186 | }, 187 | 188 | 189 | 'start_conversation': { 190 | 'endpoint': 'conversations', 191 | 'request_type': 'post', 192 | 'json': True, 193 | 'url_params': [], 194 | 'required_params': ['created', 'locale', 'message', 'to'], 195 | 'optional_params': [], 196 | 'model': MessageCollection 197 | }, 198 | 199 | 'converse': { 200 | 'endpoint': 'conversations/%s', 201 | 'request_type': 'post', 202 | 'json': True, 203 | 'url_params': ['conversation_id'], 204 | 'required_params': ['created', 'locale', 'message'], 205 | 'optional_params': [], 206 | 'model': MessageCollection 207 | }, 208 | 209 | # Posts actions 210 | 'loops': { 211 | 'endpoint': 'loops', 212 | 'request_type': 'post', 213 | 'json': True, 214 | 'url_params': [], 215 | 'required_params': ['loops'], 216 | 'optional_params': [], 217 | 'default_params': [], 218 | 'model': None 219 | }, 220 | 'like': { 221 | 'endpoint': 'posts/%s/likes', 222 | 'request_type': 'post', 223 | 'url_params': ['post_id'], 224 | 'required_params': [], 225 | 'optional_params': [], 226 | 'model': Like 227 | }, 228 | 'unlike': { 229 | 'endpoint': 'posts/%s/likes', 230 | 'request_type': 'delete', 231 | 'url_params': ['post_id'], 232 | 'required_params': [], 233 | 'optional_params': [], 234 | 'model': None 235 | }, 236 | 'comment': { 237 | 'endpoint': 'posts/%s/comments', 238 | 'request_type': 'post', 239 | 'json': True, 240 | 'url_params': ['post_id'], 241 | 'required_params': ['comment', 'entities'], 242 | 'optional_params': [], 243 | 'model': Comment 244 | }, 245 | 'uncomment': { 246 | 'endpoint': 'posts/%s/comments/%s', 247 | 'request_type': 'delete', 248 | 'url_params': ['post_id', 'comment_id'], 249 | 'required_params': [], 250 | 'optional_params': [], 251 | 'model': None 252 | }, 253 | 'revine': { 254 | 'endpoint': 'posts/%s/repost', 255 | 'request_type': 'post', 256 | 'url_params': ['post_id'], 257 | 'required_params': [], 258 | 'optional_params': [], 259 | 'model': Repost 260 | }, 261 | 'unrevine': { 262 | 'endpoint': 'posts/%s/repost/%s', 263 | 'request_type': 'delete', 264 | 'url_params': ['post_id', 'revine_id'], 265 | 'required_params': [], 266 | 'optional_params': [], 267 | 'model': None 268 | }, 269 | 'report': { 270 | 'endpoint': 'posts/%s/complaints', 271 | 'request_type': 'post', 272 | 'url_params': [], 273 | 'required_params': [], 274 | 'optional_params': [], 275 | 'model': None 276 | }, 277 | 'post': { 278 | 'endpoint': 'posts', 279 | 'request_type': 'post', 280 | 'json': True, 281 | 'url_params': [], 282 | 'required_params': ['videoUrl', 'thumbnailUrl', 'description', 'entities'], 283 | 'optional_params': ['forsquareVenueId', 'venueName'], 284 | 'default_params': [('channelId', '0')], 285 | 'model': Post 286 | }, 287 | 'delete_post': { 288 | 'endpoint': 'posts/%s', 289 | 'request_type': 'delete', 290 | 'url_params': ['post_id'], 291 | 'required_params': [], 292 | 'optional_params': [], 293 | 'model': None 294 | }, 295 | 'get_post_likes': { 296 | 'endpoint': 'posts/%s/likes', 297 | 'request_type': 'get', 298 | 'url_params': ['post_id'], 299 | 'required_params': [], 300 | 'optional_params': OPTIONAL_PARAMS, 301 | 'model': LikeCollection 302 | }, 303 | 'get_post_comments': { 304 | 'endpoint': 'posts/%s/comments', 305 | 'request_type': 'get', 306 | 'url_params': ['post_id'], 307 | 'required_params': [], 308 | 'optional_params': [], 309 | 'model': CommentCollection 310 | }, 311 | 'get_post_reposts': { 312 | 'endpoint': 'posts/%s/reposts', 313 | 'request_type': 'get', 314 | 'url_params': ['post_id'], 315 | 'required_params': [], 316 | 'optional_params': [], 317 | 'model': RepostCollection 318 | }, 319 | 320 | 321 | # Timelines 322 | 'get_post': { 323 | 'endpoint': 'timelines/posts/%s', 324 | 'request_type': 'get', 325 | 'url_params': ['post_id'], 326 | 'required_params': [], 327 | 'optional_params': OPTIONAL_PARAMS, 328 | 'model': PostCollection 329 | }, 330 | 'get_user_timeline': { 331 | 'endpoint': 'timelines/users/%s', 332 | 'request_type': 'get', 333 | 'url_params': ['user_id'], 334 | 'required_params': [], 335 | 'optional_params': OPTIONAL_PARAMS, 336 | 'model': PostCollection 337 | }, 338 | 'get_user_likes': { 339 | 'endpoint': 'timelines/users/%s/likes', 340 | 'request_type': 'get', 341 | 'url_params': ['user_id'], 342 | 'required_params': [], 343 | 'optional_params': OPTIONAL_PARAMS, 344 | 'model': PostCollection 345 | }, 346 | 'get_tag_timeline': { 347 | 'endpoint': 'timelines/tags/%s', 348 | 'request_type': 'get', 349 | 'url_params': ['tag_name'], 350 | 'required_params': [], 351 | 'optional_params': OPTIONAL_PARAMS, 352 | 'model': PostCollection 353 | }, 354 | 'get_graph_timeline': { 355 | 'endpoint': 'timelines/graph', 356 | 'request_type': 'get', 357 | 'url_params': [], 358 | 'required_params': [], 359 | 'optional_params': OPTIONAL_PARAMS, 360 | 'model': PostCollection 361 | }, 362 | 'get_popular_timeline': { 363 | 'endpoint': 'timelines/popular', 364 | 'request_type': 'get', 365 | 'url_params': [], 366 | 'required_params': [], 367 | 'optional_params': OPTIONAL_PARAMS, 368 | 'model': PostCollection 369 | }, 370 | 'get_promoted_timeline': { 371 | 'endpoint': 'timelines/promoted', 372 | 'request_type': 'get', 373 | 'url_params': [], 374 | 'required_params': [], 375 | 'optional_params': OPTIONAL_PARAMS, 376 | 'model': PostCollection 377 | }, 378 | 'get_channel_popular_timeline': { 379 | 'endpoint': 'timelines/channels/%s/popular', 380 | 'request_type': 'get', 381 | 'url_params': ['channel_id'], 382 | 'required_params': [], 383 | 'optional_params': OPTIONAL_PARAMS, 384 | 'model': PostCollection 385 | }, 386 | 'get_channel_recent_timeline': { 387 | 'endpoint': 'timelines/channels/%s/recent', 388 | 'request_type': 'get', 389 | 'url_params': ['channel_id'], 390 | 'required_params': [], 391 | 'optional_params': OPTIONAL_PARAMS, 392 | 'model': PostCollection 393 | }, 394 | 'get_venue_timeline': { 395 | 'endpoint': 'timelines/venues/%s', 396 | 'request_type': 'get', 397 | 'url_params': ['venue_id'], 398 | 'required_params': [], 399 | 'optional_params': OPTIONAL_PARAMS, 400 | 'model': PostCollection 401 | }, 402 | 403 | # Tags 404 | 'get_trending_tags': { 405 | 'endpoint': 'tags/trending', 406 | 'request_type': 'get', 407 | 'url_params': [], 408 | 'required_params': [], 409 | 'optional_params': [], 410 | 'model': TagCollection 411 | }, 412 | 413 | # Channels 414 | 'get_featured_channels': { 415 | 'endpoint': 'channels/featured', 416 | 'request_type': 'get', 417 | 'url_params': [], 418 | 'required_params': [], 419 | 'optional_params': [], 420 | 'model': ChannelCollection 421 | }, 422 | 423 | # Search 424 | 'search_tags': { 425 | 'endpoint': 'tags/search/%s', 426 | 'request_type': 'get', 427 | 'url_params': ['tag_name'], 428 | 'required_params': [], 429 | 'optional_params': OPTIONAL_PARAMS, 430 | 'model': TagCollection 431 | }, 432 | 'search_users': { 433 | 'endpoint': 'users/search/%s', 434 | 'request_type': 'get', 435 | 'url_params': ['user_name'], 436 | 'required_params': [], 437 | 'optional_params': OPTIONAL_PARAMS, 438 | 'model': UserCollection 439 | }, 440 | 441 | # Upload Media 442 | 'upload_avatar': { 443 | 'host': MEDIA_HOST, 444 | 'endpoint': 'upload/avatars/1.3.1.jpg', 445 | 'request_type': 'put', 446 | 'url_params': [], 447 | 'required_params': ['filename'], 448 | 'optional_params': [], 449 | 'model': None 450 | }, 451 | 'upload_thumb': { 452 | 'host': MEDIA_HOST, 453 | 'endpoint': 'upload/thumbs/2.5.1.15482401929932289311.mp4.jpg?private=1', 454 | 'request_type': 'put', 455 | 'url_params': [], 456 | 'required_params': ['filename'], 457 | 'optional_params': [], 458 | 'model': None 459 | }, 460 | 'upload_video': { 461 | 'host': MEDIA_HOST, 462 | 'endpoint': 'upload/videos/2.5.1.15482401929932289311.mp4?private=1', 463 | 'request_type': 'put', 464 | 'url_params': [], 465 | 'required_params': ['filename'], 466 | 'optional_params': [], 467 | 'model': None 468 | }, 469 | } 470 | -------------------------------------------------------------------------------- /vinepy/errors.py: -------------------------------------------------------------------------------- 1 | class VineError(Exception): 2 | 3 | def __init__(self, code, error): 4 | self.code = code 5 | self.error = error 6 | 7 | def __str__(self): 8 | return '#%i: %s' % (self.code, self.error) 9 | 10 | 11 | class ParameterError(Exception): 12 | 13 | def __init__(self, description): 14 | self.description = description 15 | 16 | def __str__(self): 17 | return self.description 18 | -------------------------------------------------------------------------------- /vinepy/models.py: -------------------------------------------------------------------------------- 1 | import json 2 | from .errors import * 3 | from .utils import * 4 | 5 | def parse_vine_json(fn): 6 | """Vine json pre-parser""" 7 | def _decorator(self, *args, **kwargs): 8 | self = fn(self, *args, **kwargs) 9 | 10 | # Vine adds classname+'Id' as an id to the object 11 | classname = self.__class__.__name__.lower() 12 | vineId = classname + 'Id' 13 | 14 | for key in list(self.keys()): 15 | value = self[key] 16 | 17 | if key == vineId: 18 | self['id'] = value 19 | elif key == 'userId': 20 | self['user'] = User.from_id(value) 21 | elif key == 'postId': 22 | self['post'] = Post.from_id(value) 23 | elif key == 'created': 24 | self[key] = strptime(value) 25 | elif key == 'comments': 26 | self[key] = CommentCollection.from_json(value) 27 | elif key == 'likes': 28 | self[key] = LikeCollection.from_json(value) 29 | elif key == 'reposts': 30 | self[key] = RepostCollection.from_json(value) 31 | elif key == 'tags': 32 | self[key] = PureTagCollection.from_json(value) 33 | elif key == 'entities': 34 | self[key] = PureEntityCollection.from_json(value) 35 | elif key == 'user': 36 | self[key] = User.from_json(value) 37 | 38 | name_attr = { 39 | 'user': 'username', 40 | 'post': 'description', 41 | 'comment': 'comment', 42 | 'tag': 'tag', 43 | 'channel': 'channel', 44 | 'notification': 'notificationTypeId', 45 | 46 | 'like': 'postId', 47 | 'repost': 'postId', 48 | 'conversation': 'conversationId', 49 | 'message': 'message' 50 | }.get(classname) 51 | self['name'] = self.get(name_attr, '') 52 | 53 | return self 54 | return _decorator 55 | 56 | 57 | def ensure_ownership(fn): 58 | """Ensure ownership of the object, to avoid wasting a request""" 59 | 60 | def _decorator(self, *args, **kwargs): 61 | user_id = self.get('post', {}).get('user', {}).get( 62 | 'id') or self.get('user', {}).get('id') or self.id 63 | if(user_id == self.api._user_id): 64 | return fn(self, *args, **kwargs) 65 | else: 66 | raise VineError( 67 | 4, "You don't have permission to access that record.") 68 | return _decorator 69 | 70 | 71 | def chained(fn): 72 | """Chain instance methods 73 | Can do things like user.unfollow().follow().unfollow() 74 | """ 75 | 76 | def _decorator(self, *args, **kwargs): 77 | fn(self, *args, **kwargs) 78 | return self 79 | return _decorator 80 | 81 | 82 | def inject_post(fn): 83 | """Inject Post into child model""" 84 | 85 | def _decorator(self, *args, **kwargs): 86 | obj = fn(self, *args, **kwargs) 87 | obj.post = self 88 | return obj 89 | return _decorator 90 | 91 | 92 | class Model(AttrDict): 93 | api = None 94 | 95 | @classmethod 96 | @parse_vine_json 97 | def from_json(cls, data): 98 | self = cls() 99 | self._attrs = AttrDict(data) 100 | self.json = json.dumps(data) 101 | for key, value in list(self._attrs.items()): 102 | if key not in dir(self): 103 | self[key] = value 104 | return self 105 | 106 | @classmethod 107 | def from_id(cls, _id, **kwargs): 108 | self = cls() 109 | self['id'] = _id 110 | return self 111 | 112 | def connect_api(self, api): 113 | self.api = api 114 | 115 | def __repr__(self): 116 | classname = self.__class__.__name__ 117 | name = self.get('name', '') 118 | if type(name) is int: 119 | name = str(name) 120 | else: 121 | # description, usernames and comments may contain weird chars 122 | # name = name.encode('ascii', 'ignore') # if you want to remove emojis 123 | name = name.encode('utf8') 124 | max_chars = 10 125 | name = name[:max_chars] + (name[max_chars:] and '...') 126 | 127 | return "<%s [%s] '%s'>" % (classname, self.id, name) 128 | 129 | 130 | 131 | class ModelCollection(list): 132 | model = Model 133 | 134 | @classmethod 135 | def from_json(cls, data): 136 | self = cls() 137 | for item in data: 138 | self.append(self.model.from_json(item)) 139 | return self 140 | 141 | def connect_api(self, api): 142 | for item in self: 143 | item.connect_api(api) 144 | 145 | def __iter__(self): 146 | self._iterator = list.__iter__(self) 147 | return self._iterator 148 | 149 | def __next__(self): 150 | return next(self._iterator) 151 | 152 | 153 | # Model collection with metadata 154 | class MetaModelCollection(Model): 155 | model_key = 'records' 156 | collection_class = ModelCollection 157 | 158 | @classmethod 159 | def from_json(cls, data): 160 | self = cls(Model.from_json(data)) 161 | for key, value in list(self.items()): 162 | if key == self.model_key: 163 | value = self.collection_class.from_json(value) 164 | self[key] = value 165 | return self 166 | 167 | def connect_api(self, api): 168 | for item in self.get_collection(): 169 | item.connect_api(api) 170 | 171 | def __len__(self): 172 | return len(self.get_collection()) 173 | 174 | def __getitem__(self, descriptor): 175 | # Retrieving metadata 176 | if type(descriptor) in [str, str]: 177 | return Model.__getitem__(self, descriptor) 178 | 179 | # Retrieving an element from the list 180 | else: 181 | return self.get_collection().__getitem__(descriptor) 182 | 183 | def __iter__(self): 184 | return self.get_collection().__iter__() 185 | 186 | def __repr__(self): 187 | return self.get_collection().__repr__() 188 | 189 | def __next__(self): 190 | return next(self.get_collection()) 191 | 192 | def get_collection(self): 193 | return self.get(self.model_key, []) 194 | 195 | 196 | class User(Model): 197 | 198 | def connect_api(self, api): 199 | self.api = api 200 | if('key' in list(self.keys())): 201 | self.api.authenticate(self) 202 | 203 | @chained 204 | def follow(self, user=None, **kwargs): 205 | user = user or self 206 | return self.api.follow(user_id=user.id, **kwargs) 207 | 208 | @chained 209 | def unfollow(self, user=None, **kwargs): 210 | user = user or self 211 | return self.api.unfollow(user_id=user.id, **kwargs) 212 | 213 | @chained 214 | def follow_notifications(self, user=None, **kwargs): 215 | user = user or self 216 | return self.api.follow_notifications(user_id=user.id, **kwargs) 217 | 218 | @chained 219 | def unfollow_notifications(self, user=None, **kwargs): 220 | user = user or self 221 | return self.api.unfollow_notifications(user_id=user.id, **kwargs) 222 | 223 | @chained 224 | def block(self, user=None, **kwargs): 225 | user = user or self 226 | return self.api.block(user_id=user.id, **kwargs) 227 | 228 | @chained 229 | def unblock(self, user=None, **kwargs): 230 | user = user or self 231 | 232 | return self.api.unblock(user_id=user.id, **kwargs) 233 | 234 | def followers(self, **kwargs): 235 | return self.api.get_followers(user_id=self.id, **kwargs) 236 | 237 | def following(self, **kwargs): 238 | return self.api.get_following(user_id=self.id, **kwargs) 239 | 240 | def timeline(self, **kwargs): 241 | return self.api.get_user_timeline(user_id=self.id, **kwargs) 242 | 243 | def likes(self, **kwargs): 244 | return self.api.get_user_likes(user_id=self.id, **kwargs) 245 | 246 | @ensure_ownership 247 | def pending_notifications_count(self, **kwargs): 248 | return self.api.get_pending_notifications_count(user_id=self.id, **kwargs) 249 | 250 | @ensure_ownership 251 | def notifications(self, **kwargs): 252 | return self.api.get_notifications(user_id=self.id, **kwargs) 253 | 254 | @chained 255 | @ensure_ownership 256 | def update(self, **kwargs): 257 | return self.api.update_profile(user_id=self.id, **kwargs) 258 | 259 | @chained 260 | @ensure_ownership 261 | def set_explicit(self, **kwargs): 262 | return self.api.set_explicit(user_id=self.id, **kwargs) 263 | 264 | @ensure_ownership 265 | def unset_explicit(self, **kwargs): 266 | return self.api.unset_explicit(user_id=self.id, **kwargs) 267 | 268 | def is_following(self): 269 | return bool(self._attrs.following) 270 | 271 | def is_private(self): 272 | return bool(self._attrs.private) 273 | 274 | def is_blocking(self): 275 | return bool(self._attrs.blocking) 276 | 277 | def is_blocked(self): 278 | return bool(self._attrs.blocked) 279 | 280 | def is_notifying(self): 281 | return bool(self._attrs.notifyPosts) 282 | 283 | 284 | class Post(Model): 285 | 286 | @inject_post 287 | def like(self, **kwargs): 288 | return self.api.like(post_id=self.id, **kwargs) 289 | 290 | def unlike(self, **kwargs): 291 | return self.api.unlike(post_id=self.id, **kwargs) 292 | 293 | @inject_post 294 | def revine(self, **kwargs): 295 | return self.api.revine(post_id=self.id, **kwargs) 296 | 297 | @inject_post 298 | def comment(self, comment, entities=[], **kwargs): 299 | _comment = '' 300 | if type(comment) is list: 301 | entities = [] 302 | for element in comment: 303 | if type(element) is str: 304 | _comment += element 305 | else: 306 | entity = { 307 | 'id': element.id, 308 | 'range': [len(_comment), len(_comment) + len(element.name)], 309 | 'type': 'mention', 310 | 'title': element.name 311 | } 312 | _comment += element.name + ' ' 313 | entities.append(entity) 314 | else: 315 | _comment = comment 316 | 317 | return self.api.comment(post_id=self.id, comment=_comment, entities=entities, **kwargs) 318 | 319 | @chained 320 | def report(self, **kwargs): 321 | return self.api.report(post_id=self.id, **kwargs) 322 | 323 | def likes(self, **kwargs): 324 | return self.api.get_post_likes(post_id=self.id, **kwargs) 325 | 326 | def comments(self, **kwargs): 327 | return self.api.get_post_comments(post_id=self.id, **kwargs) 328 | 329 | def reposts(self, **kwargs): 330 | return self.api.get_post_reposts(post_id=self.id, **kwargs) 331 | 332 | 333 | class Comment(Model): 334 | 335 | @ensure_ownership 336 | def delete(self, **kwargs): 337 | return self.api.uncomment(post_id=self.post.id, comment_id=self.id, **kwargs) 338 | pass 339 | 340 | 341 | class Like(Model): 342 | 343 | @ensure_ownership 344 | def delete(self, **kwargs): 345 | return self.api.unlike(post_id=self.post.id, **kwargs) 346 | pass 347 | 348 | 349 | class Repost(Model): 350 | 351 | @ensure_ownership 352 | def delete(self, **kwargs): 353 | return self.api.unrevine(post_id=self.post.id, revine_id=self.id, **kwargs) 354 | pass 355 | 356 | 357 | class Tag(Model): 358 | 359 | def timeline(self, **kwargs): 360 | return self.api.get_tag_timeline(tag_name=self.tag, **kwargs) 361 | 362 | 363 | class Channel(Model): 364 | 365 | def timeline(self, **kwargs): 366 | return self.api.get_channel_recent_timeline(channel_id=self.id, **kwargs) 367 | 368 | def recent_timeline(self, **kwargs): 369 | return self.timeline() 370 | 371 | def popular_timeline(self, **kwargs): 372 | return self.api.get_channel_popular_timeline(channel_id=self.id, **kwargs) 373 | 374 | 375 | class Notification(Model): 376 | pass 377 | 378 | 379 | # mention, tag or post in a notification, comment or title 380 | class Entity(Model): 381 | pass 382 | 383 | 384 | class Venue(Model): 385 | pass 386 | 387 | 388 | class Conversation(Model): 389 | pass 390 | 391 | 392 | class Message(Model): 393 | pass 394 | 395 | 396 | class PureUserCollection(ModelCollection): 397 | model = User 398 | 399 | 400 | class UserCollection(MetaModelCollection): 401 | collection_class = PureUserCollection 402 | 403 | 404 | class PurePostCollection(ModelCollection): 405 | model = Post 406 | 407 | 408 | # Timeline 409 | class PostCollection(MetaModelCollection): 410 | collection_class = PurePostCollection 411 | 412 | 413 | class PureCommentCollection(ModelCollection): 414 | model = Comment 415 | 416 | 417 | class CommentCollection(MetaModelCollection): 418 | collection_class = PureCommentCollection 419 | 420 | 421 | class PureLikeCollection(ModelCollection): 422 | model = Like 423 | 424 | 425 | class LikeCollection(MetaModelCollection): 426 | collection_class = PureLikeCollection 427 | 428 | 429 | class PureRepostCollection(ModelCollection): 430 | model = Repost 431 | 432 | 433 | class RepostCollection(MetaModelCollection): 434 | collection_class = PureRepostCollection 435 | 436 | 437 | class PureTagCollection(ModelCollection): 438 | model = Tag 439 | 440 | 441 | class TagCollection(MetaModelCollection): 442 | collection_class = PureTagCollection 443 | 444 | 445 | class PureChannelCollection(ModelCollection): 446 | model = Channel 447 | 448 | 449 | class ChannelCollection(MetaModelCollection): 450 | collection_class = PureChannelCollection 451 | 452 | 453 | class PureNotificationCollection(ModelCollection): 454 | model = Notification 455 | 456 | 457 | class NotificationCollection(MetaModelCollection): 458 | collection_class = PureNotificationCollection 459 | 460 | 461 | class PureEntityCollection(ModelCollection): 462 | model = Entity 463 | 464 | 465 | class PureConversationCollection(ModelCollection): 466 | model = Conversation 467 | 468 | 469 | class ConversationCollection(MetaModelCollection): 470 | collection_class = PureConversationCollection 471 | 472 | 473 | class PureMessageCollection(ModelCollection): 474 | model = Message 475 | 476 | 477 | class MessageCollection(MetaModelCollection): 478 | collection_class = PureMessageCollection 479 | model_key = 'messages' 480 | -------------------------------------------------------------------------------- /vinepy/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from functools import reduce 3 | 4 | VINE_HASHING_KEY = 'BuzaW7ZmKAqbhMOei5J1nvr6gXHwdpDjITtFUPxQ20E9VY3Ll' 5 | index_key_dict = dict([(char, index) 6 | for index, char in enumerate(VINE_HASHING_KEY)]) 7 | 8 | 9 | def post_long_id(short_id): 10 | prepared_hash = enumerate(short_id[::-1]) 11 | long_id = reduce(lambda acc, index_key: acc + index_key_dict[ 12 | index_key[1]] * len(VINE_HASHING_KEY) ** index_key[0], prepared_hash, 0) 13 | return long_id 14 | 15 | 16 | def post_short_id(long_id): 17 | id_fragments = int2base(long_id, len(VINE_HASHING_KEY)) 18 | short_id_fragments = map( 19 | lambda fragment: VINE_HASHING_KEY[fragment], id_fragments) 20 | return ''.join(short_id_fragments) 21 | 22 | 23 | def int2base(x, base): 24 | if x < 0: 25 | sign = -1 26 | elif x == 0: 27 | return 0 28 | else: 29 | sign = 1 30 | x *= sign 31 | digits = [] 32 | while x: 33 | digits.append(x % base) 34 | x //= base 35 | if sign < 0: 36 | digits.append('-') 37 | digits.reverse() 38 | return digits 39 | 40 | 41 | def strptime(string, fmt='%Y-%m-%dT%H:%M:%S.%f'): 42 | return datetime.strptime(string, fmt) 43 | 44 | # From http://stackoverflow.com/a/14620633 45 | # CAUTION: it causes memory leak in < 2.7.3 and < 3.2.3 46 | 47 | 48 | class AttrDict(dict): 49 | 50 | def __init__(self, *args, **kwargs): 51 | super(AttrDict, self).__init__(*args, **kwargs) 52 | self.__dict__ = self 53 | --------------------------------------------------------------------------------