├── .circleci └── config.yml ├── .flake8 ├── .github └── dependabot.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.rst ├── LICENSE ├── README.md ├── annict ├── __init__.py ├── api.py ├── auth.py ├── cursors.py ├── models.py ├── parsers.py ├── services.py └── utils.py ├── docs ├── Makefile ├── _gen │ ├── annict.api.API.activities.rst │ ├── annict.api.API.create_record.rst │ ├── annict.api.API.delete_record.rst │ ├── annict.api.API.edit_record.rst │ ├── annict.api.API.episodes.rst │ ├── annict.api.API.followers.rst │ ├── annict.api.API.following.rst │ ├── annict.api.API.following_activities.rst │ ├── annict.api.API.me.rst │ ├── annict.api.API.my_programs.rst │ ├── annict.api.API.my_works.rst │ ├── annict.api.API.records.rst │ ├── annict.api.API.rst │ ├── annict.api.API.search_users.rst │ ├── annict.api.API.set_status.rst │ └── annict.api.API.works.rst ├── annict.rst ├── conf.py ├── environment.yml ├── index.rst ├── make.bat ├── modules.rst └── user.rst ├── news ├── .gitignore ├── 21.doc ├── 21.feature └── _template.rst ├── poetry.lock ├── pyproject.toml ├── readthedocs.yml ├── tests ├── __init__.py ├── conftest.py ├── test_api.py ├── test_auth.py ├── test_cursors.py ├── test_models.py ├── test_parser.py ├── test_services.py └── test_utils.py └── tox.ini /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | commands: 3 | install_poetry: 4 | description: Install poetry 5 | steps: 6 | - run: curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python 7 | - run: echo 'export PATH=$HOME/.poetry/bin:$PATH' >> $BASH_ENV 8 | - run: source $BASH_ENV 9 | - run: source $HOME/.poetry/env 10 | remove_pyc: 11 | description: Remove .pyc files 12 | steps: 13 | - run: find . -name \*.pyc -delete 14 | install_dependencies: 15 | description: Install dependencies 16 | steps: 17 | - run: poetry install 18 | pytest_cov: 19 | description: Run pytest-cov 20 | steps: 21 | - run: poetry run pytest tests --cov annict -vv 22 | pytest_cov_with_reports: 23 | description: Run pytest 24 | steps: 25 | - run: poetry run pytest tests --cov-report html:test-reports/coverage --cov-report xml --cov annict -vv --junit-xml=test-reports/pytest.xml 26 | codecov: 27 | steps: 28 | - run: pip install --user codecov && $HOME/.local/bin/codecov 29 | jobs: 30 | build: 31 | docker: 32 | - image: circleci/python:3.7 33 | working_directory: ~/python-annict 34 | steps: 35 | - checkout 36 | - install_poetry 37 | - remove_pyc 38 | - install_dependencies 39 | - pytest_cov_with_reports 40 | - codecov 41 | - store_test_results: 42 | path: test-reports 43 | - store_artifacts: 44 | path: test-reports 45 | destination: test-reports 46 | py36: 47 | docker: 48 | - image: circleci/python:3.6 49 | working_directory: ~/python-annict 50 | steps: 51 | - checkout 52 | - checkout 53 | - install_poetry 54 | - remove_pyc 55 | - install_dependencies 56 | - pytest_cov 57 | workflows: 58 | version: 2 59 | test-py37: 60 | jobs: 61 | - build 62 | test-py36: 63 | jobs: 64 | - py36 65 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | max-line-length = 120 4 | exclude = migrations,docs 5 | max-complexity = 10 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "20:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: requests-cache 11 | versions: 12 | - 0.6.0 13 | - 0.6.1 14 | - 0.6.2 15 | - dependency-name: furl 16 | versions: 17 | - 2.1.1 18 | - dependency-name: sphinx 19 | versions: 20 | - 3.4.3 21 | - 3.5.0 22 | - 3.5.1 23 | - 3.5.2 24 | - 3.5.3 25 | - dependency-name: responses 26 | versions: 27 | - 0.12.1 28 | - 0.13.1 29 | - 0.13.2 30 | - dependency-name: arrow 31 | versions: 32 | - 0.17.0 33 | - 1.0.0 34 | - 1.0.2 35 | - 1.0.3 36 | - dependency-name: tox 37 | versions: 38 | - 3.21.3 39 | - 3.21.4 40 | - 3.22.0 41 | - dependency-name: pytest 42 | versions: 43 | - 6.2.2 44 | - dependency-name: sphinx-rtd-theme 45 | versions: 46 | - 0.5.1 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | .mypy_cache/ 92 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.7 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v2.1.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: debug-statements 10 | - id: flake8 11 | exclude: (tests|^docs/conf.py) 12 | 13 | - repo: https://github.com/asottile/reorder_python_imports 14 | rev: v1.3.4 15 | hooks: 16 | - id: reorder-python-imports 17 | 18 | - repo: https://github.com/ambv/black 19 | rev: stable 20 | hooks: 21 | - id: black 22 | 23 | - repo: https://github.com/pre-commit/mirrors-mypy 24 | rev: v0.610-1 25 | hooks: 26 | - id: mypy 27 | args: [--no-strict-optional, --ignore-missing-imports] 28 | exclude: ^docs/conf.py 29 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Annict 0.7.0 (2018-12-25) 2 | ========================= 3 | 4 | Features 5 | -------- 6 | 7 | - Added support cursor mode. (#21) 8 | 9 | Improved Documentation 10 | ---------------------- 11 | 12 | - Added examples of using cursor. (#21) 13 | 14 | 15 | Annict 0.6.1 (2018-12-24) 16 | ========================= 17 | 18 | Vendored Libraries 19 | ------------------ 20 | 21 | - Vendored poetry at 0.12.10 as a packaging tool. 22 | - Vendored tox at 3.6 23 | - Vendored towncrier at 18.6 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 kk6 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-annict 2 | 3 | [Annict API](https://docs.annict.com/ja/api/) wrapper for Python 4 | 5 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/e7936cf6e72a4e14b3bfb07879de1c3d)](https://app.codacy.com/app/hiro.ashiya/python-annict?utm_source=github.com&utm_medium=referral&utm_content=kk6/python-annict&utm_campaign=Badge_Grade_Dashboard) 6 | [![CircleCI](https://img.shields.io/circleci/project/github/kk6/python-annict.svg?style=flat-square)](https://circleci.com/gh/kk6/python-annict) 7 | [![codecov](https://codecov.io/gh/kk6/python-annict/branch/master/graph/badge.svg)](https://codecov.io/gh/kk6/python-annict) 8 | [![PyPI](https://img.shields.io/pypi/v/annict.svg?style=flat-square)](https://pypi.org/project/annict/) 9 | [![License](https://img.shields.io/pypi/l/annict.svg)](https://pypi.org/project/annict/) 10 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 11 | 12 | **python-annict** officially supports Python 3.6 or higher. 13 | 14 | ## Installation 15 | 16 | ```bash 17 | pip install annict 18 | ``` 19 | 20 | ## Quickstart 21 | 22 | ### Authentication 23 | 24 | Acquire the URL for authentication code. 25 | 26 | ```python 27 | >>> from annict.auth import OAuthHandler 28 | >>> handler = OAuthHandler(client_id='Your client ID', client_secret='Your client secret') 29 | >>> url = handler.get_authorization_url(scope='read write') 30 | >>> print(url) 31 | ``` 32 | 33 | Open the browser and access the URL you obtained, the authentication code will be displayed. 34 | It will be passed to the `handler.authenticate()` 's argument to get the access token. 35 | 36 | ```python 37 | >>> handler.authenticate(code='Authentication code') 38 | >>> print(handler.get_access_token()) 39 | ``` 40 | 41 | Note that this authentication flow is unnecessary when issuing a personal access token on Annict and using it. 42 | 43 | See: [Annict API: 個人用アクセストークンが発行できるようになりました](http://blog.annict.com/post/157138114218/personal-access-token) 44 | 45 | ### Hello world 46 | 47 | 48 | ```python 49 | >>> from annict.api import API 50 | >>> annict = API('Your access token') 51 | >>> results = annict.works(filter_title="Re:ゼロから始める異世界生活") 52 | >>> print(results[0].title) 53 | Re:ゼロから始める異世界生活 54 | ``` 55 | 56 | ### Cache 57 | 58 | For now, we do not have our own cache system. However, caching is also important to reduce the load on AnnictAPI. 59 | 60 | So I introduce a cache plugin for *requests* library called [requests_cache](https://github.com/reclosedev/requests-cache). 61 | 62 | Install with pip. 63 | 64 | ```bash 65 | pip insall requests_cache 66 | ``` 67 | 68 | *requests_cache* is very easy to use. 69 | 70 | ```python 71 | >>> import requests_cache 72 | >>> requests_cache.install_cache(cache_name='annict', backend='memory', expire_after=300) 73 | >>> # At first, from Annict API. 74 | >>> api.me() 75 | >>> # You can get results from cache, if it is within the expiration time. 76 | >>> api.me() 77 | 78 | ``` 79 | 80 | For more information: [Requests-cache documentation](https://requests-cache.readthedocs.io/en/latest/) 81 | 82 | ## Documentation 83 | 84 | - [This library's documentation](https://python-annict.readthedocs.io/en/latest/) 85 | - [Annict Docs(Japanese)](https://docs.annict.com/ja/) 86 | -------------------------------------------------------------------------------- /annict/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | python-annict 4 | ~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Annict API for Python. 7 | 8 | """ 9 | 10 | __title__ = "python-annict" 11 | __version__ = "0.7.0" 12 | __author__ = "Hiro Ashiya" 13 | __license__ = "MIT" 14 | 15 | from .api import API # noqa 16 | -------------------------------------------------------------------------------- /annict/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .cursors import cursor_support 3 | from .parsers import ModelParser 4 | from .services import APIMethod 5 | 6 | 7 | class API(object): 8 | """API wrapper for Annict. 9 | 10 | Basic Usage:: 11 | 12 | >>> from annict.api import API 13 | >>> api = API('your-access-token') 14 | >>> api.me() 15 | 16 | 17 | """ 18 | 19 | def __init__( 20 | self, 21 | token, 22 | base_url="https://api.annict.com", 23 | api_version="v1", 24 | parser=ModelParser, 25 | ): 26 | self.token = token 27 | self.base_url = base_url 28 | self.api_version = api_version 29 | self.parser = parser(self) 30 | 31 | @cursor_support 32 | def works( 33 | self, 34 | fields=None, 35 | filter_ids=None, 36 | filter_season=None, 37 | filter_title=None, 38 | page=None, 39 | per_page=None, 40 | sort_id=None, 41 | sort_season=None, 42 | sort_watchers_count=None, 43 | ): 44 | """Get works information 45 | 46 | :reference: https://docs.annict.com/ja/api/v1/works.html 47 | :param fields: (optional) Narrow down the fields of data contained in the response body. 48 | :type fields: list of str 49 | :param filter_ids: (optional) Filter results by IDs. 50 | :type filter_ids: list of int 51 | :param str filter_season: (optional) Filter results by release time of season. 52 | :param str filter_title: (optional) Filter results by title. 53 | :param int page: (optional) Specify the number of pages. 54 | :param int per_page: (optional) Specify how many items to acquire per page. 55 | :param str sort_id: (optional) Sort the results by their ID. You can specify `asc` or `desc`. 56 | :param str sort_season: (optional) Sort the results by their release time of season. 57 | You can specify `asc` or `desc`. 58 | :param str sort_watchers_count: (optional) Sort the results by their watchers count. 59 | You can specify `asc` or `desc`. 60 | :return: list of :class:`Work ` objects. 61 | :rtype: annict.models.ResultSet 62 | 63 | """ 64 | api_method = APIMethod( 65 | api=self, 66 | path="works", 67 | method="GET", 68 | allowed_params=( 69 | "fields", 70 | "filter_ids", 71 | "filter_season", 72 | "filter_title", 73 | "page", 74 | "per_page", 75 | "sort_id", 76 | "sort_season", 77 | "sort_watchers_count", 78 | ), 79 | payload_type="work", 80 | payload_is_list=True, 81 | ) 82 | params = api_method.build_parameters(locals()) 83 | return api_method(params) 84 | 85 | @cursor_support 86 | def episodes( 87 | self, 88 | fields=None, 89 | filter_ids=None, 90 | filter_work_id=None, 91 | page=None, 92 | per_page=None, 93 | sort_id=None, 94 | sort_sort_number=None, 95 | ): 96 | """Get episodes information 97 | 98 | :reference: https://docs.annict.com/ja/api/v1/episodes.html 99 | :param fields: (optional) Narrow down the fields of data contained in the response body. 100 | :type fields: list of str 101 | :param filter_ids: (optional) Filter results by IDs. 102 | :type filter_ids: list of int 103 | :param int filter_work_id: (optional) Filter results by Work's ID. 104 | :param int page: (optional) Specify the number of pages. 105 | :param int per_page: (optional) Specify how many items to acquire per page. 106 | :param str sort_id: (optional) Sort the results by their ID. You can specify `asc` or `desc`. 107 | :param str sort_sort_number: (optional) Sort by number for sorting. You can specify `asc` or `desc`. 108 | :return: list of :class:`Episode ` objects. 109 | :rtype: annict.models.ResultSet 110 | 111 | """ 112 | api_method = APIMethod( 113 | api=self, 114 | path="episodes", 115 | method="GET", 116 | allowed_params=( 117 | "fields", 118 | "filter_ids", 119 | "filter_work_id", 120 | "page", 121 | "per_page", 122 | "sort_id", 123 | "sort_sort_number", 124 | ), 125 | payload_type="episode", 126 | payload_is_list=True, 127 | ) 128 | params = api_method.build_parameters(locals()) 129 | return api_method(params) 130 | 131 | @cursor_support 132 | def people( 133 | self, 134 | fields=None, 135 | filter_ids=None, 136 | filter_name=None, 137 | page=None, 138 | per_page=None, 139 | sort_id=None, 140 | ): 141 | """ 142 | 143 | :param fields: (optional) Narrow down the fields of data contained in the response body. 144 | :type fields: list of str 145 | :param filter_ids: (optional) Filter results by IDs. 146 | :type filter_ids: list of int 147 | :param str filter_name: Filter results by name. 148 | :param int page: (optional) Specify the number of pages. 149 | :param int per_page: (optional) Specify how many items to acquire per page. 150 | :param str sort_id: (optional) Sort the results by their ID. You can specify `asc` or `desc`. 151 | :return: 152 | """ 153 | api_method = APIMethod( 154 | api=self, 155 | path="people", 156 | method="GET", 157 | allowed_params=( 158 | "fields", 159 | "filter_ids", 160 | "filter_name", 161 | "page", 162 | "per_page", 163 | "sort_id", 164 | ), 165 | payload_type="person", 166 | payload_is_list=True, 167 | ) 168 | params = api_method.build_parameters(locals()) 169 | return api_method(params) 170 | 171 | @cursor_support 172 | def records( 173 | self, 174 | fields=None, 175 | filter_ids=None, 176 | filter_episode_id=None, 177 | filter_has_record_comment=None, 178 | page=None, 179 | per_page=None, 180 | sort_id=None, 181 | sort_likes_count=None, 182 | ): 183 | """Get records to episodes 184 | 185 | :reference: https://docs.annict.com/ja/api/v1/records.html 186 | :param fields: (optional) Narrow down the fields of data contained in the response body. 187 | :type fields: list of str 188 | :param filter_ids: (optional) Filter results by IDs. 189 | :type filter_ids: list of int 190 | :param int filter_episode_id: (optional) Filter results by Episode's ID. 191 | :param bool filter_has_record_comment: (optional) Filter the results by the presence or absence of comments. 192 | If you specify `True`, only records with comments will be filtered. 193 | Specifying `False` Filter records without comments. 194 | :param int page: (optional) Specify the number of pages. 195 | :param int per_page: (optional) Specify how many items to acquire per page. 196 | :param str sort_id: (optional) Sort the results by their ID. You can specify `asc` or `desc`. 197 | :param str sort_likes_count: (optional) Sort the results by their number of likes. 198 | You can specify `asc` or `desc`. 199 | :return: list of :class:`Record ` objects. 200 | :rtype: annict.models.ResultSet 201 | 202 | """ 203 | api_method = APIMethod( 204 | api=self, 205 | path="records", 206 | method="GET", 207 | allowed_params=( 208 | "fields", 209 | "filter_ids", 210 | "filter_episode_id", 211 | "filter_has_record_comment", 212 | "page", 213 | "per_page", 214 | "sort_id", 215 | "sort_likes_count", 216 | ), 217 | payload_type="record", 218 | payload_is_list=True, 219 | ) 220 | params = api_method.build_parameters(locals()) 221 | return api_method(params) 222 | 223 | @cursor_support 224 | def search_users( 225 | self, 226 | fields=None, 227 | filter_ids=None, 228 | filter_usernames=None, 229 | page=None, 230 | per_page=None, 231 | sort_id=None, 232 | ): 233 | """Get users information 234 | 235 | :reference: https://docs.annict.com/ja/api/v1/users.html 236 | :param fields: (optional) Narrow down the fields of data contained in the response body. 237 | :type fields: list of str 238 | :param filter_ids: (optional) Filter results by IDs. 239 | :type filter_ids: list of int 240 | :param filter_usernames: (optional) Filter results by usernames. 241 | :type filter_usernames: list of str 242 | :param int page: (optional) Specify the number of pages. 243 | :param int per_page: (optional) Specify how many items to acquire per page. 244 | :param str sort_id: (optional) Sort the results by their ID. You can specify `asc` or `desc`. 245 | :return: list of :class:`User ` objects. 246 | :rtype: annict.models.ResultSet 247 | 248 | """ 249 | api_method = APIMethod( 250 | api=self, 251 | path="users", 252 | method="GET", 253 | allowed_params=( 254 | "fields", 255 | "filter_ids", 256 | "filter_usernames", 257 | "page", 258 | "per_page", 259 | "sort_id", 260 | ), 261 | payload_type="user", 262 | payload_is_list=True, 263 | ) 264 | params = api_method.build_parameters(locals()) 265 | return api_method(params) 266 | 267 | @cursor_support 268 | def following( 269 | self, 270 | fields=None, 271 | filter_user_id=None, 272 | filter_username=None, 273 | page=None, 274 | per_page=None, 275 | sort_id=None, 276 | ): 277 | """Get following information 278 | 279 | :reference: https://docs.annict.com/ja/api/v1/following.html 280 | :param fields: (optional) Narrow down the fields of data contained in the response body. 281 | :type fields: list of str 282 | :param int filter_user_id: (optional) Filter results by User's ID. 283 | :param str filter_username: (optional) Filter results by username. 284 | :param int page: (optional) Specify the number of pages. 285 | :param int per_page: (optional) Specify how many items to acquire per page. 286 | :param str sort_id: (optional) Sort the results by their ID. You can specify `asc` or `desc`. 287 | :return: list of :class:`User ` objects. 288 | :rtype: annict.models.ResultSet 289 | 290 | """ 291 | api_method = APIMethod( 292 | api=self, 293 | path="following", 294 | method="GET", 295 | allowed_params=( 296 | "fields", 297 | "filter_user_id", 298 | "filter_username", 299 | "page", 300 | "per_page", 301 | "sort_id", 302 | ), 303 | payload_type="user", 304 | payload_is_list=True, 305 | ) 306 | params = api_method.build_parameters(locals()) 307 | return api_method(params) 308 | 309 | @cursor_support 310 | def followers( 311 | self, 312 | fields=None, 313 | filter_user_id=None, 314 | filter_username=None, 315 | page=None, 316 | per_page=None, 317 | sort_id=None, 318 | ): 319 | """Get followers information 320 | 321 | :reference: https://docs.annict.com/ja/api/v1/followers.html 322 | :param fields: (optional) Narrow down the fields of data contained in the response body. 323 | :type fields: list of str 324 | :param int filter_user_id: (optional) Filter results by User's ID. 325 | :param str filter_username: (optional) Filter results by username. 326 | :param int page: (optional) Specify the number of pages. 327 | :param int per_page: (optional) Specify how many items to acquire per page. 328 | :param str sort_id: (optional) Sort the results by their ID. You can specify `asc` or `desc`. 329 | :return: list of :class:`User ` objects. 330 | :rtype: annict.models.ResultSet 331 | 332 | """ 333 | api_method = APIMethod( 334 | api=self, 335 | path="followers", 336 | method="GET", 337 | allowed_params=( 338 | "fields", 339 | "filter_user_id", 340 | "filter_username", 341 | "page", 342 | "per_page", 343 | "sort_id", 344 | ), 345 | payload_type="user", 346 | payload_is_list=True, 347 | ) 348 | params = api_method.build_parameters(locals()) 349 | return api_method(params) 350 | 351 | @cursor_support 352 | def activities( 353 | self, 354 | fields=None, 355 | filter_user_id=None, 356 | filter_username=None, 357 | page=None, 358 | per_page=None, 359 | sort_id=None, 360 | ): 361 | """Get activities 362 | 363 | :reference: https://docs.annict.com/ja/api/v1/activities.html 364 | :param fields: (optional) Narrow down the fields of data contained in the response body. 365 | :type fields: list of str 366 | :param int filter_user_id: (optional) Filter results by User's ID. 367 | :param str filter_username: (optional) Filter results by username. 368 | :param int page: (optional) Specify the number of pages. 369 | :param int per_page: (optional) Specify how many items to acquire per page. 370 | :param str sort_id: (optional) Sort the results by their ID. You can specify `asc` or `desc`. 371 | :return: list of :class:`Activity ` objects. 372 | :rtype: annict.models.ResultSet 373 | 374 | """ 375 | api_method = APIMethod( 376 | api=self, 377 | path="activities", 378 | method="GET", 379 | allowed_params=( 380 | "fields", 381 | "filter_user_id", 382 | "filter_username", 383 | "page", 384 | "per_page", 385 | "sort_id", 386 | ), 387 | payload_type="activity", 388 | payload_is_list=True, 389 | ) 390 | params = api_method.build_parameters(locals()) 391 | return api_method(params) 392 | 393 | def me(self, fields=None): 394 | """Get your profile information 395 | 396 | :reference: https://docs.annict.com/ja/api/v1/me.html 397 | :param fields: (optional) Narrow down the fields of data contained in the response body. 398 | :type fields: list of str 399 | :return: :class:`User ` object of your user information. 400 | 401 | """ 402 | api_method = APIMethod( 403 | api=self, 404 | path="me", 405 | method="GET", 406 | allowed_params=("fields",), 407 | payload_type="user", 408 | ) 409 | params = api_method.build_parameters(locals()) 410 | return api_method(params) 411 | 412 | def set_status(self, work_id, kind): 413 | """Set the status of the work. 414 | 415 | :reference: https://docs.annict.com/ja/api/v1/me-statuses.html 416 | :param int work_id: Work's ID 417 | :param str kind: Types of status. 418 | You can specify `wanna_watch`, `watching`, `watched`, `on_hold`, `stop_watching`, or `no_select`. 419 | :return: Returns `True` if deletion succeeded. 420 | 421 | """ 422 | api_method = APIMethod( 423 | api=self, 424 | path="me/statuses", 425 | method="POST", 426 | allowed_params=("work_id", "kind"), 427 | ) 428 | params = api_method.build_parameters(locals()) 429 | return api_method(params) 430 | 431 | def create_record( 432 | self, 433 | episode_id, 434 | comment=None, 435 | rating=None, 436 | share_twitter=False, 437 | share_facebook=False, 438 | ): 439 | """Create a record to the episode. 440 | 441 | :reference: https://docs.annict.com/ja/api/v1/me-records.html 442 | :param int episode_id: Episode's ID 443 | :param str comment: (optional) Comment. 444 | :param float rating: (optional) Rating. 445 | :param bool share_twitter: (optional) Whether to share the record on Twitter. You can enter `True` or `False`. 446 | :param bool share_facebook: (optional) Whether to share the record on Facebook. You can enter `True` or `False`. 447 | :return: :class:`Record ` object. 448 | 449 | """ 450 | api_method = APIMethod( 451 | api=self, 452 | path="me/records", 453 | method="POST", 454 | allowed_params=( 455 | "episode_id", 456 | "comment", 457 | "rating", 458 | "share_twitter", 459 | "share_facebook", 460 | ), 461 | payload_type="record", 462 | ) 463 | params = api_method.build_parameters(locals()) 464 | return api_method(params) 465 | 466 | def edit_record( 467 | self, id_, comment=None, rating=None, share_twitter=False, share_facebook=False 468 | ): 469 | """Edit the created record. 470 | 471 | :reference: https://docs.annict.com/ja/api/v1/me-records.html 472 | :param int id_: Record's ID. 473 | :param str comment: (optional) Comment. 474 | :param float rating: (optional) Rating. 475 | :param bool share_twitter: (optional) Whether to share the record on Twitter. You can enter `True` or `False`. 476 | :param bool share_facebook: (optional) Whether to share the record on Facebook. You can enter `True` or `False`. 477 | :return: :class:`Record ` object after update. 478 | 479 | """ 480 | api_method = APIMethod( 481 | api=self, 482 | path="me/records", 483 | method="PATCH", 484 | allowed_params=("comment", "rating", "share_twitter", "share_facebook"), 485 | payload_type="record", 486 | ) 487 | api_method.build_path(id_) 488 | params = api_method.build_parameters(locals()) 489 | return api_method(params) 490 | 491 | def delete_record(self, id_): 492 | """Delete the created record. 493 | 494 | :reference: https://docs.annict.com/ja/api/v1/me-records.html 495 | :param int id_: Recode's ID 496 | :return: Returns `True` if deletion succeeded. 497 | 498 | """ 499 | api_method = APIMethod( 500 | api=self, path="me/records", method="DELETE", allowed_params=() 501 | ) 502 | api_method.build_path(id_) 503 | params = api_method.build_parameters(locals()) 504 | return api_method(params) 505 | 506 | @cursor_support 507 | def my_works( 508 | self, 509 | fields=None, 510 | filter_ids=None, 511 | filter_season=None, 512 | filter_title=None, 513 | filter_status=None, 514 | page=None, 515 | per_page=None, 516 | sort_id=None, 517 | sort_season=None, 518 | sort_watchers_count=None, 519 | ): 520 | """Get the information of the work you are setting status. 521 | 522 | :reference: https://docs.annict.com/ja/api/v1/me-works.html 523 | :param fields: (optional) Narrow down the fields of data contained in the response body. 524 | :type fields: list of str 525 | :param filter_ids: (optional) Filter results by IDs. 526 | :type filter_ids: list of int 527 | :param str filter_season: (optional) Filter results by release time of season. 528 | :param str filter_title: (optional) Filter results by title. 529 | :param str filter_status: (optional) Filter results by status. 530 | You can specify `wanna_watch`, `watching`, `watched`, `on_hold`, `stop_watching`. 531 | :param int page: (optional) Specify the number of pages. 532 | :param int per_page: (optional) Specify how many items to acquire per page. 533 | :param str sort_id: (optional) Sort the results by their ID. You can specify `asc` or `desc`. 534 | :param str sort_season: (optional) Sort the results by their release time of season. 535 | You can specify `asc` or `desc`. 536 | :param str sort_watchers_count: (optional) Sort the results by their watchers count. 537 | You can specify `asc` or `desc`. 538 | :return: list of :class:`Work ` objects. 539 | :rtype: annict.models.ResultSet 540 | 541 | """ 542 | api_method = APIMethod( 543 | api=self, 544 | path="me/works", 545 | method="GET", 546 | allowed_params=( 547 | "fields", 548 | "filter_ids", 549 | "filter_season", 550 | "filter_title", 551 | "filter_status", 552 | "page", 553 | "per_page", 554 | "sort_id", 555 | "sort_season", 556 | "sort_watchers_count", 557 | ), 558 | payload_type="work", 559 | payload_is_list=True, 560 | ) 561 | params = api_method.build_parameters(locals()) 562 | return api_method(params) 563 | 564 | @cursor_support 565 | def my_programs( 566 | self, 567 | fields=None, 568 | filter_ids=None, 569 | filter_channel_ids=None, 570 | filter_work_ids=None, 571 | filter_started_at_gt=None, 572 | filter_started_at_lt=None, 573 | filter_unwatched=None, 574 | filter_rebroadcast=None, 575 | page=None, 576 | per_page=None, 577 | sort_id=None, 578 | sort_started_at=None, 579 | ): 580 | """Get the broadcast schedule. 581 | 582 | :reference: https://docs.annict.com/ja/api/v1/me-programs.html 583 | :param fields: (optional) Narrow down the fields of data contained in the response body. 584 | :type fields: list of str 585 | :param filter_ids: (optional) Filter results by IDs. 586 | :type filter_ids: list of int 587 | :param filter_channel_ids: (optional) Filter results by Channel IDs. 588 | :type filter_channel_ids: list of int 589 | :param filter_work_ids: (optional) Filter results by Work IDs. 590 | :type filter_work_ids: list of int 591 | :param datetime filter_started_at_gt: (optional) Filter results results to those with the broadcast start date 592 | and time after the specified date and time. 593 | :param datetime filter_started_at_lt: (optional) Filter results results to those with the broadcast start date 594 | and time before the specified date and time. 595 | :param bool filter_unwatched: (optional) Only get unwatched broadcast schedules. 596 | :param bool filter_rebroadcast: (optional) Filter the broadcast schedule based on the rebroadcast flag. 597 | If you pass `True`, only rebroadcasting, 598 | passing `False` will get broadcast schedules other than rebroadcast. 599 | :param int page: (optional) Specify the number of pages. 600 | :param int per_page: (optional) Specify how many items to acquire per page. 601 | :param str sort_id: (optional) Sort the results by their ID. You can specify `asc` or `desc`. 602 | :param str sort_started_at: (optional) Sort the results by started_at. 603 | :return: list of :class:`Program ` objects. 604 | :rtype: annict.models.ResultSet 605 | 606 | """ 607 | api_method = APIMethod( 608 | api=self, 609 | path="me/programs", 610 | method="GET", 611 | allowed_params=( 612 | "fields", 613 | "filter_ids", 614 | "filter_channel_ids", 615 | "filter_work_ids", 616 | "filter_started_at_gt", 617 | "filter_started_at_lt", 618 | "filter_unwatched", 619 | "filter_rebroadcast", 620 | "page", 621 | "per_page", 622 | "sort_id", 623 | "sort_started_at", 624 | ), 625 | payload_type="program", 626 | payload_is_list=True, 627 | ) 628 | params = api_method.build_parameters(locals()) 629 | return api_method(params) 630 | 631 | @cursor_support 632 | def following_activities( 633 | self, 634 | fields=None, 635 | filter_actions=None, 636 | filter_muted=None, 637 | page=None, 638 | per_page=None, 639 | sort_id=None, 640 | ): 641 | """Get the activity of the user you are following. 642 | 643 | :reference: https://docs.annict.com/ja/api/v1/me-following-activities.html 644 | :param fields: (optional) Narrow down the fields of data contained in the response body. 645 | :type fields: list of str 646 | :param str filter_actions: (optional) Filter results by action 647 | (create_record|create_multiple_records|create_status). 648 | :param bool filter_muted: (optional) Specify whether to exclude muted users with the mute function. 649 | You can exclude with `True` and not exclude with `False`. The default is `True` (exclude). 650 | :param int page: (optional) Specify the number of pages. 651 | :param int per_page: (optional) Specify how many items to acquire per page. 652 | :param str sort_id: (optional) Sort the results by their ID. You can specify `asc` or `desc`. 653 | :return: list of :class:`Activity ` objects. 654 | :rtype: annict.models.ResultSet 655 | 656 | """ 657 | api_method = APIMethod( 658 | api=self, 659 | path="me/following_activities", 660 | method="GET", 661 | allowed_params=( 662 | "fields", 663 | "filter_actions", 664 | "filter_muted", 665 | "page", 666 | "per_page", 667 | "sort_id", 668 | ), 669 | payload_type="activity", 670 | payload_is_list=True, 671 | ) 672 | params = api_method.build_parameters(locals()) 673 | return api_method(params) 674 | -------------------------------------------------------------------------------- /annict/auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | from urllib.parse import urljoin 4 | 5 | from rauth import OAuth2Service 6 | 7 | 8 | class OAuthHandler(object): 9 | """OAuth authentication handler""" 10 | 11 | def __init__( 12 | self, 13 | client_id, 14 | client_secret, 15 | name="annict", 16 | base_url="https://api.annict.com", 17 | redirect_uri="urn:ietf:wg:oauth:2.0:oob", 18 | ): 19 | self.client_id = client_id 20 | self.client_secret = client_secret 21 | self.name = name 22 | self.base_url = base_url 23 | self.redirect_uri = redirect_uri 24 | self.auth_session = None 25 | self.oauth = OAuth2Service( 26 | client_id=client_id, 27 | client_secret=client_secret, 28 | name=name, 29 | base_url=base_url, 30 | authorize_url=urljoin(base_url, "/oauth/authorize"), 31 | access_token_url=urljoin(base_url, "/oauth/token"), 32 | ) 33 | 34 | def get_authorization_url(self, scope="read"): 35 | """Returns an authorization url 36 | 37 | :param scope: (optional) Specify authority to access resources. Readonly defaults. 38 | :return: URL of page requesting permission 39 | :rtype: str 40 | 41 | """ 42 | params = { 43 | "scope": scope, 44 | "response_type": "code", 45 | "redirect_uri": self.redirect_uri, 46 | } 47 | return self.oauth.get_authorize_url(**params) 48 | 49 | def authenticate(self, code, decoder=lambda s: json.loads(s.decode("utf8"))): 50 | """Acquire the access token using the authorization code acquired after approval. 51 | 52 | :param code: Authorization code obtained after approval. 53 | :param decoder: (optional) A function used to parse the Response content. Should return a dictionary. 54 | 55 | """ 56 | data = { 57 | "code": code, 58 | "grant_type": "authorization_code", 59 | "redirect_uri": self.redirect_uri, 60 | } 61 | session = self.oauth.get_auth_session(data=data, decoder=decoder) 62 | self.auth_session = session 63 | 64 | def get_access_token(self): 65 | """Returns the access token when authenticated.""" 66 | if self.auth_session: 67 | return self.auth_session.access_token 68 | -------------------------------------------------------------------------------- /annict/cursors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from functools import wraps 3 | 4 | 5 | class SimpleCursor(object): 6 | """Simple cursor class""" 7 | 8 | def __init__(self, method, **kwargs): 9 | if not hasattr(method, "cursor_support"): 10 | raise TypeError( 11 | f"Cursor does not support this method: {method.__func__.__qualname__}" 12 | ) 13 | self.method = method 14 | self.kwargs = kwargs 15 | if "page" not in self.kwargs: 16 | self.kwargs["page"] = 1 17 | 18 | def cursor(self): 19 | while 1: 20 | results = self.method(**self.kwargs) 21 | for result in results: 22 | yield result 23 | self.kwargs["page"] += 1 24 | if not results.next_page or not results: 25 | return 26 | 27 | 28 | def cursor_support(api_method): 29 | """ 30 | Cursor support decorator 31 | 32 | :param api_method: API method that wan to correspond to the cursor. 33 | :return: wrapped method 34 | 35 | """ 36 | api_method.cursor_support = True 37 | 38 | @wraps(api_method) 39 | def wrapper(*args, **kwargs): 40 | return api_method(*args, **kwargs) 41 | 42 | return wrapper 43 | -------------------------------------------------------------------------------- /annict/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import abc 3 | 4 | import arrow 5 | 6 | 7 | class ResultSet(list): 8 | """A list like object that holds results from an Annict API query.""" 9 | 10 | def __init__(self, total_count, prev_page=None, next_page=None): 11 | super().__init__() 12 | self.total_count = total_count 13 | self.prev_page = prev_page 14 | self.next_page = next_page 15 | 16 | 17 | class Model(metaclass=abc.ABCMeta): 18 | """Abstract class of each models.""" 19 | 20 | def __init__(self, api=None): 21 | self._api = api 22 | self._children = [] 23 | 24 | @classmethod 25 | @abc.abstractmethod 26 | def parse(cls, api, json): 27 | """Parse a JSON object into a model instance.""" 28 | raise NotImplementedError 29 | 30 | @classmethod 31 | def parse_list(cls, api, json, payload_type): 32 | """Parse JSON objects into list of model instances. 33 | 34 | :param api: instance of :class:`API ` . 35 | :type api: annict.api.API 36 | :param dict json: JSON from Annict API. 37 | :param str payload_type: Type of payload. 38 | :return: list of Model objects. 39 | :rtype: ResultSet 40 | 41 | """ 42 | results = ResultSet( 43 | total_count=json["total_count"], 44 | prev_page=json["prev_page"], 45 | next_page=json["next_page"], 46 | ) 47 | results._json = json 48 | if payload_type == "activity": 49 | pluralized_payload_name = "activities" 50 | elif payload_type == "person": 51 | pluralized_payload_name = "people" 52 | else: 53 | pluralized_payload_name = "{}s".format(payload_type) 54 | for obj in json[pluralized_payload_name]: 55 | if obj: 56 | results.append(cls.parse(api, obj)) 57 | return results 58 | 59 | 60 | class User(Model): 61 | """User information model""" 62 | 63 | def __repr__(self): 64 | return f"" 65 | 66 | @classmethod 67 | def parse(cls, api, json): 68 | """Parse a JSON object into a model instance. 69 | 70 | :param api: instance of :class:`API ` . 71 | :type api: annict.api.API 72 | :param dict json: JSON from Annict API. 73 | :return: :class:`User ` object 74 | :rtype: User 75 | 76 | """ 77 | user = cls(api) 78 | user._json = json 79 | for k, v in json.items(): 80 | if k == "created_at": 81 | setattr(user, k, arrow.get(v).datetime) 82 | else: 83 | setattr(user, k, v) 84 | return user 85 | 86 | def following(self, fields=None, page=None, per_page=None, sort_id=None): 87 | """Get following information of this user. 88 | 89 | :param fields: (optional) Narrow down the fields of data contained in the response body. 90 | :type fields: list of str 91 | :param int page: (optional) Specify the number of pages. 92 | :param int per_page: (optional) Specify how many items to acquire per page. 93 | :param str sort_id: (optional) Sort the results by their ID. You can specify `asc` or `desc`. 94 | :return: list of :class:`User ` objects. 95 | :rtype: annict.models.ResultSet 96 | 97 | """ 98 | return self._api.following( 99 | fields=fields, 100 | filter_user_id=self.id, 101 | page=page, 102 | per_page=per_page, 103 | sort_id=sort_id, 104 | ) 105 | 106 | def followers(self, fields=None, page=None, per_page=None, sort_id=None): 107 | """Get following information of this user. 108 | 109 | :param fields: (optional) Narrow down the fields of data contained in the response body. 110 | :type fields: list of str 111 | :param int page: (optional) Specify the number of pages. 112 | :param int per_page: (optional) Specify how many items to acquire per page. 113 | :param str sort_id: (optional) Sort the results by their ID. You can specify `asc` or `desc`. 114 | :return: list of :class:`User ` objects. 115 | :rtype: annict.models.ResultSet 116 | 117 | """ 118 | return self._api.followers( 119 | fields=fields, 120 | filter_user_id=self.id, 121 | page=page, 122 | per_page=per_page, 123 | sort_id=sort_id, 124 | ) 125 | 126 | 127 | class Work(Model): 128 | """Work information model""" 129 | 130 | def __init__(self, *args, **kwargs): 131 | super().__init__(*args, **kwargs) 132 | self._episodes = None 133 | 134 | def __repr__(self): 135 | return f"" 136 | 137 | @classmethod 138 | def parse(cls, api, json): 139 | """Parse a JSON object into a model instance. 140 | 141 | :param api: instance of :class:`API ` . 142 | :type api: annict.api.API 143 | :param dict json: JSON from Annict API. 144 | :return: :class:`Work ` object 145 | :rtype: Work 146 | 147 | """ 148 | work = cls(api) 149 | work._json = json 150 | for k, v in json.items(): 151 | if k == "released_on": 152 | if v: 153 | date = arrow.get(v).date() 154 | else: 155 | date = None 156 | setattr(work, k, date) 157 | else: 158 | setattr(work, k, v) 159 | return work 160 | 161 | def set_status(self, kind): 162 | """Set the status of the work. 163 | 164 | :param str kind: Types of status. 165 | You can specify `wanna_watch`, `watching`, `watched`, `on_hold`, `stop_watching`, or `no_select`. 166 | :return: Returns `True` if deletion succeeded. 167 | 168 | """ 169 | return self._api.set_status(self.id, kind) 170 | 171 | def episodes( 172 | self, 173 | fields=None, 174 | filter_ids=None, 175 | page=None, 176 | per_page=None, 177 | sort_id=None, 178 | sort_sort_number=None, 179 | ): 180 | """Get episodes information 181 | 182 | :reference: https://docs.annict.com/ja/api/v1/episodes.html 183 | :param fields: (optional) Narrow down the fields of data contained in the response body. 184 | :type fields: list of str 185 | :param filter_ids: (optional) Filter results by IDs. 186 | :type filter_ids: list of int 187 | :param int page: (optional) Specify the number of pages. 188 | :param int per_page: (optional) Specify how many items to acquire per page. 189 | :param str sort_id: (optional) Sort the results by their ID. You can specify `asc` or `desc`. 190 | :param str sort_sort_number: (optional) Sort by number for sorting. You can specify `asc` or `desc`. 191 | :return: list of :class:`Episode ` objects. 192 | :rtype: annict.models.ResultSet 193 | 194 | """ 195 | return self._api.episodes( 196 | fields=fields, 197 | filter_ids=filter_ids, 198 | filter_work_id=self.id, 199 | page=page, 200 | per_page=per_page, 201 | sort_id=sort_id, 202 | sort_sort_number=sort_sort_number, 203 | ) 204 | 205 | def select_episodes(self, *numbers): 206 | """Select multiple episodes 207 | 208 | :param numbers: Episode number. 209 | :return: list of :class:`Episode ` 210 | :rtype: list of :class:`Episode ` 211 | 212 | """ 213 | if not self._episodes: 214 | self._episodes = self.episodes(sort_sort_number="asc") 215 | if not numbers: 216 | return self._episodes 217 | return [self._episodes[n - 1] for n in numbers] 218 | 219 | def get_episode(self, number): 220 | """Get Episode object 221 | 222 | :param number: Episode number 223 | :return: :class:`Episode ` object 224 | :rtype: Episode 225 | 226 | """ 227 | return self.select_episodes(number)[0] 228 | 229 | 230 | class Episode(Model): 231 | """Episode information model""" 232 | 233 | def __repr__(self): 234 | return f"" 235 | 236 | @classmethod 237 | def parse(cls, api, json): 238 | """Parse a JSON object into a model instance. 239 | 240 | :param api: instance of :class:`API ` . 241 | :type api: annict.api.API 242 | :param dict json: JSON from Annict API. 243 | :return: :class:`Episode ` object 244 | :rtype: Episode 245 | 246 | """ 247 | episode = cls(api) 248 | episode._json = json 249 | for k, v in json.items(): 250 | if k == "episode": 251 | setattr(episode, "episode_id", v) 252 | elif k == "work": 253 | work = Work.parse(api, v) 254 | setattr(episode, k, work) 255 | elif k == "prev_episode" and v: 256 | prev_episode = cls.parse(api, v) 257 | setattr(episode, k, prev_episode) 258 | elif k == "next_episode" and v: 259 | next_episode = cls.parse(api, v) 260 | setattr(episode, k, next_episode) 261 | else: 262 | setattr(episode, k, v) 263 | return episode 264 | 265 | def create_record( 266 | self, comment=None, rating=None, share_twitter=False, share_facebook=False 267 | ): 268 | """Create a record for this episode. 269 | 270 | :param str comment: (optional) Comment. 271 | :param float rating: (optional) Rating. 272 | :param bool share_twitter: (optional) Whether to share the record on Twitter. 273 | You can enter `True` or `False`. 274 | :param bool share_facebook: (optional) Whether to share the record on Facebook. 275 | You can enter `True` or `False`. 276 | :return: :class:`Record ` object. 277 | """ 278 | return self._api.create_record( 279 | self.id, comment, rating, share_twitter, share_facebook 280 | ) 281 | 282 | 283 | class Record(Model): 284 | """Record information model""" 285 | 286 | def __repr__(self): 287 | return f"" 288 | 289 | @classmethod 290 | def parse(cls, api, json): 291 | """Parse a JSON object into a model instance. 292 | 293 | :param api: instance of :class:`API ` . 294 | :type api: annict.api.API 295 | :param dict json: JSON from Annict API. 296 | :return: :class:`Record ` object 297 | :rtype: Record 298 | 299 | """ 300 | record = cls(api) 301 | record._json = json 302 | for k, v in json.items(): 303 | if k == "created_at": 304 | setattr(record, k, arrow.get(v).datetime) 305 | elif k == "user": 306 | user = User.parse(api, v) 307 | setattr(record, k, user) 308 | elif k == "work": 309 | work = Work.parse(api, v) 310 | setattr(record, k, work) 311 | elif k == "episode": 312 | episode = Episode.parse(api, v) 313 | setattr(record, k, episode) 314 | else: 315 | setattr(record, k, v) 316 | return record 317 | 318 | def edit( 319 | self, comment=None, rating=None, share_twitter=False, share_facebook=False 320 | ): 321 | """Edit the created record. 322 | 323 | :param str comment: (optional) Comment. 324 | :param float rating: (optional) Rating. 325 | :param bool share_twitter: (optional) Whether to share the record on Twitter. 326 | You can enter `True` or `False`. 327 | :param bool share_facebook: (optional) Whether to share the record on Facebook. 328 | You can enter `True` or `False`. 329 | :return: :class:`Record ` object after edit. 330 | 331 | """ 332 | return self._api.edit_record( 333 | self.id, comment, rating, share_twitter, share_facebook 334 | ) 335 | 336 | def delete(self): 337 | """Delete the created record. 338 | 339 | :return: Returns `True` if deletion succeeded. 340 | 341 | """ 342 | return self._api.delete_record(self.id) 343 | 344 | 345 | class Program(Model): 346 | """Program information model""" 347 | 348 | def __repr__(self): 349 | return f"" 350 | 351 | @classmethod 352 | def parse(cls, api, json): 353 | """Parse a JSON object into a model instance. 354 | 355 | :param api: instance of :class:`API ` . 356 | :type api: annict.api.API 357 | :param dict json: JSON from Annict API. 358 | :return: :class:`Program ` object 359 | :rtype: Program 360 | 361 | """ 362 | program = cls(api) 363 | program._json = json 364 | for k, v in json.items(): 365 | if k == "started_at": 366 | setattr(program, k, arrow.get(v).datetime) 367 | elif k == "work": 368 | work = Work.parse(api, v) 369 | setattr(program, k, work) 370 | elif k == "episode": 371 | episode = Episode.parse(api, v) 372 | setattr(program, k, episode) 373 | else: 374 | setattr(program, k, v) 375 | return program 376 | 377 | 378 | class Activity(Model): 379 | """Activity information model""" 380 | 381 | def __repr__(self): 382 | return f"" 383 | 384 | @classmethod 385 | def parse(cls, api, json): 386 | """Parse a JSON object into a model instance. 387 | 388 | :param api: instance of :class:`API ` . 389 | :type api: annict.api.API 390 | :param dict json: JSON from Annict API. 391 | :return: :class:`Activity ` object 392 | :rtype: Activity 393 | 394 | """ 395 | activity = cls(api) 396 | activity._json = json 397 | for k, v in json.items(): 398 | if k == "created_at": 399 | setattr(activity, k, arrow.get(v).datetime) 400 | elif k == "user": 401 | user = User.parse(api, v) 402 | setattr(activity, k, user) 403 | elif k == "work": 404 | work = Work.parse(api, v) 405 | setattr(activity, k, work) 406 | elif k == "episode": 407 | episode = Episode.parse(api, v) 408 | setattr(activity, k, episode) 409 | else: 410 | setattr(activity, k, v) 411 | return activity 412 | 413 | 414 | class Person(Model): 415 | """Person information model such as cast and staff""" 416 | 417 | def __repr__(self): 418 | return f"" 419 | 420 | @classmethod 421 | def parse(cls, api, json): 422 | """Parse a JSON object into a model instance. 423 | 424 | :param api: instance of :class:`API ` . 425 | :type api: annict.api.API 426 | :param dict json: JSON from Annict API. 427 | :return: :class:`Person ` object 428 | :rtype: Person 429 | 430 | """ 431 | person = cls(api) 432 | person._json = json 433 | for k, v in json.items(): 434 | if k == "birthday": 435 | if v: 436 | date = arrow.get(v).date() 437 | else: 438 | date = None 439 | setattr(person, k, date) 440 | else: 441 | setattr(person, k, v) 442 | return person 443 | 444 | 445 | MODEL_MAPPING = { 446 | "user": User, 447 | "work": Work, 448 | "episode": Episode, 449 | "record": Record, 450 | "program": Program, 451 | "activity": Activity, 452 | "person": Person, 453 | } 454 | -------------------------------------------------------------------------------- /annict/parsers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .models import MODEL_MAPPING 3 | 4 | 5 | class ModelParser(object): 6 | def __init__(self, api, model_mapping=None): 7 | self.model_mapping = model_mapping if model_mapping else MODEL_MAPPING 8 | self._api = api 9 | 10 | def parse(self, json, payload_type, payload_is_list=False): 11 | model = self.model_mapping[payload_type] 12 | if payload_is_list: 13 | return model.parse_list(self._api, json, payload_type) 14 | else: 15 | return model.parse(self._api, json) 16 | -------------------------------------------------------------------------------- /annict/services.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import requests 3 | from furl import furl 4 | 5 | from .utils import stringify 6 | 7 | 8 | class APIMethod(object): 9 | """A class abstracting each method of AnnictAPI 10 | 11 | :param api: instance of :class:`API ` . 12 | :type api: annict.api.API 13 | :param str path: Endpoint path 14 | :param str method: HTTP Method 15 | :param tuple allowed_params: (optional) List of request parameter names that can be sent. 16 | :param str payload_type: Type of payload 17 | :param bool payload_list: Specifies whether the payload is a list or not. 18 | 19 | """ 20 | 21 | def __init__( 22 | self, 23 | api, 24 | path, 25 | method, 26 | allowed_params=None, 27 | payload_type=None, 28 | payload_is_list=False, 29 | ): 30 | self.api = api 31 | self.path = path 32 | self.method = method 33 | self.allowed_params = allowed_params 34 | self.payload_type = payload_type 35 | self.payload_is_list = payload_is_list 36 | 37 | def build_path(self, id_=None): 38 | """Build an suitable path 39 | 40 | If `id_` is given, it is embedded into path. 41 | 42 | :param int id_: Target resource ID 43 | 44 | """ 45 | if id_ is not None: 46 | self.path = "/".join([self.path, str(id_)]) 47 | 48 | def build_url(self): 49 | """Build request url 50 | 51 | :return: request url 52 | :rtype: str 53 | 54 | """ 55 | url = furl(self.api.base_url) 56 | url.path.add(self.api.api_version).add(self.path) 57 | return url.url 58 | 59 | def build_parameters(self, dic): 60 | """Build a suitable parameters for request. 61 | 62 | It filters the given dictionary based on `self.allowed_params` and returns a dictionary with 63 | an additional access token. 64 | 65 | :param dict dic: dict of arguments given to annict.API's method. 66 | :return: dict for request parameter 67 | :rtype: dict 68 | 69 | """ 70 | params = { 71 | key: stringify(dic[key]) 72 | for key in self.allowed_params 73 | if key in dic and dic[key] 74 | } 75 | params["access_token"] = self.api.token 76 | return params 77 | 78 | def __call__(self, params): 79 | url = self.build_url() 80 | resp = requests.request(self.method, url, params=params) 81 | 82 | resp.raise_for_status() 83 | 84 | if resp.status_code == 200: 85 | return self.api.parser.parse( 86 | resp.json(), self.payload_type, self.payload_is_list 87 | ) 88 | elif resp.status_code == 204: 89 | return True 90 | else: 91 | return resp 92 | -------------------------------------------------------------------------------- /annict/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from functools import singledispatch 3 | 4 | 5 | @singledispatch 6 | def stringify(arg): 7 | return str(arg) 8 | 9 | 10 | @stringify.register(str) 11 | def do_not_stringify(arg): 12 | return arg 13 | 14 | 15 | @stringify.register(tuple) 16 | @stringify.register(list) 17 | @stringify.register(set) 18 | def stringify_list(arg): 19 | return ",".join([str(o) for o in arg]) 20 | 21 | 22 | @stringify.register(bool) 23 | def stringify_boolean(arg): 24 | return str(arg).lower() 25 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_elements.papersize=a4 12 | PAPEROPT_letter = -D latex_elements.papersize=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and an HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " lualatexpdf to make LaTeX files and run them through lualatex" 35 | @echo " xelatexpdf to make LaTeX files and run them through xelatex" 36 | @echo " text to make text files" 37 | @echo " man to make manual pages" 38 | @echo " texinfo to make Texinfo files" 39 | @echo " info to make Texinfo files and run them through makeinfo" 40 | @echo " gettext to make PO message catalogs" 41 | @echo " changes to make an overview of all changed/added/deprecated items" 42 | @echo " xml to make Docutils-native XML files" 43 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 44 | @echo " linkcheck to check all external links for integrity" 45 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 46 | @echo " coverage to run coverage check of the documentation (if enabled)" 47 | @echo " dummy to check syntax errors of document sources" 48 | 49 | .PHONY: clean 50 | clean: 51 | rm -rf $(BUILDDIR)/* 52 | 53 | .PHONY: html 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | .PHONY: dirhtml 60 | dirhtml: 61 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 62 | @echo 63 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 64 | 65 | .PHONY: singlehtml 66 | singlehtml: 67 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 68 | @echo 69 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 70 | 71 | .PHONY: pickle 72 | pickle: 73 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 74 | @echo 75 | @echo "Build finished; now you can process the pickle files." 76 | 77 | .PHONY: json 78 | json: 79 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 80 | @echo 81 | @echo "Build finished; now you can process the JSON files." 82 | 83 | .PHONY: htmlhelp 84 | htmlhelp: 85 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 86 | @echo 87 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 88 | ".hhp project file in $(BUILDDIR)/htmlhelp." 89 | 90 | .PHONY: qthelp 91 | qthelp: 92 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 93 | @echo 94 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 95 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 96 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/annict.qhcp" 97 | @echo "To view the help file:" 98 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/annict.qhc" 99 | 100 | .PHONY: applehelp 101 | applehelp: 102 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 103 | @echo 104 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 105 | @echo "N.B. You won't be able to view it unless you put it in" \ 106 | "~/Library/Documentation/Help or install it in your application" \ 107 | "bundle." 108 | 109 | .PHONY: devhelp 110 | devhelp: 111 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 112 | @echo 113 | @echo "Build finished." 114 | @echo "To view the help file:" 115 | @echo "# mkdir -p $$HOME/.local/share/devhelp/annict" 116 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/annict" 117 | @echo "# devhelp" 118 | 119 | .PHONY: epub 120 | epub: 121 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 122 | @echo 123 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 124 | 125 | .PHONY: epub3 126 | epub3: 127 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 128 | @echo 129 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 130 | 131 | .PHONY: latex 132 | latex: 133 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 134 | @echo 135 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 136 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 137 | "(use \`make latexpdf' here to do that automatically)." 138 | 139 | .PHONY: latexpdf 140 | latexpdf: 141 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 142 | @echo "Running LaTeX files through pdflatex..." 143 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 144 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 145 | 146 | .PHONY: latexpdfja 147 | latexpdfja: 148 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 149 | @echo "Running LaTeX files through platex and dvipdfmx..." 150 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 151 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 152 | 153 | .PHONY: lualatexpdf 154 | lualatexpdf: 155 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 156 | @echo "Running LaTeX files through lualatex..." 157 | $(MAKE) PDFLATEX=lualatex -C $(BUILDDIR)/latex all-pdf 158 | @echo "lualatex finished; the PDF files are in $(BUILDDIR)/latex." 159 | 160 | .PHONY: xelatexpdf 161 | xelatexpdf: 162 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 163 | @echo "Running LaTeX files through xelatex..." 164 | $(MAKE) PDFLATEX=xelatex -C $(BUILDDIR)/latex all-pdf 165 | @echo "xelatex finished; the PDF files are in $(BUILDDIR)/latex." 166 | 167 | .PHONY: text 168 | text: 169 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 170 | @echo 171 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 172 | 173 | .PHONY: man 174 | man: 175 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 176 | @echo 177 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 178 | 179 | .PHONY: texinfo 180 | texinfo: 181 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 182 | @echo 183 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 184 | @echo "Run \`make' in that directory to run these through makeinfo" \ 185 | "(use \`make info' here to do that automatically)." 186 | 187 | .PHONY: info 188 | info: 189 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 190 | @echo "Running Texinfo files through makeinfo..." 191 | make -C $(BUILDDIR)/texinfo info 192 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 193 | 194 | .PHONY: gettext 195 | gettext: 196 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 197 | @echo 198 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 199 | 200 | .PHONY: changes 201 | changes: 202 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 203 | @echo 204 | @echo "The overview file is in $(BUILDDIR)/changes." 205 | 206 | .PHONY: linkcheck 207 | linkcheck: 208 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 209 | @echo 210 | @echo "Link check complete; look for any errors in the above output " \ 211 | "or in $(BUILDDIR)/linkcheck/output.txt." 212 | 213 | .PHONY: doctest 214 | doctest: 215 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 216 | @echo "Testing of doctests in the sources finished, look at the " \ 217 | "results in $(BUILDDIR)/doctest/output.txt." 218 | 219 | .PHONY: coverage 220 | coverage: 221 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 222 | @echo "Testing of coverage in the sources finished, look at the " \ 223 | "results in $(BUILDDIR)/coverage/python.txt." 224 | 225 | .PHONY: xml 226 | xml: 227 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 228 | @echo 229 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 230 | 231 | .PHONY: pseudoxml 232 | pseudoxml: 233 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 234 | @echo 235 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 236 | 237 | .PHONY: dummy 238 | dummy: 239 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 240 | @echo 241 | @echo "Build finished. Dummy builder generates no files." 242 | -------------------------------------------------------------------------------- /docs/_gen/annict.api.API.activities.rst: -------------------------------------------------------------------------------- 1 | annict\.api\.API\.activities 2 | ============================ 3 | 4 | .. currentmodule:: annict.api 5 | 6 | .. automethod:: API.activities 7 | -------------------------------------------------------------------------------- /docs/_gen/annict.api.API.create_record.rst: -------------------------------------------------------------------------------- 1 | annict\.api\.API\.create\_record 2 | ================================ 3 | 4 | .. currentmodule:: annict.api 5 | 6 | .. automethod:: API.create_record 7 | -------------------------------------------------------------------------------- /docs/_gen/annict.api.API.delete_record.rst: -------------------------------------------------------------------------------- 1 | annict\.api\.API\.delete\_record 2 | ================================ 3 | 4 | .. currentmodule:: annict.api 5 | 6 | .. automethod:: API.delete_record 7 | -------------------------------------------------------------------------------- /docs/_gen/annict.api.API.edit_record.rst: -------------------------------------------------------------------------------- 1 | annict\.api\.API\.edit\_record 2 | ============================== 3 | 4 | .. currentmodule:: annict.api 5 | 6 | .. automethod:: API.edit_record 7 | -------------------------------------------------------------------------------- /docs/_gen/annict.api.API.episodes.rst: -------------------------------------------------------------------------------- 1 | annict\.api\.API\.episodes 2 | ========================== 3 | 4 | .. currentmodule:: annict.api 5 | 6 | .. automethod:: API.episodes 7 | -------------------------------------------------------------------------------- /docs/_gen/annict.api.API.followers.rst: -------------------------------------------------------------------------------- 1 | annict\.api\.API\.followers 2 | =========================== 3 | 4 | .. currentmodule:: annict.api 5 | 6 | .. automethod:: API.followers 7 | -------------------------------------------------------------------------------- /docs/_gen/annict.api.API.following.rst: -------------------------------------------------------------------------------- 1 | annict\.api\.API\.following 2 | =========================== 3 | 4 | .. currentmodule:: annict.api 5 | 6 | .. automethod:: API.following 7 | -------------------------------------------------------------------------------- /docs/_gen/annict.api.API.following_activities.rst: -------------------------------------------------------------------------------- 1 | annict\.api\.API\.following\_activities 2 | ======================================= 3 | 4 | .. currentmodule:: annict.api 5 | 6 | .. automethod:: API.following_activities 7 | -------------------------------------------------------------------------------- /docs/_gen/annict.api.API.me.rst: -------------------------------------------------------------------------------- 1 | annict\.api\.API\.me 2 | ==================== 3 | 4 | .. currentmodule:: annict.api 5 | 6 | .. automethod:: API.me 7 | -------------------------------------------------------------------------------- /docs/_gen/annict.api.API.my_programs.rst: -------------------------------------------------------------------------------- 1 | annict\.api\.API\.my\_programs 2 | ============================== 3 | 4 | .. currentmodule:: annict.api 5 | 6 | .. automethod:: API.my_programs 7 | -------------------------------------------------------------------------------- /docs/_gen/annict.api.API.my_works.rst: -------------------------------------------------------------------------------- 1 | annict\.api\.API\.my\_works 2 | =========================== 3 | 4 | .. currentmodule:: annict.api 5 | 6 | .. automethod:: API.my_works 7 | -------------------------------------------------------------------------------- /docs/_gen/annict.api.API.records.rst: -------------------------------------------------------------------------------- 1 | annict\.api\.API\.records 2 | ========================= 3 | 4 | .. currentmodule:: annict.api 5 | 6 | .. automethod:: API.records 7 | -------------------------------------------------------------------------------- /docs/_gen/annict.api.API.rst: -------------------------------------------------------------------------------- 1 | annict\.api\.API 2 | ================ 3 | 4 | .. currentmodule:: annict.api 5 | 6 | .. autoclass:: API 7 | 8 | 9 | .. automethod:: __init__ 10 | 11 | 12 | .. rubric:: Methods 13 | 14 | .. autosummary:: 15 | 16 | ~API.__init__ 17 | ~API.activities 18 | ~API.create_record 19 | ~API.delete_record 20 | ~API.edit_record 21 | ~API.episodes 22 | ~API.followers 23 | ~API.following 24 | ~API.following_activities 25 | ~API.me 26 | ~API.my_programs 27 | ~API.my_works 28 | ~API.records 29 | ~API.search_users 30 | ~API.set_status 31 | ~API.works 32 | -------------------------------------------------------------------------------- /docs/_gen/annict.api.API.search_users.rst: -------------------------------------------------------------------------------- 1 | annict\.api\.API\.search\_users 2 | =============================== 3 | 4 | .. currentmodule:: annict.api 5 | 6 | .. automethod:: API.search_users 7 | -------------------------------------------------------------------------------- /docs/_gen/annict.api.API.set_status.rst: -------------------------------------------------------------------------------- 1 | annict\.api\.API\.set\_status 2 | ============================= 3 | 4 | .. currentmodule:: annict.api 5 | 6 | .. automethod:: API.set_status 7 | -------------------------------------------------------------------------------- /docs/_gen/annict.api.API.works.rst: -------------------------------------------------------------------------------- 1 | annict\.api\.API\.works 2 | ======================= 3 | 4 | .. currentmodule:: annict.api 5 | 6 | .. automethod:: API.works 7 | -------------------------------------------------------------------------------- /docs/annict.rst: -------------------------------------------------------------------------------- 1 | annict package 2 | ============== 3 | 4 | Submodules 5 | ---------- 6 | 7 | annict\.api module 8 | ------------------ 9 | 10 | .. automodule:: annict.api 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | annict\.auth module 16 | ------------------- 17 | 18 | .. automodule:: annict.auth 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | annict\.cursors module 24 | ---------------------- 25 | 26 | .. automodule:: annict.cursors 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | annict\.models module 32 | --------------------- 33 | 34 | .. automodule:: annict.models 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | annict\.parsers module 40 | ---------------------- 41 | 42 | .. automodule:: annict.parsers 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | annict\.services module 48 | ----------------------- 49 | 50 | .. automodule:: annict.services 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | annict\.utils module 56 | -------------------- 57 | 58 | .. automodule:: annict.utils 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | 64 | Module contents 65 | --------------- 66 | 67 | .. automodule:: annict 68 | :members: 69 | :undoc-members: 70 | :show-inheritance: 71 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # annict documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Apr 15 04:25:07 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | import os 20 | import sys 21 | 22 | sys.path.insert(0, os.path.abspath("..")) 23 | 24 | from annict import __version__, __author__ 25 | 26 | 27 | # -- General configuration ------------------------------------------------ 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | # 31 | # needs_sphinx = '1.0' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | "sphinx.ext.autodoc", 38 | "sphinx.ext.todo", 39 | "sphinx.ext.viewcode", 40 | "sphinx.ext.autosummary", 41 | ] 42 | 43 | autosummary_generate = True 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ["_templates"] 47 | 48 | # The suffix(es) of source filenames. 49 | # You can specify multiple suffix as a list of string: 50 | # 51 | # source_suffix = ['.rst', '.md'] 52 | source_suffix = ".rst" 53 | 54 | # The master toctree document. 55 | master_doc = "index" 56 | 57 | # General information about the project. 58 | project = "annict" 59 | copyright = "2017, {}".format(__author__) 60 | author = __author__ 61 | 62 | # The version info for the project you're documenting, acts as replacement for 63 | # |version| and |release|, also used in various other places throughout the 64 | # built documents. 65 | # 66 | # The short X.Y version. 67 | version = __version__ 68 | # The full version, including alpha/beta/rc tags. 69 | release = __version__ 70 | 71 | # The language for content autogenerated by Sphinx. Refer to documentation 72 | # for a list of supported languages. 73 | # 74 | # This is also used if you do content translation via gettext catalogs. 75 | # Usually you set "language" from the command line for these cases. 76 | language = "en" 77 | 78 | # List of patterns, relative to source directory, that match files and 79 | # directories to ignore when looking for source files. 80 | # This patterns also effect to html_static_path and html_extra_path 81 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = "sphinx" 85 | 86 | # If true, `todo` and `todoList` produce output, else they produce nothing. 87 | todo_include_todos = True 88 | 89 | 90 | # -- Options for HTML output ---------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | # 95 | html_theme = "sphinx_rtd_theme" 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | # 101 | # html_theme_options = {} 102 | 103 | # Add any paths that contain custom static files (such as style sheets) here, 104 | # relative to this directory. They are copied after the builtin static files, 105 | # so a file named "default.css" will overwrite the builtin "default.css". 106 | html_static_path = ["_static"] 107 | 108 | 109 | # -- Options for HTMLHelp output ------------------------------------------ 110 | 111 | # Output file base name for HTML help builder. 112 | htmlhelp_basename = "annictdoc" 113 | 114 | 115 | # -- Options for LaTeX output --------------------------------------------- 116 | 117 | latex_elements = { 118 | # The paper size ('letterpaper' or 'a4paper'). 119 | # 120 | # 'papersize': 'letterpaper', 121 | # The font size ('10pt', '11pt' or '12pt'). 122 | # 123 | # 'pointsize': '10pt', 124 | # Additional stuff for the LaTeX preamble. 125 | # 126 | # 'preamble': '', 127 | # Latex figure (float) alignment 128 | # 129 | # 'figure_align': 'htbp', 130 | } 131 | 132 | # Grouping the document tree into LaTeX files. List of tuples 133 | # (source start file, target name, title, 134 | # author, documentclass [howto, manual, or own class]). 135 | latex_documents = [ 136 | (master_doc, "annict.tex", "annict Documentation", "Author", "manual") 137 | ] 138 | 139 | 140 | # -- Options for manual page output --------------------------------------- 141 | 142 | # One entry per manual page. List of tuples 143 | # (source start file, name, description, authors, manual section). 144 | man_pages = [(master_doc, "annict", "annict Documentation", [author], 1)] 145 | 146 | 147 | # -- Options for Texinfo output ------------------------------------------- 148 | 149 | # Grouping the document tree into Texinfo files. List of tuples 150 | # (source start file, target name, title, author, 151 | # dir menu entry, description, category) 152 | texinfo_documents = [ 153 | ( 154 | master_doc, 155 | "annict", 156 | "annict Documentation", 157 | author, 158 | "annict", 159 | "One line description of project.", 160 | "Miscellaneous", 161 | ) 162 | ] 163 | 164 | 165 | # -- Options for Epub output ---------------------------------------------- 166 | 167 | # Bibliographic Dublin Core info. 168 | epub_title = project 169 | epub_author = author 170 | epub_publisher = author 171 | epub_copyright = copyright 172 | 173 | # The unique identifier of the text. This can be a ISBN number 174 | # or the project homepage. 175 | # 176 | # epub_identifier = '' 177 | 178 | # A unique identification for the text. 179 | # 180 | # epub_uid = '' 181 | 182 | # A list of files that should not be packed into the epub file. 183 | epub_exclude_files = ["search.html"] 184 | -------------------------------------------------------------------------------- /docs/environment.yml: -------------------------------------------------------------------------------- 1 | name: annict_docs 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python>=3.6 6 | - Sphinx==1.8.2 7 | - pip: 8 | - sphinx-rtd-theme==0.4.2 9 | - arrow==0.12.1 10 | - furl==2.0.0 11 | - rauth==0.7.3 12 | - requests==2.21.0 13 | - requests-cache==0.4.13 14 | - annict==0.7.0 15 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. annict documentation master file, created by 2 | sphinx-quickstart on Sat Apr 15 04:25:07 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Annict: Annict API wrapper for Python 7 | ===================================== 8 | 9 | .. image:: https://img.shields.io/circleci/project/kk6/python-annict.svg 10 | :target: https://circleci.com/gh/kk6/python-annict 11 | 12 | .. image:: https://img.shields.io/pypi/v/annict.svg 13 | :target: https://pypi.python.org/pypi/annict 14 | 15 | Annict API wrapper for Python. 16 | 17 | .. code:: python 18 | 19 | >>> from annict.api import API 20 | >>> annict = API('your-access-token') 21 | >>> results = annict.works(filter_title="Re:ゼロから始める異世界生活") 22 | >>> print(results[0].title) 23 | Re:ゼロから始める異世界生活 24 | 25 | 26 | **annict** officially supports Python 3.6 or higher. 27 | 28 | User Guide 29 | ---------- 30 | 31 | .. toctree:: 32 | :maxdepth: 3 33 | 34 | user 35 | 36 | API Reference 37 | ------------- 38 | 39 | .. toctree:: 40 | :maxdepth: 2 41 | 42 | annict 43 | 44 | 45 | Indices and tables 46 | ================== 47 | 48 | * :ref:`genindex` 49 | * :ref:`modindex` 50 | * :ref:`search` 51 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | pushd %~dp0 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set BUILDDIR=_build 11 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 12 | set I18NSPHINXOPTS=%SPHINXOPTS% . 13 | if NOT "%PAPER%" == "" ( 14 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 15 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 16 | ) 17 | 18 | if "%1" == "" goto help 19 | 20 | if "%1" == "help" ( 21 | :help 22 | echo.Please use `make ^` where ^ is one of 23 | echo. html to make standalone HTML files 24 | echo. dirhtml to make HTML files named index.html in directories 25 | echo. singlehtml to make a single large HTML file 26 | echo. pickle to make pickle files 27 | echo. json to make JSON files 28 | echo. htmlhelp to make HTML files and an HTML help project 29 | echo. qthelp to make HTML files and a qthelp project 30 | echo. devhelp to make HTML files and a Devhelp project 31 | echo. epub to make an epub 32 | echo. epub3 to make an epub3 33 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 34 | echo. text to make text files 35 | echo. man to make manual pages 36 | echo. texinfo to make Texinfo files 37 | echo. gettext to make PO message catalogs 38 | echo. changes to make an overview over all changed/added/deprecated items 39 | echo. xml to make Docutils-native XML files 40 | echo. pseudoxml to make pseudoxml-XML files for display purposes 41 | echo. linkcheck to check all external links for integrity 42 | echo. doctest to run all doctests embedded in the documentation if enabled 43 | echo. coverage to run coverage check of the documentation if enabled 44 | echo. dummy to check syntax errors of document sources 45 | goto end 46 | ) 47 | 48 | if "%1" == "clean" ( 49 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 50 | del /q /s %BUILDDIR%\* 51 | goto end 52 | ) 53 | 54 | 55 | REM Check if sphinx-build is available and fallback to Python version if any 56 | %SPHINXBUILD% 1>NUL 2>NUL 57 | if errorlevel 9009 goto sphinx_python 58 | goto sphinx_ok 59 | 60 | :sphinx_python 61 | 62 | set SPHINXBUILD=python -m sphinx.__init__ 63 | %SPHINXBUILD% 2> nul 64 | if errorlevel 9009 ( 65 | echo. 66 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 67 | echo.installed, then set the SPHINXBUILD environment variable to point 68 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 69 | echo.may add the Sphinx directory to PATH. 70 | echo. 71 | echo.If you don't have Sphinx installed, grab it from 72 | echo.http://sphinx-doc.org/ 73 | exit /b 1 74 | ) 75 | 76 | :sphinx_ok 77 | 78 | 79 | if "%1" == "html" ( 80 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 84 | goto end 85 | ) 86 | 87 | if "%1" == "dirhtml" ( 88 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 92 | goto end 93 | ) 94 | 95 | if "%1" == "singlehtml" ( 96 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 100 | goto end 101 | ) 102 | 103 | if "%1" == "pickle" ( 104 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can process the pickle files. 108 | goto end 109 | ) 110 | 111 | if "%1" == "json" ( 112 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 113 | if errorlevel 1 exit /b 1 114 | echo. 115 | echo.Build finished; now you can process the JSON files. 116 | goto end 117 | ) 118 | 119 | if "%1" == "htmlhelp" ( 120 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 121 | if errorlevel 1 exit /b 1 122 | echo. 123 | echo.Build finished; now you can run HTML Help Workshop with the ^ 124 | .hhp project file in %BUILDDIR%/htmlhelp. 125 | goto end 126 | ) 127 | 128 | if "%1" == "qthelp" ( 129 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 133 | .qhcp project file in %BUILDDIR%/qthelp, like this: 134 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\annict.qhcp 135 | echo.To view the help file: 136 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\annict.ghc 137 | goto end 138 | ) 139 | 140 | if "%1" == "devhelp" ( 141 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. 145 | goto end 146 | ) 147 | 148 | if "%1" == "epub" ( 149 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 153 | goto end 154 | ) 155 | 156 | if "%1" == "epub3" ( 157 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 161 | goto end 162 | ) 163 | 164 | if "%1" == "latex" ( 165 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 169 | goto end 170 | ) 171 | 172 | if "%1" == "latexpdf" ( 173 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 174 | cd %BUILDDIR%/latex 175 | make all-pdf 176 | cd %~dp0 177 | echo. 178 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 179 | goto end 180 | ) 181 | 182 | if "%1" == "latexpdfja" ( 183 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 184 | cd %BUILDDIR%/latex 185 | make all-pdf-ja 186 | cd %~dp0 187 | echo. 188 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 189 | goto end 190 | ) 191 | 192 | if "%1" == "text" ( 193 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The text files are in %BUILDDIR%/text. 197 | goto end 198 | ) 199 | 200 | if "%1" == "man" ( 201 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 205 | goto end 206 | ) 207 | 208 | if "%1" == "texinfo" ( 209 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 213 | goto end 214 | ) 215 | 216 | if "%1" == "gettext" ( 217 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 218 | if errorlevel 1 exit /b 1 219 | echo. 220 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 221 | goto end 222 | ) 223 | 224 | if "%1" == "changes" ( 225 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 226 | if errorlevel 1 exit /b 1 227 | echo. 228 | echo.The overview file is in %BUILDDIR%/changes. 229 | goto end 230 | ) 231 | 232 | if "%1" == "linkcheck" ( 233 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 234 | if errorlevel 1 exit /b 1 235 | echo. 236 | echo.Link check complete; look for any errors in the above output ^ 237 | or in %BUILDDIR%/linkcheck/output.txt. 238 | goto end 239 | ) 240 | 241 | if "%1" == "doctest" ( 242 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 243 | if errorlevel 1 exit /b 1 244 | echo. 245 | echo.Testing of doctests in the sources finished, look at the ^ 246 | results in %BUILDDIR%/doctest/output.txt. 247 | goto end 248 | ) 249 | 250 | if "%1" == "coverage" ( 251 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 252 | if errorlevel 1 exit /b 1 253 | echo. 254 | echo.Testing of coverage in the sources finished, look at the ^ 255 | results in %BUILDDIR%/coverage/python.txt. 256 | goto end 257 | ) 258 | 259 | if "%1" == "xml" ( 260 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 261 | if errorlevel 1 exit /b 1 262 | echo. 263 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 264 | goto end 265 | ) 266 | 267 | if "%1" == "pseudoxml" ( 268 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 269 | if errorlevel 1 exit /b 1 270 | echo. 271 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 272 | goto end 273 | ) 274 | 275 | if "%1" == "dummy" ( 276 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 277 | if errorlevel 1 exit /b 1 278 | echo. 279 | echo.Build finished. Dummy builder generates no files. 280 | goto end 281 | ) 282 | 283 | :end 284 | popd 285 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | annict 2 | ====== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | annict 8 | -------------------------------------------------------------------------------- /docs/user.rst: -------------------------------------------------------------------------------- 1 | .. _user: 2 | 3 | User Guide 4 | ========== 5 | 6 | 7 | Installation 8 | ------------ 9 | 10 | From PyPI with the Python package manager:: 11 | 12 | pip install annict 13 | 14 | 15 | 16 | Quickstart 17 | ---------- 18 | 19 | Authentication 20 | ~~~~~~~~~~~~~~ 21 | 22 | Acquire the URL for acquiring the authentication code. 23 | 24 | .. code:: python 25 | 26 | >>> from annict.auth import OAuthHandler 27 | >>> handler = OAuthHandler(client_id='your-client-id', client_secret='your-client-secret') 28 | >>> url = handler.get_authorization_url(scope='read write') 29 | >>> print(url) 30 | 31 | Open the browser and access the URL you obtained, the authentication code will be displayed. 32 | It will be passed to the `handler.authenticate()` 's argument to get the access token. 33 | 34 | .. code:: python 35 | 36 | >>> handler.authenticate(code='your-authentication-code') 37 | >>> print(handler.get_access_token()) 38 | 39 | Note that this authentication flow is unnecessary when issuing a personal access token on Annict and using it. 40 | 41 | 42 | API 43 | ~~~ 44 | 45 | .. code:: python 46 | 47 | >>> from annict.api import API 48 | >>> annict = API('your-access-token') 49 | >>> results = annict.works(filter_title="Re:ゼロから始める異世界生活") 50 | >>> print(results[0].title) 51 | Re:ゼロから始める異世界生活 52 | 53 | The API class provides access to the entire Annict RESTful API methods. 54 | Each method can accept various parameters and return responses. 55 | For more information about these methods please refer to :ref:`API Reference `. 56 | 57 | 58 | Models 59 | ~~~~~~ 60 | 61 | When we invoke an API method most of the time returned back to us will be a Annict model class instance. 62 | This will contain the data returned from Annict which we can then use inside our application. 63 | For example the following code returns to us an User model: 64 | 65 | .. code:: python 66 | 67 | >>> user = api.me() 68 | 69 | Models contain the data and some helper methods which we can then use: 70 | 71 | .. code:: python 72 | 73 | >>> print(user.name) 74 | >>> print(user.records_count) 75 | >>> for follower in user.followers(): 76 | ... print(follower.username) 77 | 78 | 79 | Cursors 80 | ~~~~~~~ 81 | 82 | .. code:: python 83 | 84 | >>> from annict.cursors import SimpleCursor 85 | >>> for work in SimpleCursor(api.works, per_page=50, sort_id='desc').cursor(): 86 | ... print(work) 87 | ... 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /news/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | -------------------------------------------------------------------------------- /news/21.doc: -------------------------------------------------------------------------------- 1 | Added examples of using cursor. 2 | -------------------------------------------------------------------------------- /news/21.feature: -------------------------------------------------------------------------------- 1 | Added support cursor mode. 2 | -------------------------------------------------------------------------------- /news/_template.rst: -------------------------------------------------------------------------------- 1 | {% for section in sections %} 2 | {% set underline = "-" %} 3 | {% if section %} 4 | {{section}} 5 | {{ underline * section|length }}{% set underline = "~" %} 6 | 7 | {% endif %} 8 | {% if sections[section] %} 9 | {% for category, val in definitions.items() if category in sections[section] and category != 'trivial' %} 10 | 11 | {{ definitions[category]['name'] }} 12 | {{ underline * definitions[category]['name']|length }} 13 | 14 | {% if definitions[category]['showcontent'] %} 15 | {% for text, values in sections[section][category]|dictsort(by='value') %} 16 | - {{ text }}{% if category != 'vendor' and category != 'process' %} ({{ values|sort|join(', ') }}){% endif %} 17 | 18 | {% endfor %} 19 | {% else %} 20 | - {{ sections[section][category]['']|sort|join(', ') }} 21 | 22 | 23 | {% endif %} 24 | {% if sections[section][category]|length == 0 %} 25 | 26 | No significant changes. 27 | 28 | 29 | {% else %} 30 | {% endif %} 31 | {% endfor %} 32 | {% else %} 33 | 34 | No significant changes. 35 | 36 | 37 | {% endif %} 38 | {% endfor %} 39 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "alabaster" 3 | version = "0.7.12" 4 | description = "A configurable sidebar-enabled Sphinx theme" 5 | category = "dev" 6 | optional = true 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "arrow" 11 | version = "0.16.0" 12 | description = "Better dates & times for Python" 13 | category = "main" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 16 | 17 | [package.dependencies] 18 | python-dateutil = ">=2.7.0" 19 | 20 | [[package]] 21 | name = "atomicwrites" 22 | version = "1.2.1" 23 | description = "Atomic file writes." 24 | category = "dev" 25 | optional = false 26 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 27 | 28 | [[package]] 29 | name = "attrs" 30 | version = "20.3.0" 31 | description = "Classes Without Boilerplate" 32 | category = "dev" 33 | optional = false 34 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 35 | 36 | [package.extras] 37 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] 38 | docs = ["furo", "sphinx", "zope.interface"] 39 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 40 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] 41 | 42 | [[package]] 43 | name = "babel" 44 | version = "2.6.0" 45 | description = "Internationalization utilities" 46 | category = "dev" 47 | optional = true 48 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 49 | 50 | [package.dependencies] 51 | pytz = ">=0a" 52 | 53 | [[package]] 54 | name = "certifi" 55 | version = "2018.11.29" 56 | description = "Python package for providing Mozilla's CA Bundle." 57 | category = "main" 58 | optional = false 59 | python-versions = "*" 60 | 61 | [[package]] 62 | name = "cfgv" 63 | version = "2.0.0" 64 | description = "Validate configuration and produce human readable error messages." 65 | category = "dev" 66 | optional = true 67 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 68 | 69 | [package.dependencies] 70 | six = "*" 71 | 72 | [[package]] 73 | name = "chardet" 74 | version = "3.0.4" 75 | description = "Universal encoding detector for Python 2 and 3" 76 | category = "main" 77 | optional = false 78 | python-versions = "*" 79 | 80 | [[package]] 81 | name = "click" 82 | version = "7.0" 83 | description = "Composable command line interface toolkit" 84 | category = "dev" 85 | optional = true 86 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 87 | 88 | [[package]] 89 | name = "colorama" 90 | version = "0.4.1" 91 | description = "Cross-platform colored terminal text." 92 | category = "dev" 93 | optional = false 94 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 95 | 96 | [[package]] 97 | name = "coverage" 98 | version = "5.5" 99 | description = "Code coverage measurement for Python" 100 | category = "dev" 101 | optional = false 102 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 103 | 104 | [package.extras] 105 | toml = ["toml"] 106 | 107 | [[package]] 108 | name = "docutils" 109 | version = "0.14" 110 | description = "Docutils -- Python Documentation Utilities" 111 | category = "dev" 112 | optional = true 113 | python-versions = "*" 114 | 115 | [[package]] 116 | name = "filelock" 117 | version = "3.0.10" 118 | description = "A platform independent file lock." 119 | category = "dev" 120 | optional = false 121 | python-versions = "*" 122 | 123 | [[package]] 124 | name = "flake8" 125 | version = "3.6.0" 126 | description = "the modular source code checker: pep8, pyflakes and co" 127 | category = "dev" 128 | optional = false 129 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 130 | 131 | [package.dependencies] 132 | mccabe = ">=0.6.0,<0.7.0" 133 | pycodestyle = ">=2.4.0,<2.5.0" 134 | pyflakes = ">=2.0.0,<2.1.0" 135 | 136 | [[package]] 137 | name = "furl" 138 | version = "2.1.0" 139 | description = "URL manipulation made simple." 140 | category = "main" 141 | optional = false 142 | python-versions = "*" 143 | 144 | [package.dependencies] 145 | orderedmultidict = ">=1.0.1" 146 | six = ">=1.8.0" 147 | 148 | [[package]] 149 | name = "identify" 150 | version = "1.2.1" 151 | description = "File identification library for Python" 152 | category = "dev" 153 | optional = true 154 | python-versions = "*" 155 | 156 | [[package]] 157 | name = "idna" 158 | version = "2.8" 159 | description = "Internationalized Domain Names in Applications (IDNA)" 160 | category = "main" 161 | optional = false 162 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 163 | 164 | [[package]] 165 | name = "imagesize" 166 | version = "1.1.0" 167 | description = "Getting image size from png/jpeg/jpeg2000/gif file" 168 | category = "dev" 169 | optional = true 170 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 171 | 172 | [[package]] 173 | name = "importlib-metadata" 174 | version = "0.18" 175 | description = "Read metadata from Python packages" 176 | category = "dev" 177 | optional = false 178 | python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" 179 | 180 | [package.dependencies] 181 | zipp = ">=0.5" 182 | 183 | [package.extras] 184 | docs = ["sphinx", "docutils (==0.12)", "rst.linker"] 185 | 186 | [[package]] 187 | name = "importlib-resources" 188 | version = "1.0.2" 189 | description = "Read resources from Python packages" 190 | category = "dev" 191 | optional = true 192 | python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" 193 | 194 | [[package]] 195 | name = "incremental" 196 | version = "17.5.0" 197 | description = "" 198 | category = "dev" 199 | optional = true 200 | python-versions = "*" 201 | 202 | [package.extras] 203 | scripts = ["click (>=6.0)", "twisted (>=16.4.0)"] 204 | 205 | [[package]] 206 | name = "iniconfig" 207 | version = "1.0.0" 208 | description = "iniconfig: brain-dead simple config-ini parsing" 209 | category = "dev" 210 | optional = false 211 | python-versions = "*" 212 | 213 | [[package]] 214 | name = "jinja2" 215 | version = "2.11.3" 216 | description = "A very fast and expressive template engine." 217 | category = "dev" 218 | optional = true 219 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 220 | 221 | [package.dependencies] 222 | MarkupSafe = ">=0.23" 223 | 224 | [package.extras] 225 | i18n = ["Babel (>=0.8)"] 226 | 227 | [[package]] 228 | name = "markupsafe" 229 | version = "1.1.0" 230 | description = "Safely add untrusted strings to HTML/XML markup." 231 | category = "dev" 232 | optional = true 233 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" 234 | 235 | [[package]] 236 | name = "mccabe" 237 | version = "0.6.1" 238 | description = "McCabe checker, plugin for flake8" 239 | category = "dev" 240 | optional = false 241 | python-versions = "*" 242 | 243 | [[package]] 244 | name = "nodeenv" 245 | version = "1.3.3" 246 | description = "Node.js virtual environment builder" 247 | category = "dev" 248 | optional = true 249 | python-versions = "*" 250 | 251 | [[package]] 252 | name = "orderedmultidict" 253 | version = "1.0.1" 254 | description = "Ordered Multivalue Dictionary" 255 | category = "main" 256 | optional = false 257 | python-versions = "*" 258 | 259 | [package.dependencies] 260 | six = ">=1.8.0" 261 | 262 | [[package]] 263 | name = "packaging" 264 | version = "18.0" 265 | description = "Core utilities for Python packages" 266 | category = "dev" 267 | optional = false 268 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 269 | 270 | [package.dependencies] 271 | pyparsing = ">=2.0.2" 272 | six = "*" 273 | 274 | [[package]] 275 | name = "pluggy" 276 | version = "0.12.0" 277 | description = "plugin and hook calling mechanisms for python" 278 | category = "dev" 279 | optional = false 280 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 281 | 282 | [package.dependencies] 283 | importlib-metadata = ">=0.12" 284 | 285 | [package.extras] 286 | dev = ["pre-commit", "tox"] 287 | 288 | [[package]] 289 | name = "pre-commit" 290 | version = "2.1.1" 291 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 292 | category = "dev" 293 | optional = true 294 | python-versions = ">=3.6" 295 | 296 | [package.dependencies] 297 | cfgv = ">=2.0.0" 298 | identify = ">=1.0.0" 299 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 300 | importlib-resources = {version = "*", markers = "python_version < \"3.7\""} 301 | nodeenv = ">=0.11.1" 302 | pyyaml = ">=5.1" 303 | toml = "*" 304 | virtualenv = ">=15.2" 305 | 306 | [[package]] 307 | name = "py" 308 | version = "1.10.0" 309 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 310 | category = "dev" 311 | optional = false 312 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 313 | 314 | [[package]] 315 | name = "pycodestyle" 316 | version = "2.4.0" 317 | description = "Python style guide checker" 318 | category = "dev" 319 | optional = false 320 | python-versions = "*" 321 | 322 | [[package]] 323 | name = "pyflakes" 324 | version = "2.0.0" 325 | description = "passive checker of Python programs" 326 | category = "dev" 327 | optional = false 328 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 329 | 330 | [[package]] 331 | name = "pygments" 332 | version = "2.3.1" 333 | description = "Pygments is a syntax highlighting package written in Python." 334 | category = "dev" 335 | optional = true 336 | python-versions = "*" 337 | 338 | [[package]] 339 | name = "pyparsing" 340 | version = "2.3.1" 341 | description = "Python parsing module" 342 | category = "dev" 343 | optional = false 344 | python-versions = "*" 345 | 346 | [[package]] 347 | name = "pytest" 348 | version = "6.2.3" 349 | description = "pytest: simple powerful testing with Python" 350 | category = "dev" 351 | optional = false 352 | python-versions = ">=3.6" 353 | 354 | [package.dependencies] 355 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 356 | attrs = ">=19.2.0" 357 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 358 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 359 | iniconfig = "*" 360 | packaging = "*" 361 | pluggy = ">=0.12,<1.0.0a1" 362 | py = ">=1.8.2" 363 | toml = "*" 364 | 365 | [package.extras] 366 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 367 | 368 | [[package]] 369 | name = "pytest-cov" 370 | version = "2.11.1" 371 | description = "Pytest plugin for measuring coverage." 372 | category = "dev" 373 | optional = false 374 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 375 | 376 | [package.dependencies] 377 | coverage = ">=5.2.1" 378 | pytest = ">=4.6" 379 | 380 | [package.extras] 381 | testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", "virtualenv"] 382 | 383 | [[package]] 384 | name = "pytest-flake8" 385 | version = "1.0.7" 386 | description = "pytest plugin to check FLAKE8 requirements" 387 | category = "dev" 388 | optional = false 389 | python-versions = "*" 390 | 391 | [package.dependencies] 392 | flake8 = ">=3.5" 393 | pytest = ">=3.5" 394 | 395 | [[package]] 396 | name = "pytest-runner" 397 | version = "5.3.0" 398 | description = "Invoke py.test as distutils command with dependency resolution" 399 | category = "dev" 400 | optional = false 401 | python-versions = ">=3.6" 402 | 403 | [package.extras] 404 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 405 | testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "pytest-virtualenv", "pytest-black (>=0.3.7)", "pytest-mypy"] 406 | 407 | [[package]] 408 | name = "python-dateutil" 409 | version = "2.7.5" 410 | description = "Extensions to the standard Python datetime module" 411 | category = "main" 412 | optional = false 413 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 414 | 415 | [package.dependencies] 416 | six = ">=1.5" 417 | 418 | [[package]] 419 | name = "pytz" 420 | version = "2018.9" 421 | description = "World timezone definitions, modern and historical" 422 | category = "dev" 423 | optional = true 424 | python-versions = "*" 425 | 426 | [[package]] 427 | name = "pyyaml" 428 | version = "5.3.1" 429 | description = "YAML parser and emitter for Python" 430 | category = "dev" 431 | optional = false 432 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 433 | 434 | [[package]] 435 | name = "rauth" 436 | version = "0.7.3" 437 | description = "A Python library for OAuth 1.0/a, 2.0, and Ofly." 438 | category = "main" 439 | optional = false 440 | python-versions = "*" 441 | 442 | [package.dependencies] 443 | requests = ">=1.2.3" 444 | 445 | [[package]] 446 | name = "requests" 447 | version = "2.25.1" 448 | description = "Python HTTP for Humans." 449 | category = "main" 450 | optional = false 451 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 452 | 453 | [package.dependencies] 454 | certifi = ">=2017.4.17" 455 | chardet = ">=3.0.2,<5" 456 | idna = ">=2.5,<3" 457 | urllib3 = ">=1.21.1,<1.27" 458 | 459 | [package.extras] 460 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 461 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 462 | 463 | [[package]] 464 | name = "requests-cache" 465 | version = "0.5.2" 466 | description = "Persistent cache for requests library" 467 | category = "main" 468 | optional = false 469 | python-versions = "*" 470 | 471 | [package.dependencies] 472 | requests = ">=1.1.0" 473 | 474 | [[package]] 475 | name = "responses" 476 | version = "0.10.15" 477 | description = "A utility library for mocking out the `requests` Python library." 478 | category = "dev" 479 | optional = false 480 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 481 | 482 | [package.dependencies] 483 | requests = ">=2.0" 484 | six = "*" 485 | 486 | [package.extras] 487 | tests = ["coverage (>=3.7.1,<5.0.0)", "pytest-cov", "pytest-localserver", "flake8", "pytest (>=4.6,<5.0)", "pytest"] 488 | 489 | [[package]] 490 | name = "six" 491 | version = "1.14.0" 492 | description = "Python 2 and 3 compatibility utilities" 493 | category = "main" 494 | optional = false 495 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 496 | 497 | [[package]] 498 | name = "snowballstemmer" 499 | version = "1.2.1" 500 | description = "This package provides 16 stemmer algorithms (15 + Poerter English stemmer) generated from Snowball algorithms." 501 | category = "dev" 502 | optional = true 503 | python-versions = "*" 504 | 505 | [[package]] 506 | name = "sphinx" 507 | version = "3.1.2" 508 | description = "Python documentation generator" 509 | category = "dev" 510 | optional = true 511 | python-versions = ">=3.5" 512 | 513 | [package.dependencies] 514 | alabaster = ">=0.7,<0.8" 515 | babel = ">=1.3" 516 | colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} 517 | docutils = ">=0.12" 518 | imagesize = "*" 519 | Jinja2 = ">=2.3" 520 | packaging = "*" 521 | Pygments = ">=2.0" 522 | requests = ">=2.5.0" 523 | snowballstemmer = ">=1.1" 524 | sphinxcontrib-applehelp = "*" 525 | sphinxcontrib-devhelp = "*" 526 | sphinxcontrib-htmlhelp = "*" 527 | sphinxcontrib-jsmath = "*" 528 | sphinxcontrib-qthelp = "*" 529 | sphinxcontrib-serializinghtml = "*" 530 | 531 | [package.extras] 532 | docs = ["sphinxcontrib-websupport"] 533 | lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.780)", "docutils-stubs"] 534 | test = ["pytest", "pytest-cov", "html5lib", "typed-ast", "cython"] 535 | 536 | [[package]] 537 | name = "sphinx-rtd-theme" 538 | version = "0.5.0" 539 | description = "Read the Docs theme for Sphinx" 540 | category = "dev" 541 | optional = true 542 | python-versions = "*" 543 | 544 | [package.dependencies] 545 | sphinx = "*" 546 | 547 | [package.extras] 548 | dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] 549 | 550 | [[package]] 551 | name = "sphinxcontrib-applehelp" 552 | version = "1.0.1" 553 | description = "" 554 | category = "dev" 555 | optional = true 556 | python-versions = "*" 557 | 558 | [package.extras] 559 | test = ["pytest", "flake8", "mypy"] 560 | 561 | [[package]] 562 | name = "sphinxcontrib-devhelp" 563 | version = "1.0.1" 564 | description = "" 565 | category = "dev" 566 | optional = true 567 | python-versions = "*" 568 | 569 | [package.extras] 570 | test = ["pytest", "flake8", "mypy"] 571 | 572 | [[package]] 573 | name = "sphinxcontrib-htmlhelp" 574 | version = "1.0.1" 575 | description = "" 576 | category = "dev" 577 | optional = true 578 | python-versions = "*" 579 | 580 | [package.extras] 581 | test = ["pytest", "flake8", "mypy", "html5lib"] 582 | 583 | [[package]] 584 | name = "sphinxcontrib-jsmath" 585 | version = "1.0.1" 586 | description = "A sphinx extension which renders display math in HTML via JavaScript" 587 | category = "dev" 588 | optional = true 589 | python-versions = ">=3.5" 590 | 591 | [package.extras] 592 | test = ["pytest", "flake8", "mypy"] 593 | 594 | [[package]] 595 | name = "sphinxcontrib-qthelp" 596 | version = "1.0.2" 597 | description = "" 598 | category = "dev" 599 | optional = true 600 | python-versions = "*" 601 | 602 | [package.extras] 603 | test = ["pytest", "flake8", "mypy"] 604 | 605 | [[package]] 606 | name = "sphinxcontrib-serializinghtml" 607 | version = "1.1.1" 608 | description = "" 609 | category = "dev" 610 | optional = true 611 | python-versions = "*" 612 | 613 | [package.extras] 614 | test = ["pytest", "flake8", "mypy"] 615 | 616 | [[package]] 617 | name = "toml" 618 | version = "0.10.0" 619 | description = "Python Library for Tom's Obvious, Minimal Language" 620 | category = "dev" 621 | optional = false 622 | python-versions = "*" 623 | 624 | [[package]] 625 | name = "towncrier" 626 | version = "19.2.0" 627 | description = "Building newsfiles for your project." 628 | category = "dev" 629 | optional = true 630 | python-versions = "*" 631 | 632 | [package.dependencies] 633 | Click = "*" 634 | incremental = "*" 635 | jinja2 = "*" 636 | toml = "*" 637 | 638 | [[package]] 639 | name = "tox" 640 | version = "3.19.0" 641 | description = "tox is a generic virtualenv management and test command line tool" 642 | category = "dev" 643 | optional = false 644 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 645 | 646 | [package.dependencies] 647 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 648 | filelock = ">=3.0.0" 649 | importlib-metadata = {version = ">=0.12,<2", markers = "python_version < \"3.8\""} 650 | packaging = ">=14" 651 | pluggy = ">=0.12.0" 652 | py = ">=1.4.17" 653 | six = ">=1.14.0" 654 | toml = ">=0.9.4" 655 | virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" 656 | 657 | [package.extras] 658 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 659 | testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)"] 660 | 661 | [[package]] 662 | name = "urllib3" 663 | version = "1.24.2" 664 | description = "HTTP library with thread-safe connection pooling, file post, and more." 665 | category = "main" 666 | optional = false 667 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" 668 | 669 | [package.extras] 670 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 671 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 672 | 673 | [[package]] 674 | name = "virtualenv" 675 | version = "16.2.0" 676 | description = "Virtual Python Environment builder" 677 | category = "dev" 678 | optional = false 679 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 680 | 681 | [package.extras] 682 | docs = ["sphinx (>=1.8.0,<2)", "towncrier (>=18.5.0)", "sphinx-rtd-theme (>=0.4.2,<1)"] 683 | testing = ["pytest (>=4.0.0,<5)", "coverage (>=4.5.0,<5)", "six (>=1.10.0,<2)", "pytest-timeout (>=1.3.0,<2)", "pytest-xdist", "mock", "xonsh"] 684 | 685 | [[package]] 686 | name = "zipp" 687 | version = "0.5.2" 688 | description = "Backport of pathlib-compatible object wrapper for zip files" 689 | category = "dev" 690 | optional = false 691 | python-versions = ">=2.7" 692 | 693 | [package.extras] 694 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 695 | testing = ["pathlib2", "contextlib2", "unittest2"] 696 | 697 | [extras] 698 | doc = [] 699 | news = [] 700 | pre-commit = [] 701 | travis-ci = [] 702 | 703 | [metadata] 704 | lock-version = "1.1" 705 | python-versions = "^3.6" 706 | content-hash = "a18078e1b4a0363f1c0171e88d27f24b80e5a9e8d460836525b282702be6458f" 707 | 708 | [metadata.files] 709 | alabaster = [ 710 | {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, 711 | {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, 712 | ] 713 | arrow = [ 714 | {file = "arrow-0.16.0-py2.py3-none-any.whl", hash = "sha256:98184d8dd3e5d30b96c2df4596526f7de679ccb467f358b82b0f686436f3a6b8"}, 715 | {file = "arrow-0.16.0.tar.gz", hash = "sha256:92aac856ea5175c804f7ccb96aca4d714d936f1c867ba59d747a8096ec30e90a"}, 716 | ] 717 | atomicwrites = [ 718 | {file = "atomicwrites-1.2.1-py2.py3-none-any.whl", hash = "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0"}, 719 | {file = "atomicwrites-1.2.1.tar.gz", hash = "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"}, 720 | ] 721 | attrs = [ 722 | {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, 723 | {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, 724 | ] 725 | babel = [ 726 | {file = "Babel-2.6.0-py2.py3-none-any.whl", hash = "sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669"}, 727 | {file = "Babel-2.6.0.tar.gz", hash = "sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"}, 728 | ] 729 | certifi = [ 730 | {file = "certifi-2018.11.29-py2.py3-none-any.whl", hash = "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"}, 731 | {file = "certifi-2018.11.29.tar.gz", hash = "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7"}, 732 | ] 733 | cfgv = [ 734 | {file = "cfgv-2.0.0-py2.py3-none-any.whl", hash = "sha256:3bd31385cd2bebddbba8012200aaf15aa208539f1b33973759b4d02fc2148da5"}, 735 | {file = "cfgv-2.0.0.tar.gz", hash = "sha256:32edbe09de6f4521224b87822103a8c16a614d31a894735f7a5b3bcf0eb3c37e"}, 736 | ] 737 | chardet = [ 738 | {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, 739 | {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, 740 | ] 741 | click = [ 742 | {file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"}, 743 | {file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"}, 744 | ] 745 | colorama = [ 746 | {file = "colorama-0.4.1-py2.py3-none-any.whl", hash = "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"}, 747 | {file = "colorama-0.4.1.tar.gz", hash = "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d"}, 748 | ] 749 | coverage = [ 750 | {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, 751 | {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, 752 | {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, 753 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, 754 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, 755 | {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, 756 | {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, 757 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, 758 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, 759 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, 760 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, 761 | {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, 762 | {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, 763 | {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, 764 | {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, 765 | {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, 766 | {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, 767 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, 768 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, 769 | {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, 770 | {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, 771 | {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, 772 | {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, 773 | {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, 774 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, 775 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, 776 | {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, 777 | {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, 778 | {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, 779 | {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, 780 | {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, 781 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, 782 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, 783 | {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, 784 | {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, 785 | {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, 786 | {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, 787 | {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, 788 | {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, 789 | {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, 790 | {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, 791 | {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, 792 | {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, 793 | {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, 794 | {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, 795 | {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, 796 | {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, 797 | {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, 798 | {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, 799 | {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, 800 | {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, 801 | {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, 802 | ] 803 | docutils = [ 804 | {file = "docutils-0.14-py2-none-any.whl", hash = "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"}, 805 | {file = "docutils-0.14-py3-none-any.whl", hash = "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6"}, 806 | {file = "docutils-0.14.tar.gz", hash = "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274"}, 807 | ] 808 | filelock = [ 809 | {file = "filelock-3.0.10-py3-none-any.whl", hash = "sha256:b8d5ca5ca1c815e1574aee746650ea7301de63d87935b3463d26368b76e31633"}, 810 | {file = "filelock-3.0.10.tar.gz", hash = "sha256:d610c1bb404daf85976d7a82eb2ada120f04671007266b708606565dd03b5be6"}, 811 | ] 812 | flake8 = [ 813 | {file = "flake8-3.6.0-py2.py3-none-any.whl", hash = "sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2"}, 814 | {file = "flake8-3.6.0.tar.gz", hash = "sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670"}, 815 | ] 816 | furl = [ 817 | {file = "furl-2.1.0-py2.py3-none-any.whl", hash = "sha256:f4d6f1e5479c376a5b7bdc62795d736d8c1b2a754f366a2ad2816e46e946e22e"}, 818 | {file = "furl-2.1.0.tar.gz", hash = "sha256:c0e0231a1feee2acd256574b7033df3144775451c610cb587060d6a0d7e0b621"}, 819 | ] 820 | identify = [ 821 | {file = "identify-1.2.1-py2.py3-none-any.whl", hash = "sha256:1cf14bc0324d83a742f558051db0c2cbe15d8b9ae1c59dfefbe38935f1d1ee31"}, 822 | {file = "identify-1.2.1.tar.gz", hash = "sha256:0749c74180ef0f6a3874eaa0bf89a6990a523233180e83e6f3c7c27312ac9ba3"}, 823 | ] 824 | idna = [ 825 | {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, 826 | {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, 827 | ] 828 | imagesize = [ 829 | {file = "imagesize-1.1.0-py2.py3-none-any.whl", hash = "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8"}, 830 | {file = "imagesize-1.1.0.tar.gz", hash = "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"}, 831 | ] 832 | importlib-metadata = [ 833 | {file = "importlib_metadata-0.18-py2.py3-none-any.whl", hash = "sha256:6dfd58dfe281e8d240937776065dd3624ad5469c835248219bd16cf2e12dbeb7"}, 834 | {file = "importlib_metadata-0.18.tar.gz", hash = "sha256:cb6ee23b46173539939964df59d3d72c3e0c1b5d54b84f1d8a7e912fe43612db"}, 835 | ] 836 | importlib-resources = [ 837 | {file = "importlib_resources-1.0.2-py2.py3-none-any.whl", hash = "sha256:6e2783b2538bd5a14678284a3962b0660c715e5a0f10243fd5e00a4b5974f50b"}, 838 | {file = "importlib_resources-1.0.2.tar.gz", hash = "sha256:d3279fd0f6f847cced9f7acc19bd3e5df54d34f93a2e7bb5f238f81545787078"}, 839 | ] 840 | incremental = [ 841 | {file = "incremental-17.5.0-py2.py3-none-any.whl", hash = "sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f"}, 842 | {file = "incremental-17.5.0.tar.gz", hash = "sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3"}, 843 | ] 844 | iniconfig = [ 845 | {file = "iniconfig-1.0.0.tar.gz", hash = "sha256:aa0b40f50a00e72323cb5d41302f9c6165728fd764ac8822aa3fff00a40d56b4"}, 846 | ] 847 | jinja2 = [ 848 | {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, 849 | {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, 850 | ] 851 | markupsafe = [ 852 | {file = "MarkupSafe-1.1.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6"}, 853 | {file = "MarkupSafe-1.1.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7"}, 854 | {file = "MarkupSafe-1.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c"}, 855 | {file = "MarkupSafe-1.1.0-cp27-cp27m-win32.whl", hash = "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7"}, 856 | {file = "MarkupSafe-1.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6"}, 857 | {file = "MarkupSafe-1.1.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36"}, 858 | {file = "MarkupSafe-1.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd"}, 859 | {file = "MarkupSafe-1.1.0-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9"}, 860 | {file = "MarkupSafe-1.1.0-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550"}, 861 | {file = "MarkupSafe-1.1.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"}, 862 | {file = "MarkupSafe-1.1.0-cp34-cp34m-win32.whl", hash = "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d"}, 863 | {file = "MarkupSafe-1.1.0-cp34-cp34m-win_amd64.whl", hash = "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401"}, 864 | {file = "MarkupSafe-1.1.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1"}, 865 | {file = "MarkupSafe-1.1.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c"}, 866 | {file = "MarkupSafe-1.1.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b"}, 867 | {file = "MarkupSafe-1.1.0-cp35-cp35m-win32.whl", hash = "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e"}, 868 | {file = "MarkupSafe-1.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856"}, 869 | {file = "MarkupSafe-1.1.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492"}, 870 | {file = "MarkupSafe-1.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432"}, 871 | {file = "MarkupSafe-1.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c"}, 872 | {file = "MarkupSafe-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b"}, 873 | {file = "MarkupSafe-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2"}, 874 | {file = "MarkupSafe-1.1.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd"}, 875 | {file = "MarkupSafe-1.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af"}, 876 | {file = "MarkupSafe-1.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672"}, 877 | {file = "MarkupSafe-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834"}, 878 | {file = "MarkupSafe-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1"}, 879 | {file = "MarkupSafe-1.1.0.tar.gz", hash = "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3"}, 880 | ] 881 | mccabe = [ 882 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 883 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 884 | ] 885 | nodeenv = [ 886 | {file = "nodeenv-1.3.3.tar.gz", hash = "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a"}, 887 | ] 888 | orderedmultidict = [ 889 | {file = "orderedmultidict-1.0.1-py2.py3-none-any.whl", hash = "sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3"}, 890 | {file = "orderedmultidict-1.0.1.tar.gz", hash = "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad"}, 891 | ] 892 | packaging = [ 893 | {file = "packaging-18.0-py2.py3-none-any.whl", hash = "sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9"}, 894 | {file = "packaging-18.0.tar.gz", hash = "sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807"}, 895 | ] 896 | pluggy = [ 897 | {file = "pluggy-0.12.0-py2.py3-none-any.whl", hash = "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c"}, 898 | {file = "pluggy-0.12.0.tar.gz", hash = "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc"}, 899 | ] 900 | pre-commit = [ 901 | {file = "pre_commit-2.1.1-py2.py3-none-any.whl", hash = "sha256:09ebe467f43ce24377f8c2f200fe3cd2570d328eb2ce0568c8e96ce19da45fa6"}, 902 | {file = "pre_commit-2.1.1.tar.gz", hash = "sha256:f8d555e31e2051892c7f7b3ad9f620bd2c09271d87e9eedb2ad831737d6211eb"}, 903 | ] 904 | py = [ 905 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 906 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 907 | ] 908 | pycodestyle = [ 909 | {file = "pycodestyle-2.4.0-py2.py3-none-any.whl", hash = "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83"}, 910 | {file = "pycodestyle-2.4.0-py3.6.egg", hash = "sha256:74abc4e221d393ea5ce1f129ea6903209940c1ecd29e002e8c6933c2b21026e0"}, 911 | {file = "pycodestyle-2.4.0.tar.gz", hash = "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a"}, 912 | ] 913 | pyflakes = [ 914 | {file = "pyflakes-2.0.0-py2.py3-none-any.whl", hash = "sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae"}, 915 | {file = "pyflakes-2.0.0.tar.gz", hash = "sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49"}, 916 | ] 917 | pygments = [ 918 | {file = "Pygments-2.3.1-py2.py3-none-any.whl", hash = "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"}, 919 | {file = "Pygments-2.3.1.tar.gz", hash = "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a"}, 920 | ] 921 | pyparsing = [ 922 | {file = "pyparsing-2.3.1-py2.py3-none-any.whl", hash = "sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3"}, 923 | {file = "pyparsing-2.3.1.tar.gz", hash = "sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a"}, 924 | ] 925 | pytest = [ 926 | {file = "pytest-6.2.3-py3-none-any.whl", hash = "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"}, 927 | {file = "pytest-6.2.3.tar.gz", hash = "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634"}, 928 | ] 929 | pytest-cov = [ 930 | {file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"}, 931 | {file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"}, 932 | ] 933 | pytest-flake8 = [ 934 | {file = "pytest-flake8-1.0.7.tar.gz", hash = "sha256:f0259761a903563f33d6f099914afef339c085085e643bee8343eb323b32dd6b"}, 935 | {file = "pytest_flake8-1.0.7-py2.py3-none-any.whl", hash = "sha256:c28cf23e7d359753c896745fd4ba859495d02e16c84bac36caa8b1eec58f5bc1"}, 936 | ] 937 | pytest-runner = [ 938 | {file = "pytest-runner-5.3.0.tar.gz", hash = "sha256:ca3f58ff4957e8be6c54c55d575b235725cbbcf4dc0d5091c29c6444cfc8a5fe"}, 939 | {file = "pytest_runner-5.3.0-py3-none-any.whl", hash = "sha256:448959d9ada752de2b369cf05c1c0f9e6d2027e7d32441187c16c24c1d4d6e77"}, 940 | ] 941 | python-dateutil = [ 942 | {file = "python-dateutil-2.7.5.tar.gz", hash = "sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02"}, 943 | {file = "python_dateutil-2.7.5-py2.py3-none-any.whl", hash = "sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93"}, 944 | ] 945 | pytz = [ 946 | {file = "pytz-2018.9-py2.py3-none-any.whl", hash = "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9"}, 947 | {file = "pytz-2018.9.tar.gz", hash = "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"}, 948 | ] 949 | pyyaml = [ 950 | {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, 951 | {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, 952 | {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, 953 | {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, 954 | {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, 955 | {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, 956 | {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, 957 | {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, 958 | {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, 959 | {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, 960 | {file = "PyYAML-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a"}, 961 | {file = "PyYAML-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e"}, 962 | {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, 963 | ] 964 | rauth = [ 965 | {file = "rauth-0.7.3-py2-none-any.whl", hash = "sha256:b18590fbd77bc3d871936bbdb851377d1b0c08e337b219c303f8fc2b5a42ef2d"}, 966 | {file = "rauth-0.7.3.tar.gz", hash = "sha256:524cdbc1c28560eacfc9a9d40c59525eb8d00fdf07fbad86107ea24411477b0a"}, 967 | ] 968 | requests = [ 969 | {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, 970 | {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, 971 | ] 972 | requests-cache = [ 973 | {file = "requests-cache-0.5.2.tar.gz", hash = "sha256:813023269686045f8e01e2289cc1e7e9ae5ab22ddd1e2849a9093ab3ab7270eb"}, 974 | {file = "requests_cache-0.5.2-py2.py3-none-any.whl", hash = "sha256:81e13559baee64677a7d73b85498a5a8f0639e204517b5d05ff378e44a57831a"}, 975 | ] 976 | responses = [ 977 | {file = "responses-0.10.15-py2.py3-none-any.whl", hash = "sha256:af94d28cdfb48ded0ad82a5216616631543650f440334a693479b8991a6594a2"}, 978 | {file = "responses-0.10.15.tar.gz", hash = "sha256:7bb697a5fedeb41d81e8b87f152d453d5cab42dcd1691b6a7d6097e94d33f373"}, 979 | ] 980 | six = [ 981 | {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, 982 | {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, 983 | ] 984 | snowballstemmer = [ 985 | {file = "snowballstemmer-1.2.1-py2.py3-none-any.whl", hash = "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"}, 986 | {file = "snowballstemmer-1.2.1.tar.gz", hash = "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128"}, 987 | ] 988 | sphinx = [ 989 | {file = "Sphinx-3.1.2-py3-none-any.whl", hash = "sha256:97dbf2e31fc5684bb805104b8ad34434ed70e6c588f6896991b2fdfd2bef8c00"}, 990 | {file = "Sphinx-3.1.2.tar.gz", hash = "sha256:b9daeb9b39aa1ffefc2809b43604109825300300b987a24f45976c001ba1a8fd"}, 991 | ] 992 | sphinx-rtd-theme = [ 993 | {file = "sphinx_rtd_theme-0.5.0-py2.py3-none-any.whl", hash = "sha256:373413d0f82425aaa28fb288009bf0d0964711d347763af2f1b65cafcb028c82"}, 994 | {file = "sphinx_rtd_theme-0.5.0.tar.gz", hash = "sha256:22c795ba2832a169ca301cd0a083f7a434e09c538c70beb42782c073651b707d"}, 995 | ] 996 | sphinxcontrib-applehelp = [ 997 | {file = "sphinxcontrib-applehelp-1.0.1.tar.gz", hash = "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897"}, 998 | {file = "sphinxcontrib_applehelp-1.0.1-py2.py3-none-any.whl", hash = "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"}, 999 | ] 1000 | sphinxcontrib-devhelp = [ 1001 | {file = "sphinxcontrib-devhelp-1.0.1.tar.gz", hash = "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34"}, 1002 | {file = "sphinxcontrib_devhelp-1.0.1-py2.py3-none-any.whl", hash = "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"}, 1003 | ] 1004 | sphinxcontrib-htmlhelp = [ 1005 | {file = "sphinxcontrib-htmlhelp-1.0.1.tar.gz", hash = "sha256:0d691ca8edf5995fbacfe69b191914256071a94cbad03c3688dca47385c9206c"}, 1006 | {file = "sphinxcontrib_htmlhelp-1.0.1-py2.py3-none-any.whl", hash = "sha256:e31c8271f5a8f04b620a500c0442a7d5cfc1a732fa5c10ec363f90fe72af0cb8"}, 1007 | ] 1008 | sphinxcontrib-jsmath = [ 1009 | {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, 1010 | {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, 1011 | ] 1012 | sphinxcontrib-qthelp = [ 1013 | {file = "sphinxcontrib-qthelp-1.0.2.tar.gz", hash = "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"}, 1014 | {file = "sphinxcontrib_qthelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20"}, 1015 | ] 1016 | sphinxcontrib-serializinghtml = [ 1017 | {file = "sphinxcontrib-serializinghtml-1.1.1.tar.gz", hash = "sha256:392187ac558863b8aff0d76dc78e0731fed58f3b06e2b00e22995dcdb630f213"}, 1018 | {file = "sphinxcontrib_serializinghtml-1.1.1-py2.py3-none-any.whl", hash = "sha256:01d9b2617d7e8ddf7a00cae091f08f9fa4db587cc160b493141ee56710810932"}, 1019 | ] 1020 | toml = [ 1021 | {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, 1022 | {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, 1023 | {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, 1024 | ] 1025 | towncrier = [ 1026 | {file = "towncrier-19.2.0-py2.py3-none-any.whl", hash = "sha256:de19da8b8cb44f18ea7ed3a3823087d2af8fcf497151bb9fd1e1b092ff56ed8d"}, 1027 | {file = "towncrier-19.2.0.tar.gz", hash = "sha256:48251a1ae66d2cf7e6fa5552016386831b3e12bb3b2d08eb70374508c17a8196"}, 1028 | ] 1029 | tox = [ 1030 | {file = "tox-3.19.0-py2.py3-none-any.whl", hash = "sha256:3d94b6921a0b6dc90fd8128df83741f30bb41ccd6cd52d131a6a6944ca8f16e6"}, 1031 | {file = "tox-3.19.0.tar.gz", hash = "sha256:17e61a93afe5c49281fb969ab71f7a3f22d7586d1c56f9a74219910f356fe7d3"}, 1032 | ] 1033 | urllib3 = [ 1034 | {file = "urllib3-1.24.2-py2.py3-none-any.whl", hash = "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0"}, 1035 | {file = "urllib3-1.24.2.tar.gz", hash = "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3"}, 1036 | ] 1037 | virtualenv = [ 1038 | {file = "virtualenv-16.2.0-py2.py3-none-any.whl", hash = "sha256:34b9ae3742abed2f95d3970acf4d80533261d6061b51160b197f84e5b4c98b4c"}, 1039 | {file = "virtualenv-16.2.0.tar.gz", hash = "sha256:fa736831a7b18bd2bfeef746beb622a92509e9733d645952da136b0639cd40cd"}, 1040 | ] 1041 | zipp = [ 1042 | {file = "zipp-0.5.2-py2.py3-none-any.whl", hash = "sha256:8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec"}, 1043 | {file = "zipp-0.5.2.tar.gz", hash = "sha256:4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a"}, 1044 | ] 1045 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry>=0.12"] 3 | build-backend = "poetry.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "annict" 7 | version = "0.7.0" 8 | description = "Annict API for python" 9 | readme = "README.md" 10 | authors = ["kk6 "] 11 | homepage = "https://annict.jp/userland/projects/7" 12 | repository = "https://github.com/kk6/python-annict" 13 | documentation = "https://python-annict.readthedocs.io/en/latest/" 14 | license = "MIT" 15 | classifiers = [ 16 | "Development Status :: 3 - Alpha", 17 | "Intended Audience :: Developers", 18 | "License :: OSI Approved :: MIT License", 19 | "Programming Language :: Python", 20 | 'Programming Language :: Python :: 3', 21 | "Programming Language :: Python :: 3.6", 22 | "Programming Language :: Python :: 3.7", 23 | "Topic :: Software Development", 24 | "Topic :: Software Development :: Libraries" 25 | ] 26 | 27 | [tool.poetry.dependencies] 28 | python = "^3.6" 29 | rauth = "^0.7.3" 30 | requests = "^2.21" 31 | requests-cache = ">=0.4.13,<0.6.0" 32 | furl = "^2.0" 33 | arrow = ">=0.12.1,<0.17.0" 34 | 35 | [tool.poetry.dev-dependencies] 36 | pytest = "^6.2" 37 | pytest-cov = "^2.11" 38 | pytest-flake8 = "^1.0" 39 | responses = "^0.10.15" 40 | pytest-runner = "^5.3" 41 | towncrier = {version = "^19.2", optional = true} 42 | tox = "^3.19" 43 | sphinx = {version = "^3.1", optional = true} 44 | sphinx_rtd_theme = {version = "^0.5.0", optional = true} 45 | pre-commit = {version = "^2.1", optional = true} 46 | 47 | [tool.poetry.extras] 48 | travis-ci = ["codecov"] 49 | doc = [ 50 | "sphinx", 51 | "sphinx_rtd_theme", 52 | ] 53 | pre-commit = ["pre-commit"] 54 | news = ["towncrier"] 55 | 56 | [tool.towncrier] 57 | package = "annict" 58 | filename = "CHANGELOG.rst" 59 | directory = "news/" 60 | template = "news/_template.rst" 61 | 62 | [[tool.towncrier.type]] 63 | directory = "behavior" 64 | name = "Behavior Changes" 65 | showcontent = true 66 | 67 | [[tool.towncrier.type]] 68 | directory = "removal" 69 | name = "Deprecations and Removals" 70 | showcontent = true 71 | 72 | [[tool.towncrier.type]] 73 | directory = "feature" 74 | name = "Features" 75 | showcontent = true 76 | 77 | [[tool.towncrier.type]] 78 | directory = "bugfix" 79 | name = "Bug Fixes" 80 | showcontent = true 81 | 82 | [[tool.towncrier.type]] 83 | directory = "vendor" 84 | name = "Vendored Libraries" 85 | showcontent = true 86 | 87 | [[tool.towncrier.type]] 88 | directory = "doc" 89 | name = "Improved Documentation" 90 | showcontent = true 91 | 92 | [[tool.towncrier.type]] 93 | directory = "trivial" 94 | name = "Trivial Changes" 95 | showcontent = false 96 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | name: annict 2 | type: sphinx 3 | conda: 4 | file: docs/environment.yml 5 | python: 6 | version: 3 7 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kk6/python-annict/b6d7e88f67b9f17e1522e2001ebee3a19346da68/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def api_factory(): 7 | class APIFactory: 8 | def create(self, token="dummy_token"): 9 | from annict.api import API 10 | 11 | return API(token) 12 | 13 | return APIFactory() 14 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from urllib.parse import parse_qs 3 | from urllib.parse import urlparse 4 | 5 | import responses 6 | 7 | 8 | def get_query(call_object): 9 | result = urlparse(call_object.request.url) 10 | return parse_qs(result.query) 11 | 12 | 13 | @responses.activate 14 | def test_works(api_factory): 15 | json = """ 16 | { 17 | "works": [ 18 | { 19 | "id": 4168, 20 | "title": "SHIROBAKO", 21 | "title_kana": "しろばこ", 22 | "media": "tv", 23 | "media_text": "TV", 24 | "season_name": "2014-autumn", 25 | "season_name_text": "2014年秋", 26 | "released_on": "2014-10-09", 27 | "released_on_about": "", 28 | "official_site_url": "http://shirobako-anime.com", 29 | "wikipedia_url": "http://ja.wikipedia.org/wiki/SHIROBAKO", 30 | "twitter_username": "shirobako_anime", 31 | "twitter_hashtag": "musani", 32 | "images": { 33 | "recommended_url": "http://shirobako-anime.com/images/ogp.jpg", 34 | "facebook": { 35 | "og_image_url": "http://shirobako-anime.com/images/ogp.jpg" 36 | }, 37 | "twitter": { 38 | "mini_avatar_url": "https://twitter.com/shirobako_anime/profile_image?size=mini", 39 | "normal_avatar_url": "https://twitter.com/shirobako_anime/profile_image?size=normal", 40 | "bigger_avatar_url": "https://twitter.com/shirobako_anime/profile_image?size=bigger", 41 | "original_avatar_url": "https://twitter.com/shirobako_anime/profile_image?size=original", 42 | "image_url": "" 43 | } 44 | }, 45 | "episodes_count": 24, 46 | "watchers_count": 1254 47 | } 48 | ], 49 | "total_count": 1, 50 | "next_page": null, 51 | "prev_page": null 52 | } 53 | """ 54 | responses.add( 55 | responses.GET, 56 | "https://api.annict.com/v1/works", 57 | body=json, 58 | status=200, 59 | content_type="application/json", 60 | ) 61 | api = api_factory.create() 62 | works = api.works() 63 | assert works[0].title == "SHIROBAKO" 64 | 65 | 66 | @responses.activate 67 | def test_works_with_fields(api_factory): 68 | json = """ 69 | { 70 | "works": [ 71 | { 72 | "id": 4168, 73 | "title": "SHIROBAKO" 74 | } 75 | ], 76 | "total_count": 1, 77 | "next_page": null, 78 | "prev_page": null 79 | } 80 | """ 81 | responses.add( 82 | responses.GET, 83 | "https://api.annict.com/v1/works", 84 | body=json, 85 | status=200, 86 | content_type="application/json", 87 | ) 88 | api = api_factory.create() 89 | works = api.works("title") 90 | assert works[0].title == "SHIROBAKO" 91 | assert not hasattr(works[0], "title_kana") 92 | assert get_query(responses.calls[0]) == { 93 | "fields": ["title"], 94 | "access_token": ["dummy_token"], 95 | } 96 | 97 | 98 | @responses.activate 99 | def test_episodes(api_factory): 100 | json = """ 101 | { 102 | "episodes": [ 103 | { 104 | "id": 45, 105 | "number": null, 106 | "number_text": "第2話", 107 | "sort_number": 2, 108 | "title": "殺戮の夢幻迷宮", 109 | "records_count": 0, 110 | "record_comments_count": 0, 111 | "work": { 112 | "id": 3831, 113 | "title": "NEWドリームハンター麗夢", 114 | "title_kana": "", 115 | "media": "ova", 116 | "media_text": "OVA", 117 | "season_name": "1990-autumn", 118 | "season_name_text": "1990年秋", 119 | "released_on": "1990-12-16", 120 | "released_on_about": "", 121 | "official_site_url": "", 122 | "wikipedia_url": "", 123 | "twitter_username": "", 124 | "twitter_hashtag": "", 125 | "episodes_count": 2, 126 | "watchers_count": 10 127 | }, 128 | "prev_episode": { 129 | "id": 44, 130 | "number": null, 131 | "number_text": "第1話", 132 | "sort_number": 1, 133 | "title": " 夢の騎士達", 134 | "records_count": 0, 135 | "record_comments_count": 0 136 | }, 137 | "next_episode": null 138 | } 139 | ], 140 | "total_count": 1, 141 | "next_page": null, 142 | "prev_page": null 143 | } 144 | """ 145 | responses.add( 146 | responses.GET, 147 | "https://api.annict.com/v1/episodes", 148 | body=json, 149 | status=200, 150 | content_type="application/json", 151 | ) 152 | api = api_factory.create() 153 | episodes = api.episodes() 154 | assert episodes[0].title == "殺戮の夢幻迷宮" 155 | 156 | 157 | @responses.activate 158 | def test_people(api_factory): 159 | json = """ 160 | { 161 | "people": [ 162 | { 163 | "id": 7118, 164 | "name": "水瀬いのり", 165 | "name_kana": "みなせいのり", 166 | "name_en": "Minase, Inori", 167 | "nickname": "いのりん、いのすけ", 168 | "nickname_en": "", 169 | "gender_text": "女性", 170 | "url": "http://axl-one.com/talent/minase.html", 171 | "url_en": "", 172 | "wikipedia_url": "https://ja.wikipedia.org/wiki/%E6%B0%B4%E7%80%AC%E3%81%84%E3%81%AE%E3%82%8A", 173 | "wikipedia_url_en": "", 174 | "twitter_username": "inoriminase", 175 | "twitter_username_en": "", 176 | "birthday": "1995-12-02", 177 | "blood_type": "b", 178 | "height": 154, 179 | "favorite_people_count": 74, 180 | "casts_count": 58, 181 | "staffs_count": 0, 182 | "prefecture": { 183 | "id": 13, 184 | "name": "東京都" 185 | } 186 | } 187 | ], 188 | "total_count": 1, 189 | "next_page": null, 190 | "prev_page": null 191 | } 192 | """ 193 | responses.add( 194 | responses.GET, 195 | "https://api.annict.com/v1/people", 196 | body=json, 197 | status=200, 198 | content_type="application/json", 199 | ) 200 | api = api_factory.create() 201 | people = api.people(filter_ids=7118) 202 | assert people[0].name == "水瀬いのり" 203 | 204 | 205 | @responses.activate 206 | def test_records(api_factory): 207 | json = """ 208 | { 209 | "records": [ 210 | { 211 | "id": 425551, 212 | "comment": "ゆるふわ田舎アニメかと思ったらギャグと下ネタが多めのコメディアニメだった。これはこれで。日岡さんの声良いなあ。", 213 | "rating": 4, 214 | "is_modified": false, 215 | "likes_count": 0, 216 | "comments_count": 0, 217 | "created_at": "2016-04-11T14:19:13.974Z", 218 | "user": { 219 | "id": 2, 220 | "username": "shimbaco", 221 | "name": "Koji Shimba", 222 | "description": "アニメ好きが高じてこのサービスを作りました。聖地巡礼を年に数回しています。", 223 | "url": "http://shimba.co", 224 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_avatars/master/d8af7adc8122c96ba7639218fd8b5ede332d42f2.jpg?1431357292", 225 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_background_images/master/ee15d577fb2f2d61bdaf700cfab894b286a5762d.jpg?1486753229", 226 | "records_count": 1906, 227 | "created_at": "2014-03-02T15:38:40.000Z" 228 | }, 229 | "work": { 230 | "id": 4670, 231 | "title": "くまみこ", 232 | "title_kana": "くまみこ", 233 | "media": "tv", 234 | "media_text": "TV", 235 | "season_name": "2016-spring", 236 | "season_name_text": "2016年春", 237 | "released_on": "", 238 | "released_on_about": "", 239 | "official_site_url": "http://kmmk.tv/", 240 | "wikipedia_url": "https://ja.wikipedia.org/wiki/%E3%81%8F%E3%81%BE%E3%81%BF%E3%81%93", 241 | "twitter_username": "kmmk_anime", 242 | "twitter_hashtag": "kumamiko", 243 | "episodes_count": 6, 244 | "watchers_count": 609 245 | }, 246 | "episode": { 247 | "id": 74669, 248 | "number": "1", 249 | "number_text": "第壱話", 250 | "sort_number": 10, 251 | "title": "クマと少女 お別れの時", 252 | "records_count": 183, 253 | "record_comments_count": 53 254 | } 255 | } 256 | ], 257 | "total_count": 1, 258 | "next_page": null, 259 | "prev_page": null 260 | } 261 | """ 262 | responses.add( 263 | responses.GET, 264 | "https://api.annict.com/v1/records", 265 | body=json, 266 | status=200, 267 | content_type="application/json", 268 | ) 269 | api = api_factory.create() 270 | records = api.records(filter_episode_id=74669) 271 | assert records[0].comment.startswith("ゆるふわ田舎アニメかと思ったら") 272 | assert get_query(responses.calls[0]) == { 273 | "filter_episode_id": ["74669"], 274 | "access_token": ["dummy_token"], 275 | } 276 | 277 | 278 | @responses.activate 279 | def test_users(api_factory): 280 | json = """ 281 | { 282 | "users": [ 283 | { 284 | "id": 2, 285 | "username": "shimbaco", 286 | "name": "Koji Shimba", 287 | "description": "アニメ好きが高じてこのサービスを作りました。聖地巡礼を年に数回しています。", 288 | "url": "http://shimba.co", 289 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_avatars/master/d8af7adc8122c96ba7639218fd8b5ede332d42f2.jpg?1431357292", 290 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_background_images/master/ee15d577fb2f2d61bdaf700cfab894b286a5762d.jpg?1486753229", 291 | "records_count": 2369, 292 | "created_at": "2014-03-02T15:38:40.000Z" 293 | } 294 | ], 295 | "total_count": 1, 296 | "next_page": null, 297 | "prev_page": null 298 | } 299 | """ 300 | responses.add( 301 | responses.GET, 302 | "https://api.annict.com/v1/users", 303 | body=json, 304 | status=200, 305 | content_type="application/json", 306 | ) 307 | api = api_factory.create() 308 | users = api.search_users(filter_usernames="shimbaco") 309 | assert users[0].name == "Koji Shimba" 310 | assert get_query(responses.calls[0]) == { 311 | "filter_usernames": ["shimbaco"], 312 | "access_token": ["dummy_token"], 313 | } 314 | 315 | 316 | @responses.activate 317 | def test_following(api_factory): 318 | json = """ 319 | { 320 | "users": [ 321 | { 322 | "id": 3, 323 | "username": "builtlast", 324 | "name": "岩永勇輝 (Creasty)", 325 | "description": "Web やってる大学生\\nプログラミングとかデザインとか\\n価値を生み出せるようになりたい\\n\\nアルバイト@FICC\\n\\nC / Obj-C / Ruby / Haskell / PHP / CoffeeScript / VimScript / Photoshop / Illustrator", 326 | "url": null, 327 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/2/tombo_avatars/master/cc301ca5c5e13399144c79daa4e4727b783676de.jpg?1428129519", 328 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/2/tombo_avatars/master/cc301ca5c5e13399144c79daa4e4727b783676de.jpg?1428129519", 329 | "records_count": 0, 330 | "created_at": "2014-03-04T09:32:25.000Z" 331 | }, 332 | { 333 | "id": 4, 334 | "username": "pataiji", 335 | "name": "PATAIJI", 336 | "description": "FICC inc.ベースやってます。カブに乗ってます。AWSすごい良い。Railsすごい楽。", 337 | "url": null, 338 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/3/tombo_avatars/master/33ce537a4cf38f71b509f295f2afa3291c281dcf.jpg?1428129521", 339 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/3/tombo_avatars/master/33ce537a4cf38f71b509f295f2afa3291c281dcf.jpg?1428129521", 340 | "records_count": 0, 341 | "created_at": "2014-03-04T09:32:28.000Z" 342 | } 343 | ], 344 | "total_count": 274, 345 | "next_page": 2, 346 | "prev_page": null 347 | } 348 | """ 349 | responses.add( 350 | responses.GET, 351 | "https://api.annict.com/v1/following", 352 | body=json, 353 | status=200, 354 | content_type="application/json", 355 | ) 356 | api = api_factory.create() 357 | following = api.following(filter_username="shimbaco", per_page=2) 358 | assert following[0].username == "builtlast" 359 | assert get_query(responses.calls[0]) == { 360 | "filter_username": ["shimbaco"], 361 | "per_page": ["2"], 362 | "access_token": ["dummy_token"], 363 | } 364 | 365 | 366 | @responses.activate 367 | def test_followers(api_factory): 368 | json = """ 369 | { 370 | "users": [ 371 | { 372 | "id": 7, 373 | "username": "akirafukuoka", 374 | "name": "akirafukuoka", 375 | "description": "FICC inc. http://www.ficc.jp クリエイティブディレクター。RAW-Fi http://raw-fi.com @raw_fi もよろしくお願いします。", 376 | "url": null, 377 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/6/tombo_avatars/master/480862747fc5f7152a031e24f0c0374dc71c539a.jpg?1431596794", 378 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/6/tombo_background_images/master/7e258e0189e9ee38f4dc0c57b2c9f6b39dd2cd95.jpg?1431596795", 379 | "records_count": 260, 380 | "created_at": "2014-03-10T04:11:54.000Z" 381 | }, 382 | { 383 | "id": 8, 384 | "username": "310u8", 385 | "name": "Daisuke Nagai", 386 | "description": "歌って踊れるWebデザイナーです", 387 | "url": null, 388 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/7/tombo_avatars/master/cd7b66919fea1952e63855632665812839e2a394.jpg?1428129527", 389 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/7/tombo_avatars/master/cd7b66919fea1952e63855632665812839e2a394.jpg?1428129527", 390 | "records_count": 739, 391 | "created_at": "2014-03-10T04:11:55.000Z" 392 | } 393 | ], 394 | "total_count": 191, 395 | "next_page": 2, 396 | "prev_page": null 397 | } 398 | """ 399 | responses.add( 400 | responses.GET, 401 | "https://api.annict.com/v1/followers", 402 | body=json, 403 | status=200, 404 | content_type="application/json", 405 | ) 406 | api = api_factory.create() 407 | followers = api.followers() 408 | assert followers[0].username == "akirafukuoka" 409 | 410 | 411 | @responses.activate 412 | def test_activities(api_factory): 413 | json = """ 414 | { 415 | "activities": [ 416 | { 417 | "id": 1504708, 418 | "user": { 419 | "id": 2, 420 | "username": "shimbaco", 421 | "name": "Koji Shimba", 422 | "description": "アニメ好きが高じてこのサービスを作りました。聖地巡礼を年に数回しています。", 423 | "url": "http://shimba.co", 424 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_avatars/master/d8af7adc8122c96ba7639218fd8b5ede332d42f2.jpg?1431357292", 425 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_background_images/master/ee15d577fb2f2d61bdaf700cfab894b286a5762d.jpg?1486753229", 426 | "records_count": 2369, 427 | "created_at": "2014-03-02T15:38:40.000Z" 428 | }, 429 | "action": "create_record", 430 | "created_at": "2017-02-22T13:24:44.761Z", 431 | "work": { 432 | "id": 5036, 433 | "title": "小林さんちのメイドラゴン", 434 | "title_kana": "こばやしさんちのめいどらごん", 435 | "media": "tv", 436 | "media_text": "TV", 437 | "released_on": "", 438 | "released_on_about": "", 439 | "official_site_url": "http://maidragon.jp/", 440 | "wikipedia_url": "https://ja.wikipedia.org/wiki/%E5%B0%8F%E6%9E%97%E3%81%95%E3%82%93%E3%81%A1%E3%81%AE%E3%83%A1%E3%82%A4%E3%83%89%E3%83%A9%E3%82%B4%E3%83%B3", 441 | "twitter_username": "maidragon_anime", 442 | "twitter_hashtag": "maidragon", 443 | "episodes_count": 7, 444 | "watchers_count": 448, 445 | "season_name": "2017-winter", 446 | "season_name_text": "2017年冬" 447 | }, 448 | "episode": { 449 | "id": 89678, 450 | "number": "6", 451 | "number_text": "#6", 452 | "sort_number": 60, 453 | "title": "お宅訪問!(してないお宅もあります)", 454 | "records_count": 89, 455 | "record_comments_count": 24 456 | }, 457 | "record": { 458 | "id": 864718, 459 | "comment": "", 460 | "rating": null, 461 | "is_modified": false, 462 | "likes_count": 0, 463 | "comments_count": 0, 464 | "created_at": "2017-02-22T13:24:39.353Z" 465 | } 466 | } 467 | ], 468 | "total_count": 3705, 469 | "next_page": 2, 470 | "prev_page": null 471 | } 472 | """ 473 | responses.add( 474 | responses.GET, 475 | "https://api.annict.com/v1/activities", 476 | body=json, 477 | status=200, 478 | content_type="application/json", 479 | ) 480 | api = api_factory.create() 481 | activities = api.activities() 482 | assert activities[0].id == 1504708 483 | 484 | 485 | @responses.activate 486 | def test_me(api_factory): 487 | json = """ 488 | { 489 | "id": 2, 490 | "username": "shimbaco", 491 | "name": "Koji Shimba", 492 | "description": "アニメ好きが高じてこのサービスを作りました。聖地巡礼を年に数回しています。", 493 | "url": "http://shimba.co", 494 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_avatars/master/d8af7adc8122c96ba7639218fd8b5ede332d42f2.jpg?1431357292", 495 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_background_images/master/ee15d577fb2f2d61bdaf700cfab894b286a5762d.jpg?1486753229", 496 | "records_count": 2369, 497 | "created_at": "2014-03-02T15:38:40.000Z", 498 | "email": "me@shimba.co", 499 | "notifications_count": 0 500 | } 501 | """ 502 | responses.add( 503 | responses.GET, 504 | "https://api.annict.com/v1/me", 505 | body=json, 506 | status=200, 507 | content_type="application/json", 508 | ) 509 | api = api_factory.create() 510 | me = api.me() 511 | assert me.username == "shimbaco" 512 | 513 | 514 | @responses.activate 515 | def test_set_status(api_factory): 516 | responses.add( 517 | responses.POST, "https://api.annict.com/v1/me/statuses", body=None, status=204 518 | ) 519 | api = api_factory.create() 520 | result = api.set_status(work_id=438, kind="watching") 521 | assert result 522 | 523 | 524 | @responses.activate 525 | def test_create_record(api_factory): 526 | json = """ 527 | { 528 | "id": 470491, 529 | "comment": "あぁ^~心がぴょんぴょんするんじゃぁ^~", 530 | "rating": null, 531 | "is_modified": false, 532 | "likes_count": 0, 533 | "comments_count": 0, 534 | "created_at": "2016-05-07T09:40:32.159Z", 535 | "user": { 536 | "id": 2, 537 | "username": "shimbaco", 538 | "name": "Koji Shimba", 539 | "description": "", 540 | "url": null, 541 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_avatars/master/d8af7adc8122c96ba7639218fd8b5ede332d42f2.jpg?1431357292", 542 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_background_images/master/ee15d577fb2f2d61bdaf700cfab894b286a5762d.jpg?1486753229", 543 | "records_count": 123, 544 | "created_at": "2016-05-03T19:06:59.929Z" 545 | }, 546 | "work": { 547 | "id": 3994, 548 | "title": "ご注文はうさぎですか?", 549 | "title_kana": "ごちゅうもんはうさぎですか", 550 | "media": "tv", 551 | "media_text": "TV", 552 | "season_name": "2014-spring", 553 | "season_name_text": "2014年春", 554 | "released_on": "2014-04-10", 555 | "released_on_about": "", 556 | "official_site_url": "http://www.gochiusa.com/", 557 | "wikipedia_url": "http://ja.wikipedia.org/wiki/%E3%81%94%E6%B3%A8%E6%96%87%E3%81%AF%E3%81%86%E3%81%95%E3%81%8E%E3%81%A7%E3%81%99%E3%81%8B%3F#.E3.83.86.E3.83.AC.E3.83.93.E3.82.A2.E3.83.8B.E3.83.A1", 558 | "twitter_username": "usagi_anime", 559 | "twitter_hashtag": "gochiusa", 560 | "episodes_count": 12, 561 | "watchers_count": 850 562 | }, 563 | "episode": { 564 | "id": 5013, 565 | "number": null, 566 | "number_text": "第1羽", 567 | "sort_number": 1, 568 | "title": "ひと目で尋常でないもふもふだと見抜いたよ", 569 | "records_count": 103, 570 | "record_comments_count": 3 571 | } 572 | } 573 | """ 574 | responses.add( 575 | responses.POST, 576 | "https://api.annict.com/v1/me/records", 577 | body=json, 578 | status=200, 579 | content_type="application/json", 580 | ) 581 | api = api_factory.create() 582 | record = api.create_record(episode_id=5013, comment="あぁ^~心がぴょんぴょんするんじゃぁ^~") 583 | assert record.comment == "あぁ^~心がぴょんぴょんするんじゃぁ^~" 584 | 585 | 586 | @responses.activate 587 | def test_edit_record(api_factory): 588 | json = """ 589 | { 590 | "id": 1016, 591 | "comment": "あぁ^~心がぴょんぴょんするんじゃぁ^~", 592 | "rating": 5.0, 593 | "is_modified": true, 594 | "likes_count": 0, 595 | "comments_count": 0, 596 | "created_at": "2016-05-07T09:40:32.159Z", 597 | "user": { 598 | "id": 2, 599 | "username": "shimbaco", 600 | "name": "Koji Shimba", 601 | "description": "", 602 | "url": null, 603 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_avatars/master/d8af7adc8122c96ba7639218fd8b5ede332d42f2.jpg?1431357292", 604 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_background_images/master/ee15d577fb2f2d61bdaf700cfab894b286a5762d.jpg?1486753229", 605 | "records_count": 1234, 606 | "created_at": "2016-05-03T19:06:59.929Z" 607 | }, 608 | "work": { 609 | "id": 3994, 610 | "title": "ご注文はうさぎですか?", 611 | "title_kana": "ごちゅうもんはうさぎですか", 612 | "media": "tv", 613 | "media_text": "TV", 614 | "season_name": "2014-spring", 615 | "season_name_text": "2014年春", 616 | "released_on": "2014-04-10", 617 | "released_on_about": "", 618 | "official_site_url": "http://www.gochiusa.com/", 619 | "wikipedia_url": "http://ja.wikipedia.org/wiki/%E3%81%94%E6%B3%A8%E6%96%87%E3%81%AF%E3%81%86%E3%81%95%E3%81%8E%E3%81%A7%E3%81%99%E3%81%8B%3F#.E3.83.86.E3.83.AC.E3.83.93.E3.82.A2.E3.83.8B.E3.83.A1", 620 | "twitter_username": "usagi_anime", 621 | "twitter_hashtag": "gochiusa", 622 | "episodes_count": 12, 623 | "watchers_count": 850 624 | }, 625 | "episode": { 626 | "id": 5013, 627 | "number": null, 628 | "number_text": "第1羽", 629 | "sort_number": 1, 630 | "title": "ひと目で尋常でないもふもふだと見抜いたよ", 631 | "records_count": 103, 632 | "record_comments_count": 3 633 | } 634 | } 635 | """ 636 | responses.add( 637 | responses.PATCH, 638 | "https://api.annict.com/v1/me/records/1016", 639 | body=json, 640 | status=200, 641 | content_type="application/json", 642 | ) 643 | api = api_factory.create() 644 | record = api.edit_record( 645 | 1016, comment="あぁ^~心がぴょんぴょんするんじゃぁ^~", rating=5.0, share_facebook=True 646 | ) 647 | assert record.rating == 5.0 648 | 649 | 650 | @responses.activate 651 | def test_delete_record(api_factory): 652 | responses.add( 653 | responses.DELETE, 654 | "https://api.annict.com/v1/me/records/1016", 655 | body=None, 656 | status=204, 657 | ) 658 | api = api_factory.create() 659 | result = api.delete_record(1016) 660 | assert result 661 | 662 | 663 | @responses.activate 664 | def test_my_works(api_factory): 665 | json = """ 666 | { 667 | "works": [ 668 | { 669 | "id": 4681, 670 | "title": "ふらいんぐうぃっち", 671 | "title_kana": "ふらいんぐうぃっち", 672 | "media": "tv", 673 | "media_text": "TV", 674 | "season_name": "2016-spring", 675 | "season_name_text": "2016年春", 676 | "released_on": "", 677 | "released_on_about": "", 678 | "official_site_url": "http://www.flyingwitch.jp/", 679 | "wikipedia_url": "https://ja.wikipedia.org/wiki/%E3%81%B5%E3%82%89%E3%81%84%E3%82%93%E3%81%90%E3%81%86%E3%81%83%E3%81%A3%E3%81%A1", 680 | "twitter_username": "flying_tv", 681 | "twitter_hashtag": "flyingwitch", 682 | "episodes_count": 5, 683 | "watchers_count": 695, 684 | "status": { 685 | "kind": "watching" 686 | } 687 | } 688 | ], 689 | "total_count": 1, 690 | "next_page": null, 691 | "prev_page": null 692 | } 693 | """ 694 | responses.add( 695 | responses.GET, 696 | "https://api.annict.com/v1/me/works", 697 | body=json, 698 | status=200, 699 | content_type="application/json", 700 | ) 701 | api = api_factory.create() 702 | works = api.my_works() 703 | assert works[0].title == "ふらいんぐうぃっち" 704 | 705 | 706 | @responses.activate 707 | def test_my_programs(api_factory): 708 | json = """ 709 | { 710 | "programs": [ 711 | { 712 | "id": 35387, 713 | "started_at": "2016-05-07T20:10:00.000Z", 714 | "is_rebroadcast": false, 715 | "channel": { 716 | "id": 4, 717 | "name": "日本テレビ" 718 | }, 719 | "work": { 720 | "id": 4681, 721 | "title": "ふらいんぐうぃっち", 722 | "title_kana": "ふらいんぐうぃっち", 723 | "media": "tv", 724 | "media_text": "TV", 725 | "season_name": "2016-spring", 726 | "season_name_text": "2016年春", 727 | "released_on": "", 728 | "released_on_about": "", 729 | "official_site_url": "http://www.flyingwitch.jp/", 730 | "wikipedia_url": "https://ja.wikipedia.org/wiki/%E3%81%B5%E3%82%89%E3%81%84%E3%82%93%E3%81%90%E3%81%86%E3%81%83%E3%81%A3%E3%81%A1", 731 | "twitter_username": "flying_tv", 732 | "twitter_hashtag": "flyingwitch", 733 | "episodes_count": 5, 734 | "watchers_count": 695 735 | }, 736 | "episode": { 737 | "id": 75187, 738 | "number": "5", 739 | "number_text": "第5話", 740 | "sort_number": 50, 741 | "title": "使い魔の活用法", 742 | "records_count": 0, 743 | "record_comments_count": 0 744 | } 745 | } 746 | ], 747 | "total_count": 1, 748 | "next_page": null, 749 | "prev_page": null 750 | } 751 | """ 752 | responses.add( 753 | responses.GET, 754 | "https://api.annict.com/v1/me/programs", 755 | body=json, 756 | status=200, 757 | content_type="application/json", 758 | ) 759 | api = api_factory.create() 760 | programs = api.my_programs( 761 | sort_started_at="desc", filter_started_at_gt="2016/05/05 02:00" 762 | ) 763 | assert programs[0].id == 35387 764 | assert get_query(responses.calls[0]) == { 765 | "sort_started_at": ["desc"], 766 | "filter_started_at_gt": ["2016/05/05 02:00"], 767 | "access_token": ["dummy_token"], 768 | } 769 | 770 | 771 | @responses.activate 772 | def test_following_activities(api_factory): 773 | json = """ 774 | { 775 | "activities": [ 776 | { 777 | "id": 1504708, 778 | "user": { 779 | "id": 2, 780 | "username": "shimbaco", 781 | "name": "Koji Shimba", 782 | "description": "アニメ好きが高じてこのサービスを作りました。聖地巡礼を年に数回しています。", 783 | "url": "http://shimba.co", 784 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_avatars/master/d8af7adc8122c96ba7639218fd8b5ede332d42f2.jpg?1431357292", 785 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_background_images/master/ee15d577fb2f2d61bdaf700cfab894b286a5762d.jpg?1486753229", 786 | "records_count": 2369, 787 | "created_at": "2014-03-02T15:38:40.000Z" 788 | }, 789 | "action": "create_record", 790 | "created_at": "2017-02-22T13:24:44.761Z", 791 | "work": { 792 | "id": 5036, 793 | "title": "小林さんちのメイドラゴン", 794 | "title_kana": "こばやしさんちのめいどらごん", 795 | "media": "tv", 796 | "media_text": "TV", 797 | "released_on": "", 798 | "released_on_about": "", 799 | "official_site_url": "http://maidragon.jp/", 800 | "wikipedia_url": "https://ja.wikipedia.org/wiki/%E5%B0%8F%E6%9E%97%E3%81%95%E3%82%93%E3%81%A1%E3%81%AE%E3%83%A1%E3%82%A4%E3%83%89%E3%83%A9%E3%82%B4%E3%83%B3", 801 | "twitter_username": "maidragon_anime", 802 | "twitter_hashtag": "maidragon", 803 | "episodes_count": 7, 804 | "watchers_count": 448, 805 | "season_name": "2017-winter", 806 | "season_name_text": "2017年冬" 807 | }, 808 | "episode": { 809 | "id": 89678, 810 | "number": "6", 811 | "number_text": "#6", 812 | "sort_number": 60, 813 | "title": "お宅訪問!(してないお宅もあります)", 814 | "records_count": 89, 815 | "record_comments_count": 24 816 | }, 817 | "record": { 818 | "id": 864718, 819 | "comment": "", 820 | "rating": null, 821 | "is_modified": false, 822 | "likes_count": 0, 823 | "comments_count": 0, 824 | "created_at": "2017-02-22T13:24:39.353Z" 825 | } 826 | } 827 | ], 828 | "total_count": 138030, 829 | "next_page": 2, 830 | "prev_page": null 831 | } 832 | """ 833 | responses.add( 834 | responses.GET, 835 | "https://api.annict.com/v1/me/following_activities", 836 | body=json, 837 | status=200, 838 | content_type="application/json", 839 | ) 840 | api = api_factory.create() 841 | activities = api.following_activities(sort_id="desc", per_page=1) 842 | assert activities[0].id == 1504708 843 | assert get_query(responses.calls[0]) == { 844 | "sort_id": ["desc"], 845 | "per_page": ["1"], 846 | "access_token": ["dummy_token"], 847 | } 848 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from urllib.parse import parse_qs 3 | 4 | 5 | def test_get_authorization_url(): 6 | from annict.auth import OAuthHandler 7 | 8 | auth = OAuthHandler("dummy_client_id", "dummy_client_secret") 9 | url = auth.get_authorization_url() 10 | endpoint, qs = url.split("?") 11 | 12 | assert endpoint == "https://api.annict.com/oauth/authorize" 13 | assert parse_qs(qs) == { 14 | "scope": ["read"], 15 | "client_id": ["dummy_client_id"], 16 | "redirect_uri": ["urn:ietf:wg:oauth:2.0:oob"], 17 | "response_type": ["code"], 18 | } 19 | -------------------------------------------------------------------------------- /tests/test_cursors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import responses 4 | 5 | 6 | @responses.activate 7 | def test_cursor_supported(api_factory): 8 | json = """ 9 | { 10 | "works": [ 11 | { 12 | "id": 4168, 13 | "title": "SHIROBAKO", 14 | "title_kana": "しろばこ", 15 | "media": "tv", 16 | "media_text": "TV", 17 | "season_name": "2014-autumn", 18 | "season_name_text": "2014年秋", 19 | "released_on": "2014-10-09", 20 | "released_on_about": "", 21 | "official_site_url": "http://shirobako-anime.com", 22 | "wikipedia_url": "http://ja.wikipedia.org/wiki/SHIROBAKO", 23 | "twitter_username": "shirobako_anime", 24 | "twitter_hashtag": "musani", 25 | "images": { 26 | "recommended_url": "http://shirobako-anime.com/images/ogp.jpg", 27 | "facebook": { 28 | "og_image_url": "http://shirobako-anime.com/images/ogp.jpg" 29 | }, 30 | "twitter": { 31 | "mini_avatar_url": "https://twitter.com/shirobako_anime/profile_image?size=mini", 32 | "normal_avatar_url": "https://twitter.com/shirobako_anime/profile_image?size=normal", 33 | "bigger_avatar_url": "https://twitter.com/shirobako_anime/profile_image?size=bigger", 34 | "original_avatar_url": "https://twitter.com/shirobako_anime/profile_image?size=original", 35 | "image_url": "" 36 | } 37 | }, 38 | "episodes_count": 24, 39 | "watchers_count": 1254 40 | } 41 | ], 42 | "total_count": 1, 43 | "next_page": null, 44 | "prev_page": null 45 | } 46 | """ 47 | responses.add( 48 | responses.GET, 49 | "https://api.annict.com/v1/works", 50 | body=json, 51 | status=200, 52 | content_type="application/json", 53 | ) 54 | api = api_factory.create() 55 | from annict.cursors import SimpleCursor 56 | 57 | gen = SimpleCursor(api.works).cursor() 58 | result = next(gen) 59 | assert result.title == "SHIROBAKO" 60 | with pytest.raises(StopIteration): 61 | next(gen) 62 | 63 | 64 | @responses.activate 65 | def test_cursor_unsupported(api_factory): 66 | json = """ 67 | { 68 | "id": 2, 69 | "username": "shimbaco", 70 | "name": "Koji Shimba", 71 | "description": "アニメ好きが高じてこのサービスを作りました。聖地巡礼を年に数回しています。", 72 | "url": "http://shimba.co", 73 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_avatars/master/d8af7adc8122c96ba7639218fd8b5ede332d42f2.jpg?1431357292", 74 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_background_images/master/ee15d577fb2f2d61bdaf700cfab894b286a5762d.jpg?1486753229", 75 | "records_count": 2369, 76 | "created_at": "2014-03-02T15:38:40.000Z", 77 | "email": "me@shimba.co", 78 | "notifications_count": 0 79 | } 80 | """ 81 | responses.add( 82 | responses.GET, 83 | "https://api.annict.com/v1/me", 84 | body=json, 85 | status=200, 86 | content_type="application/json", 87 | ) 88 | api = api_factory.create() 89 | from annict.cursors import SimpleCursor 90 | 91 | with pytest.raises(TypeError): 92 | SimpleCursor(api.me).cursor() 93 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from urllib.parse import parse_qs 4 | from urllib.parse import urlparse 5 | 6 | import pytest 7 | import responses 8 | from arrow import arrow 9 | 10 | tzutc = arrow.dateutil_tz.tzutc 11 | 12 | 13 | def test_user(): 14 | from annict.models import User 15 | 16 | json = { 17 | "id": 2, 18 | "username": "shimbaco", 19 | "name": "Koji Shimba", 20 | "description": "アニメ好きが高じてこのサービスを作りました。聖地巡礼を年に数回しています。", 21 | "url": "http://shimba.co", 22 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_avatars/master/d8af7adc8122c96ba7639218fd8b5ede332d42f2.jpg?1431357292", 23 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/1/tombo_background_images/master/ee15d577fb2f2d61bdaf700cfab894b286a5762d.jpg?1486753229", 24 | "records_count": 2369, 25 | "created_at": "2014-03-02T15:38:40.000Z", 26 | "email": "me@shimba.co", 27 | "notifications_count": 0, 28 | } 29 | user = User.parse(None, json) 30 | assert user.id == 2 31 | assert user.username == "shimbaco" 32 | assert user.name == "Koji Shimba" 33 | assert user.description == "アニメ好きが高じてこのサービスを作りました。聖地巡礼を年に数回しています。" 34 | assert user.url == "http://shimba.co" 35 | assert ( 36 | user.avatar_url 37 | == "https://api-assets.annict.com/paperclip/profiles/1/tombo_avatars/master/d8af7adc8122c96ba7639218fd8b5ede332d42f2.jpg?1431357292" 38 | ) 39 | assert ( 40 | user.background_image_url 41 | == "https://api-assets.annict.com/paperclip/profiles/1/tombo_background_images/master/ee15d577fb2f2d61bdaf700cfab894b286a5762d.jpg?1486753229" 42 | ) 43 | assert user.records_count == 2369 44 | assert user.created_at == datetime.datetime(2014, 3, 2, 15, 38, 40, tzinfo=tzutc()) 45 | assert user.email == "me@shimba.co" 46 | assert user.notifications_count == 0 47 | 48 | 49 | def test_work(): 50 | json = { 51 | "episodes_count": 21, 52 | "id": 4636, 53 | "media": "tv", 54 | "media_text": "TV", 55 | "official_site_url": "http://re-zero-anime.jp/", 56 | "released_on": "2016-04-03", 57 | "released_on_about": "", 58 | "season_name": "2016-spring", 59 | "season_name_text": "2016年春", 60 | "title": "Re:ゼロから始める異世界生活", 61 | "title_kana": "りぜろからはじめるいせかいせいかつ", 62 | "twitter_hashtag": "rezero", 63 | "twitter_username": "Rezero_official", 64 | "watchers_count": 970, 65 | "wikipedia_url": ( 66 | "https://ja.wikipedia.org/wiki/Re:%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E5" 67 | "%A7%8B%E3%82%81%E3%82%8B%E7%95%B0%E4%B8%96%E7%95%8C%E7%94%9F%E6%B4%BB" 68 | ), 69 | } 70 | from annict.models import Work 71 | 72 | work = Work.parse("dummy_api", json) 73 | 74 | assert work.id == 4636 75 | assert work.title == "Re:ゼロから始める異世界生活" 76 | assert work.released_on == datetime.date(2016, 4, 3) 77 | 78 | 79 | def test_episode(): 80 | json = { 81 | "id": 45, 82 | "number": None, 83 | "number_text": "第2話", 84 | "sort_number": 2, 85 | "title": "殺戮の夢幻迷宮", 86 | "records_count": 0, 87 | "work": { 88 | "id": 3831, 89 | "title": "NEWドリームハンター麗夢", 90 | "title_kana": "", 91 | "media": "ova", 92 | "media_text": "OVA", 93 | "season_name": "1990-autumn", 94 | "season_name_text": "1990年秋", 95 | "released_on": "1990-12-16", 96 | "released_on_about": "", 97 | "official_site_url": "", 98 | "wikipedia_url": "", 99 | "twitter_username": "", 100 | "twitter_hashtag": "", 101 | "episodes_count": 2, 102 | "watchers_count": 10, 103 | }, 104 | "prev_episode": { 105 | "id": 44, 106 | "number": None, 107 | "number_text": "第1話", 108 | "sort_number": 1, 109 | "title": " 夢の騎士達", 110 | "records_count": 0, 111 | }, 112 | "next_episode": None, 113 | } 114 | from annict.models import Episode 115 | 116 | episode = Episode.parse(None, json) 117 | 118 | assert episode.id == 45 119 | assert episode.work.id == 3831 120 | assert episode.prev_episode.id == 44 121 | assert episode.next_episode is None 122 | 123 | 124 | def test_record(): 125 | json = { 126 | "id": 425551, 127 | "comment": "ゆるふわ田舎アニメかと思ったらギャグと下ネタが多めのコメディアニメだった。これはこれで。日岡さんの声良いなあ。", 128 | "rating": 4, 129 | "is_modified": False, 130 | "likes_count": 0, 131 | "comments_count": 0, 132 | "created_at": "2016-04-11T14:19:13.974Z", 133 | "user": { 134 | "id": 2, 135 | "username": "shimbaco", 136 | "name": "Koji Shimba", 137 | "description": "アニメ好きが高じてこのサービスを作りました。聖地巡礼を年に数回しています。", 138 | "url": "http://shimba.co", 139 | "records_count": 1906, 140 | "created_at": "2014-03-02T15:38:40.000Z", 141 | }, 142 | "work": { 143 | "id": 4670, 144 | "title": "くまみこ", 145 | "title_kana": "くまみこ", 146 | "media": "tv", 147 | "media_text": "TV", 148 | "season_name": "2016-spring", 149 | "season_name_text": "2016年春", 150 | "released_on": "", 151 | "released_on_about": "", 152 | "official_site_url": "http://kmmk.tv/", 153 | "wikipedia_url": "https://ja.wikipedia.org/wiki/%E3%81%8F%E3%81%BE%E3%81%BF%E3%81%93", 154 | "twitter_username": "kmmk_anime", 155 | "twitter_hashtag": "kumamiko", 156 | "episodes_count": 6, 157 | "watchers_count": 609, 158 | }, 159 | "episode": { 160 | "id": 74669, 161 | "number": "1", 162 | "number_text": "第壱話", 163 | "sort_number": 10, 164 | "title": "クマと少女 お別れの時", 165 | "records_count": 183, 166 | }, 167 | } 168 | 169 | from annict.models import Record 170 | 171 | record = Record.parse(None, json) 172 | 173 | assert record.id == 425551 174 | assert record.user.id == 2 175 | assert record.work.id == 4670 176 | assert record.episode.id == 74669 177 | assert record.created_at == datetime.datetime( 178 | 2016, 4, 11, 14, 19, 13, 974000, tzinfo=tzutc() 179 | ) 180 | 181 | 182 | def test_program(): 183 | json = { 184 | "id": 35387, 185 | "started_at": "2016-05-07T20:10:00.000Z", 186 | "is_rebroadcast": False, 187 | "channel": {"id": 4, "name": "日本テレビ"}, 188 | "work": { 189 | "id": 4681, 190 | "title": "ふらいんぐうぃっち", 191 | "title_kana": "ふらいんぐうぃっち", 192 | "media": "tv", 193 | "media_text": "TV", 194 | "season_name": "2016-spring", 195 | "season_name_text": "2016年春", 196 | "released_on": "", 197 | "released_on_about": "", 198 | "official_site_url": "http://www.flyingwitch.jp/", 199 | "wikipedia_url": "https://ja.wikipedia.org/wiki/%E3%81%B5%E3%82%89%E3%81%84%E3%82%93%E3%81%90%E3%81%86%E3%81%83%E3%81%A3%E3%81%A1", 200 | "twitter_username": "flying_tv", 201 | "twitter_hashtag": "flyingwitch", 202 | "episodes_count": 5, 203 | "watchers_count": 695, 204 | }, 205 | "episode": { 206 | "id": 75187, 207 | "number": "5", 208 | "number_text": "第5話", 209 | "sort_number": 50, 210 | "title": "使い魔の活用法", 211 | "records_count": 0, 212 | }, 213 | } 214 | 215 | from annict.models import Program 216 | 217 | program = Program.parse(None, json) 218 | 219 | assert program.id == 35387 220 | assert program.started_at == datetime.datetime( 221 | 2016, 5, 7, 20, 10, 0, 0, tzinfo=tzutc() 222 | ) 223 | assert program.channel["id"] == 4 224 | assert program.work.id == 4681 225 | assert program.episode.id == 75187 226 | 227 | 228 | class TestRepr: 229 | def test_user(self): 230 | from annict.models import User 231 | 232 | json = { 233 | "id": 2, 234 | "username": "shimbaco", 235 | "name": "Koji Shimba", 236 | "description": "", 237 | "url": None, 238 | "records_count": 1234, 239 | "created_at": "2016-05-03T19:06:59.929Z", 240 | } 241 | user = User.parse(None, json) 242 | assert user.__repr__() == "" 243 | 244 | def test_work(self): 245 | json = { 246 | "episodes_count": 21, 247 | "id": 4636, 248 | "media": "tv", 249 | "media_text": "TV", 250 | "official_site_url": "http://re-zero-anime.jp/", 251 | "released_on": "2016-04-03", 252 | "released_on_about": "", 253 | "season_name": "2016-spring", 254 | "season_name_text": "2016年春", 255 | "title": "Re:ゼロから始める異世界生活", 256 | "title_kana": "りぜろからはじめるいせかいせいかつ", 257 | "twitter_hashtag": "rezero", 258 | "twitter_username": "Rezero_official", 259 | "watchers_count": 970, 260 | "wikipedia_url": ( 261 | "https://ja.wikipedia.org/wiki/Re:%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E5" 262 | "%A7%8B%E3%82%81%E3%82%8B%E7%95%B0%E4%B8%96%E7%95%8C%E7%94%9F%E6%B4%BB" 263 | ), 264 | } 265 | from annict.models import Work 266 | 267 | work = Work.parse("dummy_api", json) 268 | assert work.__repr__() == "" 269 | 270 | def test_episode(self): 271 | json = { 272 | "id": 45, 273 | "number": None, 274 | "number_text": "第2話", 275 | "sort_number": 2, 276 | "title": "殺戮の夢幻迷宮", 277 | "records_count": 0, 278 | "work": { 279 | "id": 3831, 280 | "title": "NEWドリームハンター麗夢", 281 | "title_kana": "", 282 | "media": "ova", 283 | "media_text": "OVA", 284 | "season_name": "1990-autumn", 285 | "season_name_text": "1990年秋", 286 | "released_on": "1990-12-16", 287 | "released_on_about": "", 288 | "official_site_url": "", 289 | "wikipedia_url": "", 290 | "twitter_username": "", 291 | "twitter_hashtag": "", 292 | "episodes_count": 2, 293 | "watchers_count": 10, 294 | }, 295 | "prev_episode": { 296 | "id": 44, 297 | "number": None, 298 | "number_text": "第1話", 299 | "sort_number": 1, 300 | "title": " 夢の騎士達", 301 | "records_count": 0, 302 | }, 303 | "next_episode": None, 304 | } 305 | from annict.models import Episode 306 | 307 | episode = Episode.parse(None, json) 308 | assert episode.__repr__() == "" 309 | 310 | def test_record(self): 311 | json = { 312 | "id": 425551, 313 | "comment": "ゆるふわ田舎アニメかと思ったらギャグと下ネタが多めのコメディアニメだった。これはこれで。日岡さんの声良いなあ。", 314 | "rating": 4, 315 | "is_modified": False, 316 | "likes_count": 0, 317 | "comments_count": 0, 318 | "created_at": "2016-04-11T14:19:13.974Z", 319 | "user": { 320 | "id": 2, 321 | "username": "shimbaco", 322 | "name": "Koji Shimba", 323 | "description": "アニメ好きが高じてこのサービスを作りました。聖地巡礼を年に数回しています。", 324 | "url": "http://shimba.co", 325 | "records_count": 1906, 326 | "created_at": "2014-03-02T15:38:40.000Z", 327 | }, 328 | "work": { 329 | "id": 4670, 330 | "title": "くまみこ", 331 | "title_kana": "くまみこ", 332 | "media": "tv", 333 | "media_text": "TV", 334 | "season_name": "2016-spring", 335 | "season_name_text": "2016年春", 336 | "released_on": "", 337 | "released_on_about": "", 338 | "official_site_url": "http://kmmk.tv/", 339 | "wikipedia_url": "https://ja.wikipedia.org/wiki/%E3%81%8F%E3%81%BE%E3%81%BF%E3%81%93", 340 | "twitter_username": "kmmk_anime", 341 | "twitter_hashtag": "kumamiko", 342 | "episodes_count": 6, 343 | "watchers_count": 609, 344 | }, 345 | "episode": { 346 | "id": 74669, 347 | "number": "1", 348 | "number_text": "第壱話", 349 | "sort_number": 10, 350 | "title": "クマと少女 お別れの時", 351 | "records_count": 183, 352 | }, 353 | } 354 | 355 | from annict.models import Record 356 | 357 | record = Record.parse(None, json) 358 | assert record.__repr__() == "" 359 | 360 | def test_program(self): 361 | json = { 362 | "id": 35387, 363 | "started_at": "2016-05-07T20:10:00.000Z", 364 | "is_rebroadcast": False, 365 | "channel": {"id": 4, "name": "日本テレビ"}, 366 | "work": { 367 | "id": 4681, 368 | "title": "ふらいんぐうぃっち", 369 | "title_kana": "ふらいんぐうぃっち", 370 | "media": "tv", 371 | "media_text": "TV", 372 | "season_name": "2016-spring", 373 | "season_name_text": "2016年春", 374 | "released_on": "", 375 | "released_on_about": "", 376 | "official_site_url": "http://www.flyingwitch.jp/", 377 | "wikipedia_url": "https://ja.wikipedia.org/wiki/%E3%81%B5%E3%82%89%E3%81%84%E3%82%93%E3%81%90%E3%81%86%E3%81%83%E3%81%A3%E3%81%A1", 378 | "twitter_username": "flying_tv", 379 | "twitter_hashtag": "flyingwitch", 380 | "episodes_count": 5, 381 | "watchers_count": 695, 382 | }, 383 | "episode": { 384 | "id": 75187, 385 | "number": "5", 386 | "number_text": "第5話", 387 | "sort_number": 50, 388 | "title": "使い魔の活用法", 389 | "records_count": 0, 390 | }, 391 | } 392 | 393 | from annict.models import Program 394 | 395 | program = Program.parse(None, json) 396 | assert program.__repr__() == "" 397 | 398 | def test_activity_for_create_status_action(self): 399 | json = { 400 | "action": "create_status", 401 | "created_at": "2017-03-12T12:48:07.408Z", 402 | "id": 1535967, 403 | "status": {"kind": "watching"}, 404 | "user": { 405 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/1160/tombo_avatars/master/d607b56162ae63bf33c460c9c88330a08303a206.jpg", 406 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/1160/tombo_background_images/master/6a563f8dfb602790a92e7acd78b83cfa025bd73e.jpg", 407 | "created_at": "2015-10-16T17:16:42.743Z", 408 | "description": "絵を描きます /佐世保鎮守府/ 猫飼い / 普段はwebプログラマーやってます #python #django / Pixiv: http://www.pixiv.net/member.php?id=139100 見てるアニメ: https://annict.com/@kk6", 409 | "id": 1229, 410 | "name": "あしやひろ", 411 | "records_count": 1470, 412 | "url": "https://twitter.com/kk6", 413 | "username": "kk6", 414 | }, 415 | "work": { 416 | "episodes_count": 11, 417 | "id": 4998, 418 | "media": "tv", 419 | "media_text": "TV", 420 | "official_site_url": "http://gabdro.com/", 421 | "released_on": "", 422 | "released_on_about": "", 423 | "season_name": "2017-winter", 424 | "season_name_text": "2017年冬", 425 | "title": "ガヴリールドロップアウト", 426 | "title_kana": "がゔりーるどろっぷあうと", 427 | "twitter_hashtag": "gabdro", 428 | "twitter_username": "gabdroanime", 429 | "watchers_count": 444, 430 | "wikipedia_url": "https://ja.wikipedia.org/wiki/%E3%82%AC%E3%83%B4%E3%83%AA%E3%83%BC%E3%83%AB%E3%83%89%E3%83%AD%E3%83%83%E3%83%97%E3%82%A2%E3%82%A6%E3%83%88", 431 | }, 432 | } 433 | 434 | from annict.models import Activity 435 | 436 | activity = Activity.parse(None, json) 437 | assert activity.__repr__() == "" 438 | assert activity.status == {"kind": "watching"} 439 | 440 | 441 | class TestWorkModel: 442 | @responses.activate 443 | def test_set_status(self, api_factory): 444 | from annict.models import Work 445 | 446 | responses.add( 447 | responses.POST, 448 | "https://api.annict.com/v1/me/statuses", 449 | body=None, 450 | status=204, 451 | ) 452 | api = api_factory.create() 453 | work = Work.parse(api, {"id": 1}) 454 | result = work.set_status("watching") 455 | assert result 456 | r = urlparse(responses.calls[0].request.url) 457 | assert r.path == "/v1/me/statuses" 458 | assert parse_qs(r.query) == { 459 | "work_id": ["1"], 460 | "kind": ["watching"], 461 | "access_token": ["dummy_token"], 462 | } 463 | 464 | @pytest.mark.parametrize( 465 | "episodes,numbers,expected", 466 | [ 467 | (["#1", "#2", "#3", "#4", "#5"], (2, 4), ["#2", "#4"]), 468 | (["#1", "#2", "#3", "#4", "#5"], (-4, -2), ["#1", "#3"]), 469 | pytest.param( 470 | ["#1"], (2,), None, marks=pytest.mark.xfail(raises=IndexError) 471 | ), 472 | ], 473 | ) 474 | def test_select_episodes(self, api_factory, episodes, numbers, expected): 475 | from annict.models import Work 476 | 477 | api = api_factory.create() 478 | work = Work.parse(api, {}) 479 | work._episodes = episodes 480 | result = work.select_episodes(*numbers) 481 | assert result == expected 482 | 483 | @pytest.mark.parametrize( 484 | "episodes,number,expected", 485 | [ 486 | (["#1", "#2", "#3", "#4", "#5"], 2, "#2"), 487 | (["#1", "#2", "#3", "#4", "#5"], -2, "#3"), 488 | pytest.param(["#1"], 2, None, marks=pytest.mark.xfail(raises=IndexError)), 489 | ], 490 | ) 491 | def test_get_episode(self, api_factory, episodes, number, expected): 492 | from annict.models import Work 493 | 494 | api = api_factory.create() 495 | work = Work.parse(api, {}) 496 | work._episodes = episodes 497 | result = work.get_episode(number) 498 | assert result == expected 499 | 500 | 501 | class TestUserModel: 502 | @responses.activate 503 | def test_following(self, api_factory): 504 | from annict.models import User 505 | 506 | json = """ 507 | { 508 | "users": [ 509 | { 510 | "id": 3, 511 | "username": "builtlast", 512 | "name": "岩永勇輝 (Creasty)", 513 | "description": "Web やってる大学生\\nプログラミングとかデザインとか\\n価値を生み出せるようになりたい\\n\\nアルバイト@FICC\\n\\nC / Obj-C / Ruby / Haskell / PHP / CoffeeScript / VimScript / Photoshop / Illustrator", 514 | "url": null, 515 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/2/tombo_avatars/master/cc301ca5c5e13399144c79daa4e4727b783676de.jpg?1428129519", 516 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/2/tombo_avatars/master/cc301ca5c5e13399144c79daa4e4727b783676de.jpg?1428129519", 517 | "records_count": 0, 518 | "created_at": "2014-03-04T09:32:25.000Z" 519 | }, 520 | { 521 | "id": 4, 522 | "username": "pataiji", 523 | "name": "PATAIJI", 524 | "description": "FICC inc.ベースやってます。カブに乗ってます。AWSすごい良い。Railsすごい楽。", 525 | "url": null, 526 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/3/tombo_avatars/master/33ce537a4cf38f71b509f295f2afa3291c281dcf.jpg?1428129521", 527 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/3/tombo_avatars/master/33ce537a4cf38f71b509f295f2afa3291c281dcf.jpg?1428129521", 528 | "records_count": 0, 529 | "created_at": "2014-03-04T09:32:28.000Z" 530 | } 531 | ], 532 | "total_count": 274, 533 | "next_page": 2, 534 | "prev_page": null 535 | } 536 | """ 537 | responses.add( 538 | responses.GET, 539 | "https://api.annict.com/v1/following", 540 | body=json, 541 | status=200, 542 | content_type="application/json", 543 | ) 544 | api = api_factory.create() 545 | user = User.parse(api, {"id": 1}) 546 | results = user.following() 547 | assert results[0].username == "builtlast" 548 | assert results[1].username == "pataiji" 549 | r = urlparse(responses.calls[0].request.url) 550 | assert r.path == "/v1/following" 551 | assert parse_qs(r.query) == { 552 | "filter_user_id": ["1"], 553 | "access_token": ["dummy_token"], 554 | } 555 | 556 | @responses.activate 557 | def test_followers(self, api_factory): 558 | from annict.models import User 559 | 560 | json = """ 561 | { 562 | "users": [ 563 | { 564 | "id": 7, 565 | "username": "akirafukuoka", 566 | "name": "akirafukuoka", 567 | "description": "FICC inc. http://www.ficc.jp クリエイティブディレクター。RAW-Fi http://raw-fi.com @raw_fi もよろしくお願いします。", 568 | "url": null, 569 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/6/tombo_avatars/master/480862747fc5f7152a031e24f0c0374dc71c539a.jpg?1431596794", 570 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/6/tombo_background_images/master/7e258e0189e9ee38f4dc0c57b2c9f6b39dd2cd95.jpg?1431596795", 571 | "records_count": 260, 572 | "created_at": "2014-03-10T04:11:54.000Z" 573 | }, 574 | { 575 | "id": 8, 576 | "username": "310u8", 577 | "name": "Daisuke Nagai", 578 | "description": "歌って踊れるWebデザイナーです", 579 | "url": null, 580 | "avatar_url": "https://api-assets.annict.com/paperclip/profiles/7/tombo_avatars/master/cd7b66919fea1952e63855632665812839e2a394.jpg?1428129527", 581 | "background_image_url": "https://api-assets.annict.com/paperclip/profiles/7/tombo_avatars/master/cd7b66919fea1952e63855632665812839e2a394.jpg?1428129527", 582 | "records_count": 739, 583 | "created_at": "2014-03-10T04:11:55.000Z" 584 | } 585 | ], 586 | "total_count": 191, 587 | "next_page": 2, 588 | "prev_page": null 589 | } 590 | """ 591 | responses.add( 592 | responses.GET, 593 | "https://api.annict.com/v1/followers", 594 | body=json, 595 | status=200, 596 | content_type="application/json", 597 | ) 598 | api = api_factory.create() 599 | user = User.parse(api, {"id": 1}) 600 | results = user.followers() 601 | assert results[0].username == "akirafukuoka" 602 | assert results[1].username == "310u8" 603 | r = urlparse(responses.calls[0].request.url) 604 | assert r.path == "/v1/followers" 605 | assert parse_qs(r.query) == { 606 | "filter_user_id": ["1"], 607 | "access_token": ["dummy_token"], 608 | } 609 | -------------------------------------------------------------------------------- /tests/test_parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class DummyModel(object): 5 | @classmethod 6 | def parse(cls, api, json): 7 | return "Model.parse is called." 8 | 9 | @classmethod 10 | def parse_list(cls, api, json, payload_type): 11 | return "Model.parse_list is called." 12 | 13 | 14 | def test_called_parse(): 15 | from annict.parsers import ModelParser 16 | 17 | model_mapping = {"model": DummyModel} 18 | parser = ModelParser("api", model_mapping) 19 | json = {} 20 | r = parser.parse(json, "model") 21 | assert r == "Model.parse is called." 22 | 23 | 24 | def test_called_parse_list(): 25 | from annict.parsers import ModelParser 26 | 27 | model_mapping = {"model": DummyModel} 28 | parser = ModelParser("api", model_mapping) 29 | json = {"total_count": 100} 30 | r = parser.parse(json, "model", payload_is_list=True) 31 | assert r == "Model.parse_list is called." 32 | -------------------------------------------------------------------------------- /tests/test_services.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def dummy_api(): 7 | class DummyAPI: 8 | base_url = "https://api.annict.com" 9 | api_version = "v1" 10 | token = "dummy_token" 11 | 12 | return DummyAPI() 13 | 14 | 15 | @pytest.mark.parametrize( 16 | "arg,expected", 17 | [(1, "path/to/1"), (0, "path/to/0"), ("1", "path/to/1"), (None, "path/to")], 18 | ) 19 | def test_build_path(arg, expected, dummy_api): 20 | from annict.services import APIMethod 21 | 22 | api_method = APIMethod(api=dummy_api, path="path/to", method="GET") 23 | api_method.build_path(arg) 24 | assert api_method.path == expected 25 | 26 | 27 | def test_build_url(dummy_api): 28 | from annict.services import APIMethod 29 | 30 | api_method = APIMethod(api=dummy_api, path="path/to", method="GET") 31 | assert api_method.build_url() == "https://api.annict.com/v1/path/to" 32 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | 5 | @pytest.mark.parametrize( 6 | "arg,expected", 7 | [ 8 | ("string", "string"), 9 | (1, "1"), 10 | (4.5, "4.5"), 11 | ([1, 2], "1,2"), 12 | ((2, 3), "2,3"), 13 | ({4, 5}, "4,5"), 14 | ], 15 | ) 16 | def test_stringify(arg, expected): 17 | from annict.utils import stringify 18 | 19 | assert stringify(arg) == expected 20 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist = True 3 | envlist = py36, py37 4 | 5 | [testenv] 6 | whitelist_externals = poetry 7 | skip_install = true 8 | commands = 9 | poetry install -v 10 | poetry run pytest tests/ 11 | --------------------------------------------------------------------------------