├── .flake8 ├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── CNAME ├── about │ ├── contributing.md │ ├── installation.md │ ├── license.md │ └── release-notes.md ├── articles │ ├── index.md │ └── release.md ├── diagram.png ├── imgs │ ├── introducing-1.png │ ├── introducing-2.png │ ├── introducing-3.png │ ├── introducing-4.png │ ├── runinglog.png │ ├── runningitems.png │ ├── runningresult.png │ ├── runningstatus.png │ └── step-0-1.png ├── index.md ├── logo.png ├── quickstart.md ├── topics │ ├── api.md │ ├── cache.md │ ├── item.md │ ├── selector.md │ ├── settings.md │ └── storage.md └── tutorials │ ├── introducing.md │ ├── step0-creating-new-project.md │ ├── step1-global-settings.md │ ├── step2-redis.md │ ├── step3-sqlite3.md │ ├── step4-defining-items.md │ └── step5-deploy.md ├── examples ├── click │ ├── app.py │ ├── static │ │ └── main.js │ └── templates │ │ └── index.html └── hackernews_page.py ├── mkdocs.yml ├── poetry.lock ├── pyproject.toml ├── tests └── test_toapi.py └── toapi ├── __init__.py ├── api.py ├── cli.py ├── item.py └── log.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 80 3 | max-complexity = 10 4 | exclude = 5 | __pycache__, 6 | __init__.py, 7 | build, 8 | .git, 9 | .gitlab, 10 | .eggs, 11 | *.egg-info, 12 | *.pyc, 13 | tmp 14 | select = C,E,F,W,B,B950 15 | ignore = E501,E203,W503,C407,C901 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | .html/ 7 | # C extensions 8 | *.so 9 | .pytest_cache/ 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | env27/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | .venv 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | /tests/data.sqlite 105 | /examples/toapi-pic/data.sqlite 106 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/timothycrosley/isort 3 | rev: 5.7.0 4 | hooks: 5 | - id: isort 6 | - repo: https://github.com/psf/black 7 | rev: 20.8b1 8 | hooks: 9 | - id: black 10 | - repo: https://gitlab.com/pycqa/flake8 11 | rev: 3.8.4 12 | hooks: 13 | - id: flake8 14 | additional_dependencies: 15 | [flake8-comprehensions>=3.2.2, flake8-builtins>=1.5.2] 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - "3.8" 5 | install: 6 | - pip install poetry 7 | - poetry install 8 | script: python -m coverage run --source=toapi -m pytest && coverage report 9 | 10 | after_success: 11 | - codecov 12 | branches: 13 | only: 14 | - master -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Elliot Gao 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Toapi 2 | 3 | [![Build](https://travis-ci.org/gaojiuli/toapi.svg?branch=master)](https://travis-ci.org/gaojiuli/toapi) 4 | [![Coverage](https://codecov.io/gh/gaojiuli/toapi/branch/master/graph/badge.svg)](https://codecov.io/gh/gaojiuli/toapi) 5 | [![Python](https://img.shields.io/pypi/pyversions/toapi.svg)](https://pypi.python.org/pypi/toapi/) 6 | [![Version](https://img.shields.io/pypi/v/toapi.svg)](https://pypi.python.org/pypi/toapi/) 7 | [![License](https://img.shields.io/pypi/l/toapi.svg)](https://pypi.python.org/pypi/toapi/) 8 | 9 | ## Overview 10 | 11 | Toapi give you the ability to make every web site provides APIs. 12 | 13 | - v1.0.0 Documentation: [http://www.toapi.org](http://www.toapi.org) 14 | - Awesome: [https://github.com/toapi/awesome-toapi](https://github.com/toapi/awesome-toapi) 15 | - Organization: [https://github.com/toapi](https://github.com/toapi) 16 | 17 | ## Features 18 | 19 | - Automatic converting HTML web site to API service. 20 | - Automatic caching every page of source site. 21 | - Automatic caching every request. 22 | - Support merging multiple web sites into one API service. 23 | 24 | ## Get Started 25 | 26 | ### Installation 27 | 28 | ```text 29 | $ pip install toapi 30 | ``` 31 | 32 | ### Usage 33 | 34 | create `app.py` and copy the code: 35 | 36 | ```python 37 | from flask import request 38 | from htmlparsing import Attr, Text 39 | from toapi import Api, Item 40 | 41 | api = Api() 42 | 43 | 44 | @api.site('https://news.ycombinator.com') 45 | @api.list('.athing') 46 | @api.route('/posts?page={page}', '/news?p={page}') 47 | @api.route('/posts', '/news?p=1') 48 | class Post(Item): 49 | url = Attr('.storylink', 'href') 50 | title = Text('.storylink') 51 | 52 | 53 | @api.site('https://news.ycombinator.com') 54 | @api.route('/posts?page={page}', '/news?p={page}') 55 | @api.route('/posts', '/news?p=1') 56 | class Page(Item): 57 | next_page = Attr('.morelink', 'href') 58 | 59 | def clean_next_page(self, value): 60 | return api.convert_string('/' + value, '/news?p={page}', request.host_url.strip('/') + '/posts?page={page}') 61 | 62 | 63 | api.run(debug=True, host='0.0.0.0', port=5000) 64 | ``` 65 | 66 | run `python app.py` 67 | 68 | then open your browser and visit `http://127.0.0.1:5000/posts?page=1` 69 | 70 | you will get the result like: 71 | 72 | ```json 73 | { 74 | "Page": { 75 | "next_page": "http://127.0.0.1:5000/posts?page=2" 76 | }, 77 | "Post": [ 78 | { 79 | "title": "Mathematicians Crack the Cursed Curve", 80 | "url": "https://www.quantamagazine.org/mathematicians-crack-the-cursed-curve-20171207/" 81 | }, 82 | { 83 | "title": "Stuffing a Tesla Drivetrain into a 1981 Honda Accord", 84 | "url": "https://jalopnik.com/this-glorious-madman-stuffed-a-p85-tesla-drivetrain-int-1823461909" 85 | } 86 | ] 87 | } 88 | ``` 89 | 90 | 91 | ## Contributing 92 | 93 | Write code and test code and pull request. 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | www.toapi.org 2 | -------------------------------------------------------------------------------- /docs/about/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Toapi 2 | 3 | An introduction to contributing to the Toapi project. 4 | 5 | The Toapi project welcomes, and depends, on contributions from developers and 6 | users in the open source community. Contributions can be made in a number of 7 | ways, a few examples are: 8 | 9 | - Code patches via pull requests 10 | - Documentation improvements 11 | - Bug reports and patch reviews 12 | 13 | ## Donate 14 | 15 | [BTC(0.005)](https://blockchain.info/payment_request?address=18QChGWtGWAQyXKQVmnpKf7pdm7mYxoYTQ&amount=0.005&message=For%20Github%20Projects.) 16 | 17 | ## Code of Conduct 18 | 19 | Everyone interacting in the Toapi project's codebases, issue trackers, chat 20 | rooms, and mailing lists is expected to follow the [PyPA Code of Conduct]. 21 | 22 | ## Reporting an Issue 23 | 24 | Please include as much detail as you can. Let us know your platform and Toapi 25 | version. If the problem is visual (for example a theme or design issue) please 26 | add a screenshot and if you get an error please include the full error and 27 | traceback. 28 | 29 | ## Installing for Development 30 | 31 | Run the following command. It is **strongly** recommended that you do 32 | this within a [virtualenv]. 33 | 34 | ```bash 35 | git clone https://github.com/gaojiuli/toapi 36 | cd toapi 37 | pip install --editable . 38 | ``` 39 | 40 | This will install Toapi in development mode which binds the `toapi` command 41 | to the git repository. 42 | 43 | ## Running the tests 44 | 45 | To run the tests, it is recommended that you use [pytest]. This just needs 46 | to be pip installed and then the test suite can be ran for Toapi but running 47 | the command `pytest` in the root of your Toapi repository. 48 | 49 | It will attempt to run the tests against all of the Python versions we 50 | support. So don't be concerned if you are missing some and they fail. The rest 51 | will be verified by [Travis] when you submit a pull request. 52 | 53 | ## Submitting Pull Requests 54 | 55 | Once you are happy with your changes or you are ready for some feedback, push 56 | it to your fork and send a pull request. For a change to be accepted it will 57 | most likely need to have tests and documentation if it is a new feature. 58 | 59 | [virtualenv]: https://virtualenv.pypa.io/en/latest/userguide.html 60 | [pytest]: https://docs.pytest.org/en/latest/ 61 | [travis]: https://travis-ci.org/repositories 62 | [PyPA Code of Conduct]: https://www.pypa.io/en/latest/code-of-conduct/ 63 | -------------------------------------------------------------------------------- /docs/about/installation.md: -------------------------------------------------------------------------------- 1 | ### Manual Installation 2 | 3 | In order to manually install Toapi you'll need [Python] installed on your 4 | system, as well as the Python package manager, [pip]. You can check if you have 5 | these already installed from the command line: 6 | 7 | ```text 8 | $ python --version 9 | Python 3.5.2 10 | $ pip --version 11 | pip 9.0.1 12 | ``` 13 | 14 | Toapi supports Python3.5+. 15 | 16 | #### Installing Python 17 | 18 | Install [Python] by downloading an installer appropriate for your system from 19 | [python.org] and running it. 20 | 21 | !!! Note 22 | 23 | If you are installing Python on Windows, be sure to check the box to have 24 | Python added to your PATH if the installer offers such an option (it's 25 | normally off by default). 26 | 27 | [python.org]: https://www.python.org/downloads/ 28 | 29 | #### Installing pip 30 | 31 | If you're using a recent version of Python, the Python package manager, [pip], 32 | is most likely installed by default. However, you may need to upgrade pip to the 33 | lasted version: 34 | 35 | ```text 36 | pip install --upgrade pip 37 | ``` 38 | 39 | If you need to install [pip] for the first time, download [get-pip.py]. 40 | Then run the following command to install it: 41 | 42 | ```text 43 | python get-pip.py 44 | ``` 45 | 46 | #### Installing Toapi 47 | 48 | Install the `toapi` package using pip: 49 | 50 | ```text 51 | pip install toapi 52 | ``` 53 | 54 | You should now have the `toapi` command installed on your system. Run `toapi 55 | --version` to check that everything worked okay. 56 | 57 | ```text 58 | $ toapi --version 59 | toapi, version 1.0.0 60 | ``` 61 | 62 | !!! Note 63 | If you are using Windows, some of the above commands may not work 64 | out-of-the-box. 65 | 66 | A quick solution may be to preface every Python command with `python -m` 67 | like this: 68 | 69 | python -m pip install toapi 70 | python -m api 71 | 72 | For a more permanent solution, you may need to edit your `PATH` environment 73 | variable to include the `Scripts` directory of your Python installation. 74 | Recent versions of Python include a script to do this for you. Navigate to 75 | your Python installation directory (for example `C:\Python34\`), open the 76 | `Tools`, then `Scripts` folder, and run the `win_add2path.py` file by double 77 | clicking on it. Alternatively, you can [download][a2p] the script and run it 78 | (`python win_add2path.py`). 79 | 80 | [a2p]: https://svn.python.org/projects/python/trunk/Tools/scripts/win_add2path.py 81 | 82 | --- 83 | 84 | [pip]: http://pip.readthedocs.io/en/stable/installing/ 85 | [get-pip.py]: https://bootstrap.pypa.io/get-pip.py 86 | [Python]: https://www.python.org/ 87 | -------------------------------------------------------------------------------- /docs/about/license.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 Jiuli Gao 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /docs/about/release-notes.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | --- 4 | 5 | ## Upgrading 6 | 7 | To upgrade Toapi to the latest version, use pip: 8 | 9 | ```text 10 | pip install -U toapi 11 | ``` 12 | 13 | 14 | You can determine your currently installed version using `toapi --version`: 15 | 16 | ```text 17 | $ toapi --version 18 | toapi, version 1.0.0 19 | ``` 20 | 21 | ## Maintenance team 22 | 23 | - [@gaojiuli](https://github.com/gaojiuli/) 24 | - [@howie6879](https://github.com/howie6879/) 25 | - [@wuqiangroy](https://github.com/wuqiangroy/) 26 | 27 | 28 | ## Changelog 29 | 30 | ### 1.0.0 (2017-12-26) 31 | 32 | - Initial release -------------------------------------------------------------------------------- /docs/articles/index.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotgao2/toapi/6ae043cab28d16beb0be1bd9b1cd0fdc9c19baa6/docs/articles/index.md -------------------------------------------------------------------------------- /docs/articles/release.md: -------------------------------------------------------------------------------- 1 | ## Toapi released! You will never lack of data sources! 2 | 3 | #### Brief Introduction 4 | 5 | Toapi is a framework for converting a website to an api service. 6 | 7 | Whenever I want to start an app or website, I always have a problem with no data sources. 8 | No APi, no database. But I often found the data I need on the website. I need the data. 9 | 10 | So I write [Toapi](https://github.com/gaojiuli/toapi) for converting website to api service. 11 | The [Toapi](https://github.com/gaojiuli/toapi) could help me fetch any data I want. 12 | 13 | - Project: [https://github.com/gaojiuli/toapi](https://github.com/gaojiuli/toapi) 14 | - Organization (welcome to join us): [https://github.com/toapi](https://github.com/toapi) 15 | - Document: [http://www.toapi.org/](http://www.toapi.org/) 16 | 17 | What the result looks like? 18 | 19 | ```json 20 | // http://127.0.0.1:5000/pic/?q=coffee 21 | 22 | { 23 | "Pixabay": [ 24 | { 25 | "img": "https://cdn.pixabay.com/photo/2017/06/21/05/28/coffee-2426110__340.png" 26 | }, 27 | { 28 | "img": "/static/img/blank.gif" 29 | } 30 | ], 31 | "Pexels": [ 32 | { 33 | "img": "https://images.pexels.com/photos/302899/pexels-photo-302899.jpeg?h=350&auto=compress&cs=tinysrgb" 34 | }, 35 | { 36 | "img": "https://images.pexels.com/photos/34085/pexels-photo.jpg?h=350&auto=compress&cs=tinysrgb" 37 | } 38 | ] 39 | } 40 | ``` 41 | 42 | #### How 43 | 44 | ``` python 45 | from toapi import XPath, Item, Api, Settings 46 | 47 | 48 | class MySettings(Settings): 49 | web = { 50 | "with_ajax": True, 51 | "request_config": {}, 52 | "headers": None 53 | } 54 | 55 | api = Api('https://news.ycombinator.com', settings=MySettings) 56 | 57 | class Post(Item): 58 | url = XPath('//a[@class="storylink"]/@href') 59 | title = XPath('//a[@class="storylink"]/text()') 60 | 61 | class Meta: 62 | source = XPath('//tr[@class="athing"]') 63 | route = {'/news?p=:page': '/news?p=:page'} 64 | 65 | class Page(Item): 66 | next_page = XPath('//a[@class="morelink"]/@href') 67 | 68 | class Meta: 69 | source = None 70 | route = {'/news?p=:page': '/news?p=:page'} 71 | 72 | def clean_next_page(self, next_page): 73 | return "http://127.0.0.1:5000/" + next_page 74 | 75 | api.register(Page) 76 | api.register(Post) 77 | 78 | api.serve() 79 | # Visit http://127.0.0.1:5000/news?p=1 80 | ``` 81 | 82 | As you can see. The only thing you should to is writing very little code that is necessary. 83 | Then you finish the APIs of [hackernews](https://news.ycombinator.com). 84 | 85 | There are some templates: 86 | 87 | - [toapi-search](https://github.com/toapi/toapi-search): Baidu, Bing, Google, Sogou aggregation API 88 | - [toapi-one](https://github.com/toapi/toapi-one) API service of One app 89 | - [toapi-ebooks](https://github.com/toapi/toapi-ebooks): API service of IT e-book source 90 | - [toapi-instagram](https://github.com/toapi/toapi-instagram): API service of Instagram 91 | - [toapi-pic](https://github.com/toapi/toapi-pic): API service of HD photos website 92 | - etc. 93 | 94 | ### What else 95 | 96 | Toapi development team([@gaojiuli](https://github.com/gaojiuli/), [@howie6879](https://github.com/howie6879/), [@wuqiangroy](https://github.com/wuqiangroy/)) -------------------------------------------------------------------------------- /docs/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotgao2/toapi/6ae043cab28d16beb0be1bd9b1cd0fdc9c19baa6/docs/diagram.png -------------------------------------------------------------------------------- /docs/imgs/introducing-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotgao2/toapi/6ae043cab28d16beb0be1bd9b1cd0fdc9c19baa6/docs/imgs/introducing-1.png -------------------------------------------------------------------------------- /docs/imgs/introducing-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotgao2/toapi/6ae043cab28d16beb0be1bd9b1cd0fdc9c19baa6/docs/imgs/introducing-2.png -------------------------------------------------------------------------------- /docs/imgs/introducing-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotgao2/toapi/6ae043cab28d16beb0be1bd9b1cd0fdc9c19baa6/docs/imgs/introducing-3.png -------------------------------------------------------------------------------- /docs/imgs/introducing-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotgao2/toapi/6ae043cab28d16beb0be1bd9b1cd0fdc9c19baa6/docs/imgs/introducing-4.png -------------------------------------------------------------------------------- /docs/imgs/runinglog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotgao2/toapi/6ae043cab28d16beb0be1bd9b1cd0fdc9c19baa6/docs/imgs/runinglog.png -------------------------------------------------------------------------------- /docs/imgs/runningitems.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotgao2/toapi/6ae043cab28d16beb0be1bd9b1cd0fdc9c19baa6/docs/imgs/runningitems.png -------------------------------------------------------------------------------- /docs/imgs/runningresult.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotgao2/toapi/6ae043cab28d16beb0be1bd9b1cd0fdc9c19baa6/docs/imgs/runningresult.png -------------------------------------------------------------------------------- /docs/imgs/runningstatus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotgao2/toapi/6ae043cab28d16beb0be1bd9b1cd0fdc9c19baa6/docs/imgs/runningstatus.png -------------------------------------------------------------------------------- /docs/imgs/step-0-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotgao2/toapi/6ae043cab28d16beb0be1bd9b1cd0fdc9c19baa6/docs/imgs/step-0-1.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Toapi 2 | 3 | Every web site provides APIs. 4 | 5 | [![Build](https://travis-ci.org/gaojiuli/toapi.svg?branch=master)](https://travis-ci.org/gaojiuli/toapi) 6 | [![Python](https://img.shields.io/pypi/pyversions/toapi.svg)](https://pypi.python.org/pypi/toapi/) 7 | [![Version](https://img.shields.io/pypi/v/toapi.svg)](https://pypi.python.org/pypi/toapi/) 8 | [![License](https://img.shields.io/pypi/l/toapi.svg)](https://pypi.python.org/pypi/toapi/) 9 | 10 | ## Overview 11 | 12 | Toapi is a **clever**, **simple** and **fast** library letting any web site provide APIs. 13 | In the past, we would crawl a website and store the data to build an API around it. 14 | What's more we then had to manage updating the data. 15 | 16 | This library make things easy. The only thing you need to do is defining your data structures 17 | that will be shared as an api service automatically. 18 | 19 | Documentation: [Toapi Documentation](http://www.toapi.org) 20 | 21 | ## Code Snippets: 22 | 23 | ```python 24 | from toapi import XPath, Item, Api 25 | from toapi import Settings 26 | 27 | class MySettings(Settings): 28 | web = { 29 | "with_ajax": False 30 | } 31 | 32 | api = Api('https://news.ycombinator.com/', settings=MySettings) 33 | 34 | class Post(Item): 35 | url = XPath('//a[@class="storylink"]/@href') 36 | title = XPath('//a[@class="storylink"]/text()') 37 | 38 | class Meta: 39 | source = XPath('//tr[@class="athing"]') 40 | route = {'/news?page=:page':'/news?p=:page'} 41 | 42 | class Page(Item): 43 | next_page = XPath('//a[@class="morelink"]/@href') 44 | 45 | class Meta: 46 | source = None 47 | route = {'/news?page=:page':'/news?p=:page'} 48 | 49 | def clean_next_page(self, next_page): 50 | return "http://127.0.0.1:5000/" + str(next_page) 51 | 52 | api.register(Post) 53 | api.register(Page) 54 | 55 | api.serve() 56 | 57 | # Visit: http://127.0.0.1:5000/ 58 | ``` 59 | 60 | ## Diagram 61 | 62 | [![asciicast](https://asciinema.org/a/shet2Ba9d4muCbZ6C3f56EbAt.png)](https://asciinema.org/a/shet2Ba9d4muCbZ6C3f56EbAt) 63 | 64 | 65 | ![Toapi](./diagram.png) 66 | 67 | 68 | - Sending only one request to source web site with the same url. 69 | - Most of the data fetched from cache and storage. 70 | - Getting HTML from storage when the cache expired. 71 | - Getting HTML from source site when the storage expired. 72 | 73 | ## Get Started 74 | 75 | ### Installation 76 | 77 | ```text 78 | $ pip install toapi 79 | $ toapi -v 80 | toapi, version 0.1.12 81 | ``` 82 | 83 | ### New Project 84 | 85 | ```text 86 | $ toapi new api 87 | 2017/12/14 09:16:54 [New project] OK Creating project directory "api" 88 | Cloning into 'api'... 89 | remote: Counting objects: 10, done. 90 | remote: Compressing objects: 100% (8/8), done. 91 | remote: Total 10 (delta 1), reused 10 (delta 1), pack-reused 0 92 | Unpacking objects: 100% (10/10), done. 93 | Checking connectivity... done. 94 | 2017/12/14 09:16:56 [New project] OK Success! 95 | 96 | cd api 97 | toapi run 98 | 99 | ``` 100 | 101 | ### Run 102 | 103 | In the directory of 'api' created above. Run the command line as follows. 104 | 105 | ```text 106 | $ toapi run 107 | 2017/12/14 09:27:18 [Serving ] OK http://127.0.0.1:5000 108 | ``` 109 | 110 | Then, everything is done. Visit http://127.0.0.1:5000 in your browser! 111 | 112 | ### Deploy 113 | 114 | A Toapi app is a flask app. So you can deploy it as follows: 115 | 116 | 117 | > While lightweight and easy to use, Flask’s built-in server is not suitable for production as it doesn’t scale well and by default serves only one request at a time. Some of the options available for properly running Flask in production are documented here. 118 | 119 | > If you want to deploy your Flask application to a WSGI server not listed here, look up the server documentation about how to use a WSGI app with it. Just remember that your Flask application object is the actual WSGI application. 120 | 121 | [Deployment Options — Flask Documentation (0.12)](http://flask.pocoo.org/docs/0.12/deploying/) 122 | 123 | ## Screenshots 124 | 125 | ```python 126 | toapi new toapi/toapi-pic 127 | cd toapi-pic 128 | toapi run 129 | ``` 130 | 131 | ### Running Log 132 | 133 | ![Running Log](./imgs/runinglog.png) 134 | 135 | ### Running Items 136 | 137 | > http://127.0.0.1:5000/_items 138 | 139 | ``` json 140 | 141 | { 142 | "/pic/?q=:key": [ 143 | "Pixabay", 144 | "Pexels" 145 | ] 146 | } 147 | 148 | ``` 149 | 150 | ### Running Status 151 | 152 | > http://127.0.0.1:5000/_status 153 | 154 | ``` json 155 | 156 | { 157 | "cache_get": 2, 158 | "cache_set": 2, 159 | "received": 4, 160 | "sent": 2, 161 | "storage_get": 1, 162 | "storage_set": 2 163 | } 164 | 165 | ``` 166 | 167 | ### Running Results 168 | 169 | > http://127.0.0.1:5000/pic/?q=coffee 170 | 171 | ``` json 172 | 173 | { 174 | "Pixabay": [ 175 | { 176 | "img": "https://cdn.pixabay.com/photo/2017/06/21/05/28/coffee-2426110__340.png" 177 | }, 178 | { 179 | "img": "/static/img/blank.gif" 180 | } 181 | ], 182 | "Pexels": [ 183 | { 184 | "img": "https://images.pexels.com/photos/302899/pexels-photo-302899.jpeg?h=350&auto=compress&cs=tinysrgb" 185 | }, 186 | { 187 | "img": "https://images.pexels.com/photos/34085/pexels-photo.jpg?h=350&auto=compress&cs=tinysrgb" 188 | } 189 | ] 190 | } 191 | 192 | ``` 193 | 194 | ## Features 195 | 196 | ### Multiple caching 197 | 198 | Toapi use cache to prevent repeated parsing and use storage to prevent sending request. 199 | 200 | ### Multiple sites 201 | 202 | A toapi app has an ability to gather pages of multiple websites and convert them to easy to use APIs 203 | 204 | ### Multiple Templates & Applications 205 | 206 | Any application created by toapi could be shared to others. 207 | 208 | ### Easy to deploy. 209 | 210 | A toapi app is a standard flask app, so that you can deploy your app as deploying a flask app. 211 | 212 | ### Status Monitor 213 | 214 | A toapi app will automatically count kinds of states of itself and you can visit the states whenever you want. 215 | 216 | ## Getting help 217 | 218 | To get help with Toapi, please use the [GitHub issues] 219 | 220 | [GitHub issues]: https://github.com/gaojiuli/toapi/issues 221 | [GitHub project pages]: https://help.github.com/articles/creating-project-pages-manually/ 222 | [pip]: http://pip.readthedocs.io/en/stable/installing/ 223 | [Python]: https://www.python.org/ 224 | 225 | ## Todo 226 | 227 | 1. Checking system every time running the app. 228 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotgao2/toapi/6ae043cab28d16beb0be1bd9b1cd0fdc9c19baa6/docs/logo.png -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotgao2/toapi/6ae043cab28d16beb0be1bd9b1cd0fdc9c19baa6/docs/quickstart.md -------------------------------------------------------------------------------- /docs/topics/api.md: -------------------------------------------------------------------------------- 1 | Api is the whole program entrance which connects items, cache, storage, 2 | handles the request from user and fetches html from source sites. For 3 | example: 4 | 5 | ```python 6 | from toapi import XPath, Item, Api 7 | 8 | api = Api(base_url='https://news.ycombinator.com') 9 | 10 | 11 | class Post(Item): 12 | url = XPath('//a[@class="storylink"]/@href') 13 | title = XPath('//a[@class="storylink"]/text()') 14 | 15 | class Meta: 16 | source = XPath('//tr[@class="athing"]') 17 | route = '/' 18 | 19 | 20 | api.register(Post) 21 | api.serve() 22 | ``` 23 | 24 | ## Arguments 25 | 26 | ### `base_url` 27 | 28 | The argument `base_url` is hostname of source web site. `default = None` 29 | 30 | ### `settings` 31 | 32 | The argument `settings` is the global configuration of the whole app. `default = None` means use default settings. 33 | 34 | --- 35 | 36 | ## Methods 37 | 38 | ### .register(self, item) 39 | 40 | Register an item so that we could parse it. 41 | 42 | 43 | ### .serve(self, ip='127.0.0.1', port=5000, **options) 44 | 45 | Start to serve. 46 | 47 | 48 | ### .parse(self, path, params=None, **kwargs) 49 | 50 | Parse items if the path is defined in registered items. 51 | 52 | 53 | ### .fetch_page_source(self, url, item, params=None, **kwargs) 54 | 55 | Fetch html from an url. 56 | 57 | 58 | ### .get_browser(self, settings, item_with_ajax=False) 59 | 60 | Init a PhantomJS instance to the Api instance. 61 | 62 | ### .update_status(self, key) 63 | 64 | Update status of Api instance. 65 | 66 | ### .get_status(self, key) 67 | 68 | Get status of Api instance. 69 | 70 | ### .set_cache(self, key, value) 71 | 72 | Set cache. In Api instance, the value usually in type of `dict`. 73 | 74 | ### .get_cache(self, key) 75 | 76 | Get cache. 77 | 78 | ### .set_storage(self, key, value) 79 | 80 | Set storage.In Api instance, the value is usually a HTML. 81 | 82 | ### .get_storage(self, key) 83 | 84 | Get storage. 85 | 86 | ### .parse_item(self, html, item) 87 | 88 | Parse items from HTML. 89 | 90 | 91 | -------------------------------------------------------------------------------- /docs/topics/cache.md: -------------------------------------------------------------------------------- 1 | When you are writing a service, maybe you need to be able to save a piece of JSON data to your system's memory. 2 | 3 | There are three ways to create a cache, which are MemoryCache, RedisCache or MemcachedCache. 4 | 5 | What is the difference between these three ways? 6 | 7 | - [MemoryCache](https://github.com/gaojiuli/toapi/blob/master/toapi/cache/memory_cache.py): easy to configure, but it automatically destroys when the server is stopped. 8 | - [RedisCache](https://github.com/gaojiuli/toapi/blob/master/toapi/cache/redis_cache.py): stable but you have to install Redis. 9 | - [MemcachedCache](https://github.com/gaojiuli/toapi/blob/master/toapi/cache/memcached_cache.py): stable but you have to install Memcached. 10 | 11 | There are two serialization schemes we provide for you: 12 | 13 | - [JsonSerializer](https://github.com/gaojiuli/toapi/blob/master/toapi/cache/serializer.py) 14 | - [PickleSerializer](https://github.com/gaojiuli/toapi/blob/master/toapi/cache/serializer.py) 15 | 16 | ## Core arguments 17 | 18 | `RedisCache` and `MemcachedCache` class constructor must takes these arguments. Some Field classes take additional, field-specific arguments, but the following should always be accepted: 19 | 20 | - host 21 | - port 22 | 23 | !!! Note 24 | `MemoryCache` can be instantiated directly 25 | 26 | ## Methods 27 | 28 | ### .set(self, key, value, ttl=None, **kwargs) 29 | Set the value at key ``key`` to ``value`` 30 | 31 | ### .get(self, key, default=None, **kwargs) 32 | Return the value at key ``name``, or None if the key doesn't exist 33 | 34 | ### .delete(self, key, **kwargs) 35 | Delete one or more keys specified by ``keys`` 36 | 37 | ### .exists(self, key, **kwargs) 38 | Returns a boolean indicating whether key ``name`` exists 39 | 40 | ### .incr(self, key, **kwargs) 41 | Increments the value of ``key`` 42 | 43 | ### .api_cached(self, ttl=None, **kwargs) 44 | This decorator provides a caching mechanism for the data 45 | 46 | - param cache_class: such as RedisCache MemcachedCache MemoryCache 47 | - param ttl: int seconds to store the data 48 | - param serializer: serialize the value 49 | 50 | ## Usage 51 | 52 | These methods can be used with very convenient, first of all, you just need to add one new class which inheritance the [`Toapi.Settings`](https://github.com/gaojiuli/toapi/blob/master/toapi/settings.py). 53 | 54 | Let's take a look at a quick example of using MemoryCache.Start off by adding the following to your `settings.py`: 55 | 56 | ``` python 57 | from toapi.cache import MemoryCache, RedisCache 58 | 59 | 60 | class MyMemorySettings(Settings): 61 | """ 62 | Create custom configuration 63 | """ 64 | cache = { 65 | # If you want to use other classes, just replace it 66 | 'cache_class': MemoryCache, 67 | 'cache_config': {}, 68 | # Default value is JsonSerializer 69 | 'serializer': None, 70 | 'ttl': None 71 | } 72 | 73 | class MyRedisSettings(Settings): 74 | """ 75 | If you want to use Redis, you can create your configuration like this 76 | """ 77 | cache = { 78 | # If you want to use other classes, just replace it 79 | 'cache_class': RedisCache, 80 | 'cache_config': { 81 | 'host': '127.0.0.1', 82 | 'port': 6379, 83 | 'db': 0, 84 | 'password': None 85 | }, 86 | 'serializer': None, 87 | 'ttl': None 88 | } 89 | 90 | ``` 91 | 92 | Next create `cache_demo.py`: 93 | 94 | ``` python 95 | from toapi.cache import cached, CacheSetting 96 | 97 | from settings import MyMemorySettings 98 | 99 | # Create a cache instance 100 | # Or cache_ins = CacheSetting(MyRedisSettings) 101 | cache_ins = CacheSetting(MyMemorySettings) 102 | 103 | # Set the value at key, the key will be automatically deleted after 10s 104 | cache_ins.set(key='name', value='toapi', ttl=10) 105 | 106 | # Return the value at key ``name``, 107 | value = cache_ins.get(key='name') 108 | 109 | # Output 110 | # toapi 111 | ``` 112 | 113 | Once a cache instance has been created, you can use it anywhere to implement data caching. 114 | 115 | Now you know some of the basic operations for using cache_ins, but how can you add a cache to your API service? 116 | 117 | Add the following to your `app.py`: 118 | 119 | ``` python 120 | 121 | from toapi import Api 122 | 123 | from settings import MyMemorySettings 124 | 125 | api = Api('https://www.github.com', settings=MyMemorySettings) 126 | 127 | ``` 128 | 129 | ## How It Works? 130 | 131 | The [`Api`](https://github.com/gaojiuli/toapi/blob/master/toapi/api.py) class will initialize the `cache` attribute based on the value of settings. 132 | 133 | !!! Note 134 | Api class can accept a parameter named settings, if settings is None, the default configuration will take effect 135 | 136 | You can read the basic configuration from [`toapi/settings.py`](https://github.com/gaojiuli/toapi/blob/master/toapi/settings.py) -------------------------------------------------------------------------------- /docs/topics/item.md: -------------------------------------------------------------------------------- 1 | Item is the key to the whole system which determine what is the result and 2 | where is the result. 3 | 4 | ```python 5 | from toapi import XPath, Item 6 | 7 | class MovieList(Item): 8 | __base_url__ = 'http://www.dy2018.com' 9 | 10 | url = XPath('//b//a[@class="ulink"]/@href') 11 | title = XPath('//b//a[@class="ulink"]/text()') 12 | 13 | class Meta: 14 | source = XPath('//table[@class="tbspan"]') 15 | route = {'/movies/?page=1': '/html/gndy/dyzz/', 16 | '/movies/?page=:page': '/html/gndy/dyzz/index_:page.html', 17 | '/movies/': '/html/gndy/dyzz/'} 18 | ``` 19 | 20 | When you visit `http://127.0.0.1:/movies/?page=2`, You could get the item from `http://www.dy2018.com/html/gndy/dyzz/index_2.html` 21 | 22 | As you can see. The fields of item are [selector instances](selector). 23 | And the Meta class determine the basic attributes of item. 24 | 25 | - Meta.source: A section of a HTML, which should contains one complete item. It is a [selector instance](selector) 26 | - Meta.route: The url path regex expression of source site. 27 | 28 | ## Clean Data 29 | 30 | The `clean_{field}` method of item instance is for further processing the returned values. For example: 31 | 32 | ```python 33 | from toapi import XPath, Item 34 | 35 | class Post(Item): 36 | url = XPath('//a[@class="storylink"]/@href') 37 | title = XPath('//a[@class="storylink"]/text()') 38 | 39 | class Meta: 40 | source = XPath('//tr[@class="athing"]') 41 | route = {'/':'/'} 42 | 43 | def clean_url(self, url): 44 | return 'http://127.0.0.1%s' % url 45 | ``` -------------------------------------------------------------------------------- /docs/topics/selector.md: -------------------------------------------------------------------------------- 1 | Selector fields is used to parse field values from HTML. There are three selectors right now: 2 | 3 | - XPath 4 | - Css 5 | - Regex 6 | 7 | ## Core arguments 8 | 9 | Each Selector field class constructor takes at least these arguments. Some Field classes take additional, field-specific arguments, but the following should always be accepted: 10 | 11 | ### `rule` 12 | 13 | The arguments `rule` is the rule of selector which maybe a xpath expression or a css select expression or a regex expression. 14 | 15 | ## XPath Selector 16 | 17 | The rule argument is xpath expression. 18 | 19 | ```python 20 | from toapi import XPath 21 | 22 | field = XPath('//a[@class="user"]/text()') 23 | ``` 24 | 25 | **Signature:** `XPath(rule)` 26 | 27 | --- 28 | 29 | ## Css Selector 30 | 31 | The rule argument is css select expression. 32 | 33 | ```python 34 | from toapi import Css 35 | 36 | field = Css('a.user', attr='href') 37 | ``` 38 | 39 | **Signature:** `Css(rule, attr=None)` 40 | 41 | - `attr` Css select expression can't determine which part ot parse. We need the `attr` argument for that. 42 | 43 | --- 44 | 45 | ## Regex Selector 46 | 47 | The rule argument regex expression. 48 | 49 | ```python 50 | from toapi import Regex 51 | 52 | field = Regex('\d{18}') 53 | ``` 54 | 55 | **Signature:** `Regex(rule)` 56 | 57 | --- -------------------------------------------------------------------------------- /docs/topics/settings.md: -------------------------------------------------------------------------------- 1 | Global settings. 2 | 3 | ```python 4 | import os 5 | from toapi import Api, Settings 6 | from toapi.cache import MemoryCache 7 | 8 | class MySettings(Settings): 9 | """ 10 | Create custom configuration 11 | """ 12 | storage = { 13 | "PATH": os.getcwd(), 14 | "DB_URL": None 15 | } 16 | cache = { 17 | # If you want to use other classes, just replace it 18 | 'cache_class': MemoryCache, 19 | 'cache_config': {}, 20 | # Default value is JsonSerializer 21 | 'serializer': None, 22 | 'ttl': None 23 | } 24 | web = { 25 | "with_ajax": True, 26 | "request_config": {}, 27 | "headers": None 28 | } 29 | 30 | api = Api('https://www.github.com', settings=MySettings) 31 | ``` 32 | 33 | ## Attributes 34 | 35 | ### cache 36 | 37 | Config how the app perform [cache](cache). 38 | 39 | ### storage 40 | 41 | Config how the app perform [storage](storage). 42 | 43 | ### web 44 | 45 | Config how the app perform request. 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /docs/topics/storage.md: -------------------------------------------------------------------------------- 1 | Storage provides 2 ways to you to store your data -- local disk and database. 2 | 3 | ## How to configure them 4 | 5 | ### Local Disk 6 | Your data will be stored in a hidden file called .html which will be created in where programe running. 7 | You can still input the path you want to store in Settings. 8 | 9 | ```python 10 | from toapi import Api, Settings 11 | 12 | 13 | class MySettings(Setting): 14 | '''your own settings''' 15 | 16 | storage = { 17 | "PATH": "/Users/username/toapi", 18 | "DB_URL": None 19 | } 20 | ``` 21 | Sytem will load your path, and create a hidden file -- .html under your path: /Users/username/toapi/.html, and every site will be stored here. 22 | 23 | ### Database 24 | You need a database to save your import data and make it read faster. 25 | To use database, you have to configure it first. 26 | 27 | ```python 28 | from toapi import Api, Settings 29 | 30 | 31 | class MySettings(Setting): 32 | '''your own settings''' 33 | 34 | storage = { 35 | "PATH": "/Users/username/toapi", 36 | "DB_URL": "sqlite:////User/username/toapi/data.sqlite" 37 | } 38 | ``` 39 | warning: if you configure PATH and DB_URL both, system will use database only! 40 | 41 | 42 | ## How to use storage 43 | 44 | It‘s easy and cheeryful to use it, you even do not configure anything. 45 | ```python 46 | from storage import Storage 47 | from api import Settings 48 | 49 | store = Storage(Settings) 50 | url = "www.toapi.org" 51 | html = "html content" 52 | store.save(url, html) 53 | store.get(url) # you can give a expiration 54 | ``` 55 | 56 | ## Attributes 57 | 58 | ### save(url, html) 59 | save() receive two params, url and html. 60 | - url is the url you current surf 61 | - html is the contents you current surf. 62 | 63 | ### get(url, default, expiration) 64 | get() receive three params, url, default and expiration. 65 | - url is easy and simple to know. 66 | - default is the returned value while no result finding in disk or database. 67 | - expiration means you want to get the contents in expired time, if data expired, the system will automatically delete. 68 | -------------------------------------------------------------------------------- /docs/tutorials/introducing.md: -------------------------------------------------------------------------------- 1 | ## Aim 2 | 3 | 4 | Our aim is to build an api server that provides free pictures. Those pictures are fetched from those websites: 5 | 6 | - [https://pixabay.com/](https://pixabay.com/) 7 | - [https://www.pexels.com/](https://www.pexels.com/) 8 | 9 | You can find the source code in [examples/toapi-pic](https://github.com/gaojiuli/toapi/blob/master/examples/toapi-pic) 10 | 11 | ## What it look like? 12 | 13 | 14 | ### Running 15 | 16 | ![](../imgs/introducing-1.png) 17 | 18 | ### Index 19 | 20 | ![](../imgs/introducing-2.png) 21 | 22 | ### Items 23 | 24 | ![](../imgs/introducing-3.png) 25 | 26 | ### Results 27 | 28 | ![](../imgs/introducing-4.png) -------------------------------------------------------------------------------- /docs/tutorials/step0-creating-new-project.md: -------------------------------------------------------------------------------- 1 | ## New Project 2 | 3 | You can run the command 'toapi new' whenever you want to start a new api server. 4 | 5 | ```text 6 | $ toapi new toapi/toapi-pic 7 | 2017/12/26 11:41:38 [New project] OK Creating project directory "toapi-pic" 8 | Cloning into 'toapi-pic'... 9 | remote: Counting objects: 13, done. 10 | remote: Compressing objects: 100% (10/10), done. 11 | remote: Total 13 (delta 2), reused 9 (delta 1), pack-reused 0 12 | Unpacking objects: 100% (13/13), done. 13 | Checking connectivity... done. 14 | 2017/12/26 11:41:40 [New project] OK Success! 15 | 16 | cd toapi-pic 17 | toapi run 18 | 19 | ``` 20 | 21 | This command create a new folder named `toapi-pic`, which include some files: 22 | 23 | ```text 24 | $ tree . 25 | . 26 | ├── app.py 27 | ├── items 28 | │   ├── __init__.py 29 | │   ├── pexels.py 30 | │   └── pixabay.py 31 | ├── README.md 32 | ├── settings.py 33 | └── wsgi.py 34 | 35 | 1 directory, 7 files 36 | ``` 37 | 38 | - app.py: define the app instance. 39 | - settings.py: global configs. 40 | - items: define items you want to extract. 41 | - wsgi.py: expose interface to gunicorn, uwsgi .etc for serving. -------------------------------------------------------------------------------- /docs/tutorials/step1-global-settings.md: -------------------------------------------------------------------------------- 1 | ## Settings 2 | 3 | Settings allow you config cache, storage, request. In our toapi-pic project. 4 | You could see the code: 5 | 6 | ```python 7 | import os 8 | 9 | from toapi.cache import MemoryCache 10 | from toapi.settings import Settings 11 | 12 | class MySettings(Settings): 13 | """ 14 | Create custom configuration 15 | http://www.toapi.org/topics/settings/ 16 | """ 17 | 18 | cache = { 19 | 'cache_class': MemoryCache, 20 | 'cache_config': {}, 21 | 'serializer': None, 22 | 'ttl': None 23 | } 24 | storage = { 25 | "PATH": os.getcwd(), 26 | "DB_URL": None 27 | } 28 | web = { 29 | "with_ajax": False, 30 | "request_config": {}, 31 | "headers": None 32 | } 33 | ``` 34 | 35 | - cache: config what kind of cache you use. Default is memory cache 36 | - storage: config what kind of storage you use. Default is local file storage. 37 | - web: config the request headers and if using ajax. Default without ajax. 38 | 39 | You can find more detail describe for theme in [topics](/topics/settings) -------------------------------------------------------------------------------- /docs/tutorials/step2-redis.md: -------------------------------------------------------------------------------- 1 | ## Prepare redis 2 | 3 | ```text 4 | $ redis-cli -v 5 | redis-cli 3.0.6 6 | ``` 7 | 8 | If you don't have redis, you need to install it. 9 | 10 | ## Setting 11 | 12 | Edit the `settings.py`, change it to: 13 | 14 | ```python 15 | import os 16 | 17 | from toapi.cache import RedisCache, JsonSerializer 18 | from toapi.settings import Settings 19 | 20 | class MySettings(Settings): 21 | """ 22 | Create custom configuration 23 | http://www.toapi.org/topics/settings/ 24 | """ 25 | 26 | cache = { 27 | 'cache_class': RedisCache, 28 | 'cache_config': { 29 | 'host': '127.0.0.1', 30 | 'port': 6379, 31 | 'db': 0 32 | }, 33 | 'serializer': JsonSerializer, 34 | 'ttl': 10000 35 | } 36 | storage = { 37 | "PATH": os.getcwd(), 38 | "DB_URL": None 39 | } 40 | web = { 41 | "with_ajax": False, 42 | "request_config": {}, 43 | "headers": None 44 | } 45 | ``` 46 | 47 | Try to run command `toapi run`. If it works, you do good job. 48 | If something wrong, please check the redis and the redis library for python. -------------------------------------------------------------------------------- /docs/tutorials/step3-sqlite3.md: -------------------------------------------------------------------------------- 1 | ## Prepare Sqlite3 2 | 3 | ```text 4 | $ sqlite3 --version 5 | 3.11.0 2016-02-15 17:29:24 3d862f207e3adc00f78066799ac5a8c282430a5f 6 | ``` 7 | 8 | If you don't have sqlite3, you need to install it. 9 | 10 | ## Setting 11 | 12 | Edit the `settings.py`, change it to: 13 | 14 | ```python 15 | import os 16 | 17 | from toapi.cache import RedisCache 18 | from toapi.settings import Settings 19 | 20 | 21 | class MySettings(Settings): 22 | """ 23 | Create custom configuration 24 | http://www.toapi.org/topics/settings/ 25 | """ 26 | 27 | cache = { 28 | 'cache_class': RedisCache, 29 | 'cache_config': { 30 | 'host': '127.0.0.1', 31 | 'port': 6379, 32 | 'db': 0 33 | }, 34 | 'ttl': 10000 35 | } 36 | storage = { 37 | "PATH": os.getcwd(), 38 | "DB_URL": 'sqlite:///data.sqlite' 39 | } 40 | web = { 41 | "with_ajax": False, 42 | "request_config": {}, 43 | "headers": None 44 | } 45 | 46 | ``` 47 | 48 | Try to run command `toapi run`. If it works, you do good job. 49 | In the root directory there should be a `data.sqlite` file. 50 | If something wrong, please check the sqlite3 and the sqlite3 library for python. -------------------------------------------------------------------------------- /docs/tutorials/step4-defining-items.md: -------------------------------------------------------------------------------- 1 | ## Focus on the item directory 2 | 3 | `pexels.py`: 4 | 5 | ```python 6 | from toapi import Item, XPath 7 | 8 | 9 | class Pexels(Item): 10 | __base_url__ = 'https://www.pexels.com' 11 | img = XPath('//a//img/@src') 12 | 13 | class Meta: 14 | source = XPath('//article[@class="photo-item"]') 15 | route = {'/pic/?q=:key': '/search/:key/'} 16 | ``` 17 | 18 | `pixabay.py`: 19 | 20 | ```python 21 | from toapi import Item, XPath 22 | 23 | 24 | class Pixabay(Item): 25 | __base_url__ = 'https://pixabay.com/' 26 | img = XPath('//a//img/@src') 27 | 28 | class Meta: 29 | source = XPath('//div[@class="item"]') 30 | route = {'/pic/?q=:key': '/zh/photos/?q=:key'} 31 | 32 | ``` 33 | 34 | Pretty simple. 35 | 36 | 1. Define the section you want to parse. (Meta.source) 37 | 2. Define the fileds relative to section. 38 | 3. Define the map of expose route of our api server and the routes of source website. 39 | 40 | 41 | ## Register 42 | 43 | When you defined your items, you have to register theme to app. So that the app 44 | could know how to work. 45 | 46 | In the file `app.py`: 47 | 48 | ```python 49 | from toapi import Api 50 | 51 | from items.pexels import Pexels 52 | from items.pixabay import Pixabay 53 | from settings import MySettings 54 | 55 | api = Api(settings=MySettings) 56 | api.register(Pixabay) 57 | api.register(Pexels) 58 | 59 | if __name__ == '__main__': 60 | api.serve() 61 | ``` -------------------------------------------------------------------------------- /docs/tutorials/step5-deploy.md: -------------------------------------------------------------------------------- 1 | ## Caddy 2 | 3 | > Caddy is the HTTP/2 web server with automatic HTTPS. 4 | 5 | [https://caddyserver.com/docs](https://caddyserver.com/docs) 6 | 7 | Make sure you install caddy: 8 | 9 | ```text 10 | $ caddy -version 11 | Caddy 0.10.10 (non-commercial use only) 12 | ``` 13 | 14 | ## Gunicorn 15 | 16 | > Gunicorn 'Green Unicorn' is a Python WSGI HTTP Server for UNIX. 17 | It's a pre-fork worker model. 18 | The Gunicorn server is broadly compatible with various web frameworks, 19 | simply implemented, light on server resources, and fairly speedy. 20 | 21 | Make sure you install caddy: 22 | 23 | ```text 24 | $ gunicorn -v 25 | gunicorn (version 19.7.1) 26 | ``` 27 | 28 | ## Run! 29 | 30 | Launch gunicorn: gunicorn -b "127.0.0.1:5000" wsgi Usually, 31 | you will have your gunicorn script on a supervisor, or something else 32 | 33 | ```text 34 | $ gunicorn -b "127.0.0.1:5000" wsgi:app 35 | [2017-12-26 10:55:18 +0800] [21545] [INFO] Starting gunicorn 19.7.1 36 | [2017-12-26 10:55:18 +0800] [21545] [INFO] Listening at: http://127.0.0.1:5000 (21545) 37 | [2017-12-26 10:55:18 +0800] [21545] [INFO] Using worker: sync 38 | [2017-12-26 10:55:18 +0800] [21548] [INFO] Booting worker with pid: 21548 39 | 2017/12/26 10:55:18 [Register] OK 40 | 2017/12/26 10:55:18 [Register] OK 41 | ``` 42 | 43 | Create Caddyfile 44 | 45 | ```text 46 | $ touch Caddyfile 47 | ``` 48 | 49 | Edit the Caddyfile as follows: 50 | 51 | ```text 52 | :8080 { 53 | proxy / localhost:5000 { 54 | transparent 55 | } 56 | } 57 | ``` 58 | 59 | Next step: 60 | 61 | ```text 62 | $ caddy 63 | Activating privacy features... done. 64 | http://localhost:8080 65 | ``` 66 | 67 | Well done! Everything is ok! You can get more information from `topics` or just 68 | go to write your own APIs! 69 | 70 | ## The result directory structure: 71 | 72 | ```text 73 | $ tree . 74 | . 75 | ├── app.py 76 | ├── Caddyfile 77 | ├── data.sqlite 78 | ├── items 79 | │   ├── __init__.py 80 | │   ├── pexels.py 81 | │   ├── pixabay.py 82 | │   └── __pycache__ 83 | │   ├── __init__.cpython-36.pyc 84 | │   ├── pexels.cpython-36.pyc 85 | │   └── pixabay.cpython-36.pyc 86 | ├── __pycache__ 87 | │   ├── app.cpython-36.pyc 88 | │   ├── settings.cpython-36.pyc 89 | │   └── wsgi.cpython-36.pyc 90 | ├── README.md 91 | ├── settings.py 92 | └── wsgi.py 93 | 94 | 3 directories, 15 files 95 | ``` -------------------------------------------------------------------------------- /examples/click/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | 3 | from toapi import Api 4 | 5 | app = Flask(__name__) 6 | 7 | 8 | @app.route("/") 9 | def index(): 10 | html = Api().fetch("https://movie.douban.com/") 11 | return render_template("index.html", **{"html": html}) 12 | 13 | 14 | app.run(debug=True) 15 | -------------------------------------------------------------------------------- /examples/click/static/main.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotgao2/toapi/6ae043cab28d16beb0be1bd9b1cd0fdc9c19baa6/examples/click/static/main.js -------------------------------------------------------------------------------- /examples/click/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 45 | 46 | 49 |
50 | {{ html |safe}} 51 |
52 | 53 | 54 | 74 | 75 | -------------------------------------------------------------------------------- /examples/hackernews_page.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from htmlparsing import Attr, Text 3 | 4 | from toapi import Api, Item 5 | 6 | api = Api(browser="/home/bug/桌面/geckodriver") 7 | 8 | 9 | @api.site("https://news.ycombinator.com") 10 | @api.list(".athing") 11 | @api.route("/posts?page={page}", "/news?p={page}") 12 | @api.route("/posts", "/news?p=1") 13 | class Post(Item): 14 | url = Attr(".storylink", "href") 15 | title = Text(".storylink") 16 | 17 | 18 | @api.site("https://news.ycombinator.com") 19 | @api.route("/posts?page={page}", "/news?p={page}") 20 | @api.route("/posts", "/news?p=1") 21 | class Page(Item): 22 | next_page = Attr(".morelink", "href") 23 | 24 | def clean_next_page(self, value): 25 | return api.convert_string( 26 | "/" + value, 27 | "/news?p={page}", 28 | request.host_url.strip("/") + "/posts?page={page}", 29 | ) 30 | 31 | 32 | api.run(debug=True, host="0.0.0.0", port=5000) 33 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Toapi 2 | site_url: http://www.toapi.org 3 | site_description: Every web site provides APIs. 4 | site_author: Jiuli Gao 5 | 6 | repo_url: https://github.com/gaojiuli/toapi/ 7 | repo_name: 'gaojiuli/toapi' 8 | 9 | pages: 10 | - Toapi: 11 | - Introduce: index.md 12 | - Installation: about/installation.md 13 | - Release Notes: about/release-notes.md 14 | - Contributing: about/contributing.md 15 | - License: about/license.md 16 | - Tutorials: 17 | - Introducing Aim: tutorials/introducing.md 18 | - Step 0 Creating New Project: tutorials/step0-creating-new-project.md 19 | - Step 1 Global Settings: tutorials/step1-global-settings.md 20 | - Step 2 Using Redis: tutorials/step2-redis.md 21 | - Step 3 Using Sqlite: tutorials/step3-sqlite3.md 22 | - Step 4 Defining Items: tutorials/step4-defining-items.md 23 | - Step 5 Deploy: tutorials/step5-deploy.md 24 | - Topics: 25 | - Api: topics/api.md 26 | - Item: topics/item.md 27 | - Selector: topics/selector.md 28 | - Settings: topics/settings.md 29 | - Cache: topics/cache.md 30 | - Storage: topics/storage.md 31 | - Articles: 32 | - Toapi released: articles/release.md 33 | 34 | markdown_extensions: 35 | - toc: 36 | permalink:  37 | - admonition 38 | - def_list 39 | - codehilite 40 | 41 | copyright: Copyright © 2017 Jiuli Gao. 42 | 43 | theme: 44 | name: 'material' 45 | palette: 46 | primary: 'blue' 47 | accent: 'blue' 48 | font: 49 | text: 'Ubuntu' 50 | code: 'Ubuntu Mono' 51 | logo: './logo.png' 52 | social: 53 | - type: 'github' 54 | link: 'https://github.com/gaojiuli' 55 | plugins: 56 | - search -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appdirs" 3 | version = "1.4.4" 4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [package.source] 10 | type = "legacy" 11 | url = "https://pypi.doubanio.com/simple" 12 | reference = "douban" 13 | 14 | [[package]] 15 | name = "atomicwrites" 16 | version = "1.4.0" 17 | description = "Atomic file writes." 18 | category = "dev" 19 | optional = false 20 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 21 | 22 | [package.source] 23 | type = "legacy" 24 | url = "https://pypi.doubanio.com/simple" 25 | reference = "douban" 26 | 27 | [[package]] 28 | name = "attrs" 29 | version = "21.2.0" 30 | description = "Classes Without Boilerplate" 31 | category = "dev" 32 | optional = false 33 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 34 | 35 | [package.extras] 36 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] 37 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 38 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] 39 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] 40 | 41 | [package.source] 42 | type = "legacy" 43 | url = "https://pypi.doubanio.com/simple" 44 | reference = "douban" 45 | 46 | [[package]] 47 | name = "beautifulsoup4" 48 | version = "4.9.3" 49 | description = "Screen-scraping library" 50 | category = "dev" 51 | optional = false 52 | python-versions = "*" 53 | 54 | [package.dependencies] 55 | soupsieve = {version = ">1.2", markers = "python_version >= \"3.0\""} 56 | 57 | [package.extras] 58 | html5lib = ["html5lib"] 59 | lxml = ["lxml"] 60 | 61 | [package.source] 62 | type = "legacy" 63 | url = "https://pypi.doubanio.com/simple" 64 | reference = "douban" 65 | 66 | [[package]] 67 | name = "cchardet" 68 | version = "2.1.7" 69 | description = "cChardet is high speed universal character encoding detector." 70 | category = "main" 71 | optional = false 72 | python-versions = "*" 73 | 74 | [package.source] 75 | type = "legacy" 76 | url = "https://pypi.doubanio.com/simple" 77 | reference = "douban" 78 | 79 | [[package]] 80 | name = "certifi" 81 | version = "2021.5.30" 82 | description = "Python package for providing Mozilla's CA Bundle." 83 | category = "main" 84 | optional = false 85 | python-versions = "*" 86 | 87 | [package.source] 88 | type = "legacy" 89 | url = "https://pypi.doubanio.com/simple" 90 | reference = "douban" 91 | 92 | [[package]] 93 | name = "cfgv" 94 | version = "3.3.0" 95 | description = "Validate configuration and produce human readable error messages." 96 | category = "dev" 97 | optional = false 98 | python-versions = ">=3.6.1" 99 | 100 | [package.source] 101 | type = "legacy" 102 | url = "https://pypi.doubanio.com/simple" 103 | reference = "douban" 104 | 105 | [[package]] 106 | name = "chardet" 107 | version = "4.0.0" 108 | description = "Universal encoding detector for Python 2 and 3" 109 | category = "main" 110 | optional = false 111 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 112 | 113 | [package.source] 114 | type = "legacy" 115 | url = "https://pypi.doubanio.com/simple" 116 | reference = "douban" 117 | 118 | [[package]] 119 | name = "click" 120 | version = "8.0.1" 121 | description = "Composable command line interface toolkit" 122 | category = "main" 123 | optional = false 124 | python-versions = ">=3.6" 125 | 126 | [package.dependencies] 127 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 128 | 129 | [package.source] 130 | type = "legacy" 131 | url = "https://pypi.doubanio.com/simple" 132 | reference = "douban" 133 | 134 | [[package]] 135 | name = "codecov" 136 | version = "2.1.11" 137 | description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" 138 | category = "dev" 139 | optional = false 140 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 141 | 142 | [package.dependencies] 143 | coverage = "*" 144 | requests = ">=2.7.9" 145 | 146 | [package.source] 147 | type = "legacy" 148 | url = "https://pypi.doubanio.com/simple" 149 | reference = "douban" 150 | 151 | [[package]] 152 | name = "colorama" 153 | version = "0.4.4" 154 | description = "Cross-platform colored terminal text." 155 | category = "main" 156 | optional = false 157 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 158 | 159 | [package.source] 160 | type = "legacy" 161 | url = "https://pypi.doubanio.com/simple" 162 | reference = "douban" 163 | 164 | [[package]] 165 | name = "coverage" 166 | version = "5.5" 167 | description = "Code coverage measurement for Python" 168 | category = "dev" 169 | optional = false 170 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 171 | 172 | [package.extras] 173 | toml = ["toml"] 174 | 175 | [package.source] 176 | type = "legacy" 177 | url = "https://pypi.doubanio.com/simple" 178 | reference = "douban" 179 | 180 | [[package]] 181 | name = "cssselect" 182 | version = "1.1.0" 183 | description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" 184 | category = "main" 185 | optional = false 186 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 187 | 188 | [package.source] 189 | type = "legacy" 190 | url = "https://pypi.doubanio.com/simple" 191 | reference = "douban" 192 | 193 | [[package]] 194 | name = "distlib" 195 | version = "0.3.2" 196 | description = "Distribution utilities" 197 | category = "dev" 198 | optional = false 199 | python-versions = "*" 200 | 201 | [package.source] 202 | type = "legacy" 203 | url = "https://pypi.doubanio.com/simple" 204 | reference = "douban" 205 | 206 | [[package]] 207 | name = "execnet" 208 | version = "1.9.0" 209 | description = "execnet: rapid multi-Python deployment" 210 | category = "dev" 211 | optional = false 212 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 213 | 214 | [package.extras] 215 | testing = ["pre-commit"] 216 | 217 | [package.source] 218 | type = "legacy" 219 | url = "https://pypi.doubanio.com/simple" 220 | reference = "douban" 221 | 222 | [[package]] 223 | name = "filelock" 224 | version = "3.0.12" 225 | description = "A platform independent file lock." 226 | category = "dev" 227 | optional = false 228 | python-versions = "*" 229 | 230 | [package.source] 231 | type = "legacy" 232 | url = "https://pypi.doubanio.com/simple" 233 | reference = "douban" 234 | 235 | [[package]] 236 | name = "flask" 237 | version = "2.0.1" 238 | description = "A simple framework for building complex web applications." 239 | category = "main" 240 | optional = false 241 | python-versions = ">=3.6" 242 | 243 | [package.dependencies] 244 | click = ">=7.1.2" 245 | itsdangerous = ">=2.0" 246 | Jinja2 = ">=3.0" 247 | Werkzeug = ">=2.0" 248 | 249 | [package.extras] 250 | async = ["asgiref (>=3.2)"] 251 | dotenv = ["python-dotenv"] 252 | 253 | [package.source] 254 | type = "legacy" 255 | url = "https://pypi.doubanio.com/simple" 256 | reference = "douban" 257 | 258 | [[package]] 259 | name = "ghp-import" 260 | version = "2.0.1" 261 | description = "Copy your docs directly to the gh-pages branch." 262 | category = "dev" 263 | optional = false 264 | python-versions = "*" 265 | 266 | [package.dependencies] 267 | python-dateutil = ">=2.8.1" 268 | 269 | [package.extras] 270 | dev = ["twine", "markdown", "flake8"] 271 | 272 | [package.source] 273 | type = "legacy" 274 | url = "https://pypi.doubanio.com/simple" 275 | reference = "douban" 276 | 277 | [[package]] 278 | name = "html2text" 279 | version = "2020.1.16" 280 | description = "Turn HTML into equivalent Markdown-structured text." 281 | category = "main" 282 | optional = false 283 | python-versions = ">=3.5" 284 | 285 | [package.source] 286 | type = "legacy" 287 | url = "https://pypi.doubanio.com/simple" 288 | reference = "douban" 289 | 290 | [[package]] 291 | name = "htmlfetcher" 292 | version = "0.0.6" 293 | description = "No pain HTML fetching library." 294 | category = "main" 295 | optional = false 296 | python-versions = "*" 297 | 298 | [package.dependencies] 299 | selenium = "*" 300 | 301 | [package.source] 302 | type = "legacy" 303 | url = "https://pypi.doubanio.com/simple" 304 | reference = "douban" 305 | 306 | [[package]] 307 | name = "htmlparsing" 308 | version = "0.1.5" 309 | description = "Pure HTML parsing library." 310 | category = "main" 311 | optional = false 312 | python-versions = "*" 313 | 314 | [package.dependencies] 315 | html2text = "*" 316 | lxml = "*" 317 | parse = "*" 318 | 319 | [package.source] 320 | type = "legacy" 321 | url = "https://pypi.doubanio.com/simple" 322 | reference = "douban" 323 | 324 | [[package]] 325 | name = "identify" 326 | version = "2.2.10" 327 | description = "File identification library for Python" 328 | category = "dev" 329 | optional = false 330 | python-versions = ">=3.6.1" 331 | 332 | [package.extras] 333 | license = ["editdistance-s"] 334 | 335 | [package.source] 336 | type = "legacy" 337 | url = "https://pypi.doubanio.com/simple" 338 | reference = "douban" 339 | 340 | [[package]] 341 | name = "idna" 342 | version = "2.10" 343 | description = "Internationalized Domain Names in Applications (IDNA)" 344 | category = "main" 345 | optional = false 346 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 347 | 348 | [package.source] 349 | type = "legacy" 350 | url = "https://pypi.doubanio.com/simple" 351 | reference = "douban" 352 | 353 | [[package]] 354 | name = "importlib-metadata" 355 | version = "4.6.0" 356 | description = "Read metadata from Python packages" 357 | category = "dev" 358 | optional = false 359 | python-versions = ">=3.6" 360 | 361 | [package.dependencies] 362 | zipp = ">=0.5" 363 | 364 | [package.extras] 365 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 366 | perf = ["ipython"] 367 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 368 | 369 | [package.source] 370 | type = "legacy" 371 | url = "https://pypi.doubanio.com/simple" 372 | reference = "douban" 373 | 374 | [[package]] 375 | name = "iniconfig" 376 | version = "1.1.1" 377 | description = "iniconfig: brain-dead simple config-ini parsing" 378 | category = "dev" 379 | optional = false 380 | python-versions = "*" 381 | 382 | [package.source] 383 | type = "legacy" 384 | url = "https://pypi.doubanio.com/simple" 385 | reference = "douban" 386 | 387 | [[package]] 388 | name = "itsdangerous" 389 | version = "2.0.1" 390 | description = "Safely pass data to untrusted environments and back." 391 | category = "main" 392 | optional = false 393 | python-versions = ">=3.6" 394 | 395 | [package.source] 396 | type = "legacy" 397 | url = "https://pypi.doubanio.com/simple" 398 | reference = "douban" 399 | 400 | [[package]] 401 | name = "jinja2" 402 | version = "3.0.1" 403 | description = "A very fast and expressive template engine." 404 | category = "main" 405 | optional = false 406 | python-versions = ">=3.6" 407 | 408 | [package.dependencies] 409 | MarkupSafe = ">=2.0" 410 | 411 | [package.extras] 412 | i18n = ["Babel (>=2.7)"] 413 | 414 | [package.source] 415 | type = "legacy" 416 | url = "https://pypi.doubanio.com/simple" 417 | reference = "douban" 418 | 419 | [[package]] 420 | name = "lxml" 421 | version = "4.6.3" 422 | description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." 423 | category = "main" 424 | optional = false 425 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" 426 | 427 | [package.extras] 428 | cssselect = ["cssselect (>=0.7)"] 429 | html5 = ["html5lib"] 430 | htmlsoup = ["beautifulsoup4"] 431 | source = ["Cython (>=0.29.7)"] 432 | 433 | [package.source] 434 | type = "legacy" 435 | url = "https://pypi.doubanio.com/simple" 436 | reference = "douban" 437 | 438 | [[package]] 439 | name = "markdown" 440 | version = "3.3.4" 441 | description = "Python implementation of Markdown." 442 | category = "dev" 443 | optional = false 444 | python-versions = ">=3.6" 445 | 446 | [package.extras] 447 | testing = ["coverage", "pyyaml"] 448 | 449 | [package.source] 450 | type = "legacy" 451 | url = "https://pypi.doubanio.com/simple" 452 | reference = "douban" 453 | 454 | [[package]] 455 | name = "markupsafe" 456 | version = "2.0.1" 457 | description = "Safely add untrusted strings to HTML/XML markup." 458 | category = "main" 459 | optional = false 460 | python-versions = ">=3.6" 461 | 462 | [package.source] 463 | type = "legacy" 464 | url = "https://pypi.doubanio.com/simple" 465 | reference = "douban" 466 | 467 | [[package]] 468 | name = "mergedeep" 469 | version = "1.3.4" 470 | description = "A deep merge function for 🐍." 471 | category = "dev" 472 | optional = false 473 | python-versions = ">=3.6" 474 | 475 | [package.source] 476 | type = "legacy" 477 | url = "https://pypi.doubanio.com/simple" 478 | reference = "douban" 479 | 480 | [[package]] 481 | name = "mkdocs" 482 | version = "1.2.1" 483 | description = "Project documentation with Markdown." 484 | category = "dev" 485 | optional = false 486 | python-versions = ">=3.6" 487 | 488 | [package.dependencies] 489 | click = ">=3.3" 490 | ghp-import = ">=1.0" 491 | importlib-metadata = ">=3.10" 492 | Jinja2 = ">=2.10.1" 493 | Markdown = ">=3.2.1" 494 | mergedeep = ">=1.3.4" 495 | packaging = ">=20.5" 496 | PyYAML = ">=3.10" 497 | pyyaml-env-tag = ">=0.1" 498 | watchdog = ">=2.0" 499 | 500 | [package.extras] 501 | i18n = ["babel (>=2.9.0)"] 502 | 503 | [package.source] 504 | type = "legacy" 505 | url = "https://pypi.doubanio.com/simple" 506 | reference = "douban" 507 | 508 | [[package]] 509 | name = "mkdocs-material" 510 | version = "7.1.9" 511 | description = "A Material Design theme for MkDocs" 512 | category = "dev" 513 | optional = false 514 | python-versions = "*" 515 | 516 | [package.dependencies] 517 | markdown = ">=3.2" 518 | mkdocs = ">=1.1" 519 | mkdocs-material-extensions = ">=1.0" 520 | Pygments = ">=2.4" 521 | pymdown-extensions = ">=7.0" 522 | 523 | [package.source] 524 | type = "legacy" 525 | url = "https://pypi.doubanio.com/simple" 526 | reference = "douban" 527 | 528 | [[package]] 529 | name = "mkdocs-material-extensions" 530 | version = "1.0.1" 531 | description = "Extension pack for Python Markdown." 532 | category = "dev" 533 | optional = false 534 | python-versions = ">=3.5" 535 | 536 | [package.dependencies] 537 | mkdocs-material = ">=5.0.0" 538 | 539 | [package.source] 540 | type = "legacy" 541 | url = "https://pypi.doubanio.com/simple" 542 | reference = "douban" 543 | 544 | [[package]] 545 | name = "nodeenv" 546 | version = "1.6.0" 547 | description = "Node.js virtual environment builder" 548 | category = "dev" 549 | optional = false 550 | python-versions = "*" 551 | 552 | [package.source] 553 | type = "legacy" 554 | url = "https://pypi.doubanio.com/simple" 555 | reference = "douban" 556 | 557 | [[package]] 558 | name = "packaging" 559 | version = "20.9" 560 | description = "Core utilities for Python packages" 561 | category = "dev" 562 | optional = false 563 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 564 | 565 | [package.dependencies] 566 | pyparsing = ">=2.0.2" 567 | 568 | [package.source] 569 | type = "legacy" 570 | url = "https://pypi.doubanio.com/simple" 571 | reference = "douban" 572 | 573 | [[package]] 574 | name = "parse" 575 | version = "1.19.0" 576 | description = "parse() is the opposite of format()" 577 | category = "main" 578 | optional = false 579 | python-versions = "*" 580 | 581 | [package.source] 582 | type = "legacy" 583 | url = "https://pypi.doubanio.com/simple" 584 | reference = "douban" 585 | 586 | [[package]] 587 | name = "pep8" 588 | version = "1.7.1" 589 | description = "Python style guide checker" 590 | category = "dev" 591 | optional = false 592 | python-versions = "*" 593 | 594 | [package.source] 595 | type = "legacy" 596 | url = "https://pypi.doubanio.com/simple" 597 | reference = "douban" 598 | 599 | [[package]] 600 | name = "pluggy" 601 | version = "0.13.1" 602 | description = "plugin and hook calling mechanisms for python" 603 | category = "dev" 604 | optional = false 605 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 606 | 607 | [package.extras] 608 | dev = ["pre-commit", "tox"] 609 | 610 | [package.source] 611 | type = "legacy" 612 | url = "https://pypi.doubanio.com/simple" 613 | reference = "douban" 614 | 615 | [[package]] 616 | name = "pre-commit" 617 | version = "2.13.0" 618 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 619 | category = "dev" 620 | optional = false 621 | python-versions = ">=3.6.1" 622 | 623 | [package.dependencies] 624 | cfgv = ">=2.0.0" 625 | identify = ">=1.0.0" 626 | nodeenv = ">=0.11.1" 627 | pyyaml = ">=5.1" 628 | toml = "*" 629 | virtualenv = ">=20.0.8" 630 | 631 | [package.source] 632 | type = "legacy" 633 | url = "https://pypi.doubanio.com/simple" 634 | reference = "douban" 635 | 636 | [[package]] 637 | name = "py" 638 | version = "1.10.0" 639 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 640 | category = "dev" 641 | optional = false 642 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 643 | 644 | [package.source] 645 | type = "legacy" 646 | url = "https://pypi.doubanio.com/simple" 647 | reference = "douban" 648 | 649 | [[package]] 650 | name = "pygments" 651 | version = "2.9.0" 652 | description = "Pygments is a syntax highlighting package written in Python." 653 | category = "dev" 654 | optional = false 655 | python-versions = ">=3.5" 656 | 657 | [package.source] 658 | type = "legacy" 659 | url = "https://pypi.doubanio.com/simple" 660 | reference = "douban" 661 | 662 | [[package]] 663 | name = "pymdown-extensions" 664 | version = "8.2" 665 | description = "Extension pack for Python Markdown." 666 | category = "dev" 667 | optional = false 668 | python-versions = ">=3.6" 669 | 670 | [package.dependencies] 671 | Markdown = ">=3.2" 672 | 673 | [package.source] 674 | type = "legacy" 675 | url = "https://pypi.doubanio.com/simple" 676 | reference = "douban" 677 | 678 | [[package]] 679 | name = "pyparsing" 680 | version = "2.4.7" 681 | description = "Python parsing module" 682 | category = "dev" 683 | optional = false 684 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 685 | 686 | [package.source] 687 | type = "legacy" 688 | url = "https://pypi.doubanio.com/simple" 689 | reference = "douban" 690 | 691 | [[package]] 692 | name = "pytest" 693 | version = "6.2.4" 694 | description = "pytest: simple powerful testing with Python" 695 | category = "dev" 696 | optional = false 697 | python-versions = ">=3.6" 698 | 699 | [package.dependencies] 700 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 701 | attrs = ">=19.2.0" 702 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 703 | iniconfig = "*" 704 | packaging = "*" 705 | pluggy = ">=0.12,<1.0.0a1" 706 | py = ">=1.8.2" 707 | toml = "*" 708 | 709 | [package.extras] 710 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 711 | 712 | [package.source] 713 | type = "legacy" 714 | url = "https://pypi.doubanio.com/simple" 715 | reference = "douban" 716 | 717 | [[package]] 718 | name = "pytest-cache" 719 | version = "1.0" 720 | description = "pytest plugin with mechanisms for caching across test runs" 721 | category = "dev" 722 | optional = false 723 | python-versions = "*" 724 | 725 | [package.dependencies] 726 | execnet = ">=1.1.dev1" 727 | pytest = ">=2.2" 728 | 729 | [package.source] 730 | type = "legacy" 731 | url = "https://pypi.doubanio.com/simple" 732 | reference = "douban" 733 | 734 | [[package]] 735 | name = "pytest-cov" 736 | version = "2.12.1" 737 | description = "Pytest plugin for measuring coverage." 738 | category = "dev" 739 | optional = false 740 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 741 | 742 | [package.dependencies] 743 | coverage = ">=5.2.1" 744 | pytest = ">=4.6" 745 | toml = "*" 746 | 747 | [package.extras] 748 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 749 | 750 | [package.source] 751 | type = "legacy" 752 | url = "https://pypi.doubanio.com/simple" 753 | reference = "douban" 754 | 755 | [[package]] 756 | name = "pytest-pep8" 757 | version = "1.0.6" 758 | description = "pytest plugin to check PEP8 requirements" 759 | category = "dev" 760 | optional = false 761 | python-versions = "*" 762 | 763 | [package.dependencies] 764 | pep8 = ">=1.3" 765 | pytest = ">=2.4.2" 766 | pytest-cache = "*" 767 | 768 | [package.source] 769 | type = "legacy" 770 | url = "https://pypi.doubanio.com/simple" 771 | reference = "douban" 772 | 773 | [[package]] 774 | name = "python-dateutil" 775 | version = "2.8.1" 776 | description = "Extensions to the standard Python datetime module" 777 | category = "dev" 778 | optional = false 779 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 780 | 781 | [package.dependencies] 782 | six = ">=1.5" 783 | 784 | [package.source] 785 | type = "legacy" 786 | url = "https://pypi.doubanio.com/simple" 787 | reference = "douban" 788 | 789 | [[package]] 790 | name = "pyyaml" 791 | version = "5.4.1" 792 | description = "YAML parser and emitter for Python" 793 | category = "dev" 794 | optional = false 795 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 796 | 797 | [package.source] 798 | type = "legacy" 799 | url = "https://pypi.doubanio.com/simple" 800 | reference = "douban" 801 | 802 | [[package]] 803 | name = "pyyaml-env-tag" 804 | version = "0.1" 805 | description = "A custom YAML tag for referencing environment variables in YAML files." 806 | category = "dev" 807 | optional = false 808 | python-versions = ">=3.6" 809 | 810 | [package.dependencies] 811 | pyyaml = "*" 812 | 813 | [package.source] 814 | type = "legacy" 815 | url = "https://pypi.doubanio.com/simple" 816 | reference = "douban" 817 | 818 | [[package]] 819 | name = "requests" 820 | version = "2.25.1" 821 | description = "Python HTTP for Humans." 822 | category = "main" 823 | optional = false 824 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 825 | 826 | [package.dependencies] 827 | certifi = ">=2017.4.17" 828 | chardet = ">=3.0.2,<5" 829 | idna = ">=2.5,<3" 830 | urllib3 = ">=1.21.1,<1.27" 831 | 832 | [package.extras] 833 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 834 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 835 | 836 | [package.source] 837 | type = "legacy" 838 | url = "https://pypi.doubanio.com/simple" 839 | reference = "douban" 840 | 841 | [[package]] 842 | name = "selenium" 843 | version = "3.141.0" 844 | description = "Python bindings for Selenium" 845 | category = "main" 846 | optional = false 847 | python-versions = "*" 848 | 849 | [package.dependencies] 850 | urllib3 = "*" 851 | 852 | [package.source] 853 | type = "legacy" 854 | url = "https://pypi.doubanio.com/simple" 855 | reference = "douban" 856 | 857 | [[package]] 858 | name = "six" 859 | version = "1.16.0" 860 | description = "Python 2 and 3 compatibility utilities" 861 | category = "dev" 862 | optional = false 863 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 864 | 865 | [package.source] 866 | type = "legacy" 867 | url = "https://pypi.doubanio.com/simple" 868 | reference = "douban" 869 | 870 | [[package]] 871 | name = "soupsieve" 872 | version = "2.2.1" 873 | description = "A modern CSS selector implementation for Beautiful Soup." 874 | category = "dev" 875 | optional = false 876 | python-versions = ">=3.6" 877 | 878 | [package.source] 879 | type = "legacy" 880 | url = "https://pypi.doubanio.com/simple" 881 | reference = "douban" 882 | 883 | [[package]] 884 | name = "toml" 885 | version = "0.10.2" 886 | description = "Python Library for Tom's Obvious, Minimal Language" 887 | category = "dev" 888 | optional = false 889 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 890 | 891 | [package.source] 892 | type = "legacy" 893 | url = "https://pypi.doubanio.com/simple" 894 | reference = "douban" 895 | 896 | [[package]] 897 | name = "ujson" 898 | version = "4.0.2" 899 | description = "Ultra fast JSON encoder and decoder for Python" 900 | category = "dev" 901 | optional = false 902 | python-versions = ">=3.6" 903 | 904 | [package.source] 905 | type = "legacy" 906 | url = "https://pypi.doubanio.com/simple" 907 | reference = "douban" 908 | 909 | [[package]] 910 | name = "urllib3" 911 | version = "1.26.6" 912 | description = "HTTP library with thread-safe connection pooling, file post, and more." 913 | category = "main" 914 | optional = false 915 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 916 | 917 | [package.extras] 918 | brotli = ["brotlipy (>=0.6.0)"] 919 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 920 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 921 | 922 | [package.source] 923 | type = "legacy" 924 | url = "https://pypi.doubanio.com/simple" 925 | reference = "douban" 926 | 927 | [[package]] 928 | name = "virtualenv" 929 | version = "20.4.7" 930 | description = "Virtual Python Environment builder" 931 | category = "dev" 932 | optional = false 933 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 934 | 935 | [package.dependencies] 936 | appdirs = ">=1.4.3,<2" 937 | distlib = ">=0.3.1,<1" 938 | filelock = ">=3.0.0,<4" 939 | six = ">=1.9.0,<2" 940 | 941 | [package.extras] 942 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] 943 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] 944 | 945 | [package.source] 946 | type = "legacy" 947 | url = "https://pypi.doubanio.com/simple" 948 | reference = "douban" 949 | 950 | [[package]] 951 | name = "waitress" 952 | version = "2.0.0" 953 | description = "Waitress WSGI server" 954 | category = "dev" 955 | optional = false 956 | python-versions = ">=3.6.0" 957 | 958 | [package.extras] 959 | docs = ["Sphinx (>=1.8.1)", "docutils", "pylons-sphinx-themes (>=1.0.9)"] 960 | testing = ["pytest", "pytest-cover", "coverage (>=5.0)"] 961 | 962 | [package.source] 963 | type = "legacy" 964 | url = "https://pypi.doubanio.com/simple" 965 | reference = "douban" 966 | 967 | [[package]] 968 | name = "watchdog" 969 | version = "2.1.3" 970 | description = "Filesystem events monitoring" 971 | category = "dev" 972 | optional = false 973 | python-versions = ">=3.6" 974 | 975 | [package.extras] 976 | watchmedo = ["PyYAML (>=3.10)", "argh (>=0.24.1)"] 977 | 978 | [package.source] 979 | type = "legacy" 980 | url = "https://pypi.doubanio.com/simple" 981 | reference = "douban" 982 | 983 | [[package]] 984 | name = "webob" 985 | version = "1.8.7" 986 | description = "WSGI request and response object" 987 | category = "dev" 988 | optional = false 989 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*" 990 | 991 | [package.extras] 992 | docs = ["Sphinx (>=1.7.5)", "pylons-sphinx-themes"] 993 | testing = ["pytest (>=3.1.0)", "coverage", "pytest-cov", "pytest-xdist"] 994 | 995 | [package.source] 996 | type = "legacy" 997 | url = "https://pypi.doubanio.com/simple" 998 | reference = "douban" 999 | 1000 | [[package]] 1001 | name = "webtest" 1002 | version = "2.0.35" 1003 | description = "Helper to test WSGI applications" 1004 | category = "dev" 1005 | optional = false 1006 | python-versions = "*" 1007 | 1008 | [package.dependencies] 1009 | beautifulsoup4 = "*" 1010 | six = "*" 1011 | waitress = ">=0.8.5" 1012 | WebOb = ">=1.2" 1013 | 1014 | [package.extras] 1015 | docs = ["Sphinx (>=1.8.1)", "docutils", "pylons-sphinx-themes (>=1.0.8)"] 1016 | tests = ["nose (<1.3.0)", "coverage", "mock", "pastedeploy", "wsgiproxy2", "pyquery"] 1017 | 1018 | [package.source] 1019 | type = "legacy" 1020 | url = "https://pypi.doubanio.com/simple" 1021 | reference = "douban" 1022 | 1023 | [[package]] 1024 | name = "werkzeug" 1025 | version = "2.0.1" 1026 | description = "The comprehensive WSGI web application library." 1027 | category = "main" 1028 | optional = false 1029 | python-versions = ">=3.6" 1030 | 1031 | [package.extras] 1032 | watchdog = ["watchdog"] 1033 | 1034 | [package.source] 1035 | type = "legacy" 1036 | url = "https://pypi.doubanio.com/simple" 1037 | reference = "douban" 1038 | 1039 | [[package]] 1040 | name = "zipp" 1041 | version = "3.4.1" 1042 | description = "Backport of pathlib-compatible object wrapper for zip files" 1043 | category = "dev" 1044 | optional = false 1045 | python-versions = ">=3.6" 1046 | 1047 | [package.extras] 1048 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 1049 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 1050 | 1051 | [package.source] 1052 | type = "legacy" 1053 | url = "https://pypi.doubanio.com/simple" 1054 | reference = "douban" 1055 | 1056 | [metadata] 1057 | lock-version = "1.1" 1058 | python-versions = "^3.8" 1059 | content-hash = "89f8d652f628c35ee821d1241dc91923b928719d3d3bd38d61caa9f476016c4b" 1060 | 1061 | [metadata.files] 1062 | appdirs = [ 1063 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 1064 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 1065 | ] 1066 | atomicwrites = [ 1067 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 1068 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 1069 | ] 1070 | attrs = [ 1071 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 1072 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 1073 | ] 1074 | beautifulsoup4 = [ 1075 | {file = "beautifulsoup4-4.9.3-py2-none-any.whl", hash = "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35"}, 1076 | {file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"}, 1077 | {file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"}, 1078 | ] 1079 | cchardet = [ 1080 | {file = "cchardet-2.1.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6f70139aaf47ffb94d89db603af849b82efdf756f187cdd3e566e30976c519f"}, 1081 | {file = "cchardet-2.1.7-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5a25f9577e9bebe1a085eec2d6fdd72b7a9dd680811bba652ea6090fb2ff472f"}, 1082 | {file = "cchardet-2.1.7-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6b6397d8a32b976a333bdae060febd39ad5479817fabf489e5596a588ad05133"}, 1083 | {file = "cchardet-2.1.7-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:228d2533987c450f39acf7548f474dd6814c446e9d6bd228e8f1d9a2d210f10b"}, 1084 | {file = "cchardet-2.1.7-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:54341e7e1ba9dc0add4c9d23b48d3a94e2733065c13920e85895f944596f6150"}, 1085 | {file = "cchardet-2.1.7-cp36-cp36m-win32.whl", hash = "sha256:eee4f5403dc3a37a1ca9ab87db32b48dc7e190ef84601068f45397144427cc5e"}, 1086 | {file = "cchardet-2.1.7-cp36-cp36m-win_amd64.whl", hash = "sha256:f86e0566cb61dc4397297696a4a1b30f6391b50bc52b4f073507a48466b6255a"}, 1087 | {file = "cchardet-2.1.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:302aa443ae2526755d412c9631136bdcd1374acd08e34f527447f06f3c2ddb98"}, 1088 | {file = "cchardet-2.1.7-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:70eeae8aaf61192e9b247cf28969faef00578becd2602526ecd8ae7600d25e0e"}, 1089 | {file = "cchardet-2.1.7-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a39526c1c526843965cec589a6f6b7c2ab07e3e56dc09a7f77a2be6a6afa4636"}, 1090 | {file = "cchardet-2.1.7-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:b154effa12886e9c18555dfc41a110f601f08d69a71809c8d908be4b1ab7314f"}, 1091 | {file = "cchardet-2.1.7-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ec3eb5a9c475208cf52423524dcaf713c394393e18902e861f983c38eeb77f18"}, 1092 | {file = "cchardet-2.1.7-cp37-cp37m-win32.whl", hash = "sha256:50ad671e8d6c886496db62c3bd68b8d55060688c655873aa4ce25ca6105409a1"}, 1093 | {file = "cchardet-2.1.7-cp37-cp37m-win_amd64.whl", hash = "sha256:54d0b26fd0cd4099f08fb9c167600f3e83619abefeaa68ad823cc8ac1f7bcc0c"}, 1094 | {file = "cchardet-2.1.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b59ddc615883835e03c26f81d5fc3671fab2d32035c87f50862de0da7d7db535"}, 1095 | {file = "cchardet-2.1.7-cp38-cp38-manylinux1_i686.whl", hash = "sha256:27a9ba87c9f99e0618e1d3081189b1217a7d110e5c5597b0b7b7c3fedd1c340a"}, 1096 | {file = "cchardet-2.1.7-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:90086e5645f8a1801350f4cc6cb5d5bf12d3fa943811bb08667744ec1ecc9ccd"}, 1097 | {file = "cchardet-2.1.7-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:45456c59ec349b29628a3c6bfb86d818ec3a6fbb7eb72de4ff3bd4713681c0e3"}, 1098 | {file = "cchardet-2.1.7-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f16517f3697569822c6d09671217fdeab61dfebc7acb5068634d6b0728b86c0b"}, 1099 | {file = "cchardet-2.1.7-cp38-cp38-win32.whl", hash = "sha256:0b859069bbb9d27c78a2c9eb997e6f4b738db2d7039a03f8792b4058d61d1109"}, 1100 | {file = "cchardet-2.1.7-cp38-cp38-win_amd64.whl", hash = "sha256:273699c4e5cd75377776501b72a7b291a988c6eec259c29505094553ee505597"}, 1101 | {file = "cchardet-2.1.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:48ba829badef61441e08805cfa474ccd2774be2ff44b34898f5854168c596d4d"}, 1102 | {file = "cchardet-2.1.7-cp39-cp39-manylinux1_i686.whl", hash = "sha256:bd7f262f41fd9caf5a5f09207a55861a67af6ad5c66612043ed0f81c58cdf376"}, 1103 | {file = "cchardet-2.1.7-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fdac1e4366d0579fff056d1280b8dc6348be964fda8ebb627c0269e097ab37fa"}, 1104 | {file = "cchardet-2.1.7-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:80e6faae75ecb9be04a7b258dc4750d459529debb6b8dee024745b7b5a949a34"}, 1105 | {file = "cchardet-2.1.7-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c96aee9ebd1147400e608a3eff97c44f49811f8904e5a43069d55603ac4d8c97"}, 1106 | {file = "cchardet-2.1.7-cp39-cp39-win32.whl", hash = "sha256:2309ff8fc652b0fc3c0cff5dbb172530c7abb92fe9ba2417c9c0bcf688463c1c"}, 1107 | {file = "cchardet-2.1.7-cp39-cp39-win_amd64.whl", hash = "sha256:24974b3e40fee9e7557bb352be625c39ec6f50bc2053f44a3d1191db70b51675"}, 1108 | {file = "cchardet-2.1.7.tar.gz", hash = "sha256:c428b6336545053c2589f6caf24ea32276c6664cb86db817e03a94c60afa0eaf"}, 1109 | ] 1110 | certifi = [ 1111 | {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, 1112 | {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, 1113 | ] 1114 | cfgv = [ 1115 | {file = "cfgv-3.3.0-py2.py3-none-any.whl", hash = "sha256:b449c9c6118fe8cca7fa5e00b9ec60ba08145d281d52164230a69211c5d597a1"}, 1116 | {file = "cfgv-3.3.0.tar.gz", hash = "sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1"}, 1117 | ] 1118 | chardet = [ 1119 | {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, 1120 | {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, 1121 | ] 1122 | click = [ 1123 | {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, 1124 | {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, 1125 | ] 1126 | codecov = [ 1127 | {file = "codecov-2.1.11-py2.py3-none-any.whl", hash = "sha256:ba8553a82942ce37d4da92b70ffd6d54cf635fc1793ab0a7dc3fecd6ebfb3df8"}, 1128 | {file = "codecov-2.1.11.tar.gz", hash = "sha256:6cde272454009d27355f9434f4e49f238c0273b216beda8472a65dc4957f473b"}, 1129 | ] 1130 | colorama = [ 1131 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 1132 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 1133 | ] 1134 | coverage = [ 1135 | {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, 1136 | {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, 1137 | {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, 1138 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, 1139 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, 1140 | {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, 1141 | {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, 1142 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, 1143 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, 1144 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, 1145 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, 1146 | {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, 1147 | {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, 1148 | {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, 1149 | {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, 1150 | {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, 1151 | {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, 1152 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, 1153 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, 1154 | {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, 1155 | {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, 1156 | {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, 1157 | {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, 1158 | {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, 1159 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, 1160 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, 1161 | {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, 1162 | {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, 1163 | {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, 1164 | {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, 1165 | {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, 1166 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, 1167 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, 1168 | {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, 1169 | {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, 1170 | {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, 1171 | {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, 1172 | {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, 1173 | {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, 1174 | {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, 1175 | {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, 1176 | {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, 1177 | {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, 1178 | {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, 1179 | {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, 1180 | {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, 1181 | {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, 1182 | {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, 1183 | {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, 1184 | {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, 1185 | {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, 1186 | {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, 1187 | ] 1188 | cssselect = [ 1189 | {file = "cssselect-1.1.0-py2.py3-none-any.whl", hash = "sha256:f612ee47b749c877ebae5bb77035d8f4202c6ad0f0fc1271b3c18ad6c4468ecf"}, 1190 | {file = "cssselect-1.1.0.tar.gz", hash = "sha256:f95f8dedd925fd8f54edb3d2dfb44c190d9d18512377d3c1e2388d16126879bc"}, 1191 | ] 1192 | distlib = [ 1193 | {file = "distlib-0.3.2-py2.py3-none-any.whl", hash = "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"}, 1194 | {file = "distlib-0.3.2.zip", hash = "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736"}, 1195 | ] 1196 | execnet = [ 1197 | {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, 1198 | {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, 1199 | ] 1200 | filelock = [ 1201 | {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, 1202 | {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, 1203 | ] 1204 | flask = [ 1205 | {file = "Flask-2.0.1-py3-none-any.whl", hash = "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9"}, 1206 | {file = "Flask-2.0.1.tar.gz", hash = "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55"}, 1207 | ] 1208 | ghp-import = [ 1209 | {file = "ghp-import-2.0.1.tar.gz", hash = "sha256:753de2eace6e0f7d4edfb3cce5e3c3b98cd52aadb80163303d1d036bda7b4483"}, 1210 | ] 1211 | html2text = [ 1212 | {file = "html2text-2020.1.16-py3-none-any.whl", hash = "sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b"}, 1213 | {file = "html2text-2020.1.16.tar.gz", hash = "sha256:e296318e16b059ddb97f7a8a1d6a5c1d7af4544049a01e261731d2d5cc277bbb"}, 1214 | ] 1215 | htmlfetcher = [ 1216 | {file = "htmlfetcher-0.0.6.tar.gz", hash = "sha256:b6233e0a262b019ca688661024cb175028fe7e8dfe6724a8ca9495c265535859"}, 1217 | ] 1218 | htmlparsing = [ 1219 | {file = "htmlparsing-0.1.5.tar.gz", hash = "sha256:226a22527f237830f8158cb72b37ace4cb0a29e072959e118500446382f27e94"}, 1220 | ] 1221 | identify = [ 1222 | {file = "identify-2.2.10-py2.py3-none-any.whl", hash = "sha256:18d0c531ee3dbc112fa6181f34faa179de3f57ea57ae2899754f16a7e0ff6421"}, 1223 | {file = "identify-2.2.10.tar.gz", hash = "sha256:5b41f71471bc738e7b586308c3fca172f78940195cb3bf6734c1e66fdac49306"}, 1224 | ] 1225 | idna = [ 1226 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 1227 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 1228 | ] 1229 | importlib-metadata = [ 1230 | {file = "importlib_metadata-4.6.0-py3-none-any.whl", hash = "sha256:c6513572926a96458f8c8f725bf0e00108fba0c9583ade9bd15b869c9d726e33"}, 1231 | {file = "importlib_metadata-4.6.0.tar.gz", hash = "sha256:4a5611fea3768d3d967c447ab4e93f567d95db92225b43b7b238dbfb855d70bb"}, 1232 | ] 1233 | iniconfig = [ 1234 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 1235 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 1236 | ] 1237 | itsdangerous = [ 1238 | {file = "itsdangerous-2.0.1-py3-none-any.whl", hash = "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c"}, 1239 | {file = "itsdangerous-2.0.1.tar.gz", hash = "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"}, 1240 | ] 1241 | jinja2 = [ 1242 | {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, 1243 | {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, 1244 | ] 1245 | lxml = [ 1246 | {file = "lxml-4.6.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2"}, 1247 | {file = "lxml-4.6.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f"}, 1248 | {file = "lxml-4.6.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d"}, 1249 | {file = "lxml-4.6.3-cp27-cp27m-win32.whl", hash = "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106"}, 1250 | {file = "lxml-4.6.3-cp27-cp27m-win_amd64.whl", hash = "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee"}, 1251 | {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f"}, 1252 | {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"}, 1253 | {file = "lxml-4.6.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51"}, 1254 | {file = "lxml-4.6.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586"}, 1255 | {file = "lxml-4.6.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354"}, 1256 | {file = "lxml-4.6.3-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:5c8c163396cc0df3fd151b927e74f6e4acd67160d6c33304e805b84293351d16"}, 1257 | {file = "lxml-4.6.3-cp35-cp35m-win32.whl", hash = "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2"}, 1258 | {file = "lxml-4.6.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4"}, 1259 | {file = "lxml-4.6.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4"}, 1260 | {file = "lxml-4.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3"}, 1261 | {file = "lxml-4.6.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d"}, 1262 | {file = "lxml-4.6.3-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d916d31fd85b2f78c76400d625076d9124de3e4bda8b016d25a050cc7d603f24"}, 1263 | {file = "lxml-4.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec"}, 1264 | {file = "lxml-4.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:c47ff7e0a36d4efac9fd692cfa33fbd0636674c102e9e8d9b26e1b93a94e7617"}, 1265 | {file = "lxml-4.6.3-cp36-cp36m-win32.whl", hash = "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04"}, 1266 | {file = "lxml-4.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a"}, 1267 | {file = "lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654"}, 1268 | {file = "lxml-4.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0"}, 1269 | {file = "lxml-4.6.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3"}, 1270 | {file = "lxml-4.6.3-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:36108c73739985979bf302006527cf8a20515ce444ba916281d1c43938b8bb96"}, 1271 | {file = "lxml-4.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2"}, 1272 | {file = "lxml-4.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:cdaf11d2bd275bf391b5308f86731e5194a21af45fbaaaf1d9e8147b9160ea92"}, 1273 | {file = "lxml-4.6.3-cp37-cp37m-win32.whl", hash = "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade"}, 1274 | {file = "lxml-4.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b"}, 1275 | {file = "lxml-4.6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa"}, 1276 | {file = "lxml-4.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a"}, 1277 | {file = "lxml-4.6.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927"}, 1278 | {file = "lxml-4.6.3-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:e1cbd3f19a61e27e011e02f9600837b921ac661f0c40560eefb366e4e4fb275e"}, 1279 | {file = "lxml-4.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791"}, 1280 | {file = "lxml-4.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:1b38116b6e628118dea5b2186ee6820ab138dbb1e24a13e478490c7db2f326ae"}, 1281 | {file = "lxml-4.6.3-cp38-cp38-win32.whl", hash = "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28"}, 1282 | {file = "lxml-4.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7"}, 1283 | {file = "lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0"}, 1284 | {file = "lxml-4.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1"}, 1285 | {file = "lxml-4.6.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23"}, 1286 | {file = "lxml-4.6.3-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:3082c518be8e97324390614dacd041bb1358c882d77108ca1957ba47738d9d59"}, 1287 | {file = "lxml-4.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969"}, 1288 | {file = "lxml-4.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:6f12e1427285008fd32a6025e38e977d44d6382cf28e7201ed10d6c1698d2a9a"}, 1289 | {file = "lxml-4.6.3-cp39-cp39-win32.whl", hash = "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f"}, 1290 | {file = "lxml-4.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83"}, 1291 | {file = "lxml-4.6.3.tar.gz", hash = "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468"}, 1292 | ] 1293 | markdown = [ 1294 | {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, 1295 | {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, 1296 | ] 1297 | markupsafe = [ 1298 | {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, 1299 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, 1300 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, 1301 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, 1302 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, 1303 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, 1304 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, 1305 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, 1306 | {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, 1307 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, 1308 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, 1309 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, 1310 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, 1311 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, 1312 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, 1313 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, 1314 | {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, 1315 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, 1316 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, 1317 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, 1318 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, 1319 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, 1320 | {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, 1321 | {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, 1322 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, 1323 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, 1324 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, 1325 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, 1326 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, 1327 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, 1328 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, 1329 | {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, 1330 | {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, 1331 | {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, 1332 | ] 1333 | mergedeep = [ 1334 | {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, 1335 | {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, 1336 | ] 1337 | mkdocs = [ 1338 | {file = "mkdocs-1.2.1-py3-none-any.whl", hash = "sha256:11141126e5896dd9d279b3e4814eb488e409a0990fb638856255020406a8e2e7"}, 1339 | {file = "mkdocs-1.2.1.tar.gz", hash = "sha256:6e0ea175366e3a50d334597b0bc042b8cebd512398cdd3f6f34842d0ef524905"}, 1340 | ] 1341 | mkdocs-material = [ 1342 | {file = "mkdocs-material-7.1.9.tar.gz", hash = "sha256:5a2fd487f769f382a7c979e869e4eab1372af58d7dec44c4365dd97ef5268cb5"}, 1343 | {file = "mkdocs_material-7.1.9-py2.py3-none-any.whl", hash = "sha256:92c8a2bd3bd44d5948eefc46ba138e2d3285cac658900112b6bf5722c7d067a5"}, 1344 | ] 1345 | mkdocs-material-extensions = [ 1346 | {file = "mkdocs-material-extensions-1.0.1.tar.gz", hash = "sha256:6947fb7f5e4291e3c61405bad3539d81e0b3cd62ae0d66ced018128af509c68f"}, 1347 | {file = "mkdocs_material_extensions-1.0.1-py3-none-any.whl", hash = "sha256:d90c807a88348aa6d1805657ec5c0b2d8d609c110e62b9dce4daf7fa981fa338"}, 1348 | ] 1349 | nodeenv = [ 1350 | {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, 1351 | {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, 1352 | ] 1353 | packaging = [ 1354 | {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, 1355 | {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, 1356 | ] 1357 | parse = [ 1358 | {file = "parse-1.19.0.tar.gz", hash = "sha256:9ff82852bcb65d139813e2a5197627a94966245c897796760a3a2a8eb66f020b"}, 1359 | ] 1360 | pep8 = [ 1361 | {file = "pep8-1.7.1-py2.py3-none-any.whl", hash = "sha256:b22cfae5db09833bb9bd7c8463b53e1a9c9b39f12e304a8d0bba729c501827ee"}, 1362 | {file = "pep8-1.7.1.tar.gz", hash = "sha256:fe249b52e20498e59e0b5c5256aa52ee99fc295b26ec9eaa85776ffdb9fe6374"}, 1363 | ] 1364 | pluggy = [ 1365 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 1366 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 1367 | ] 1368 | pre-commit = [ 1369 | {file = "pre_commit-2.13.0-py2.py3-none-any.whl", hash = "sha256:b679d0fddd5b9d6d98783ae5f10fd0c4c59954f375b70a58cbe1ce9bcf9809a4"}, 1370 | {file = "pre_commit-2.13.0.tar.gz", hash = "sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378"}, 1371 | ] 1372 | py = [ 1373 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 1374 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 1375 | ] 1376 | pygments = [ 1377 | {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, 1378 | {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, 1379 | ] 1380 | pymdown-extensions = [ 1381 | {file = "pymdown-extensions-8.2.tar.gz", hash = "sha256:b6daa94aad9e1310f9c64c8b1f01e4ce82937ab7eb53bfc92876a97aca02a6f4"}, 1382 | {file = "pymdown_extensions-8.2-py3-none-any.whl", hash = "sha256:141452d8ed61165518f2c923454bf054866b85cf466feedb0eb68f04acdc2560"}, 1383 | ] 1384 | pyparsing = [ 1385 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 1386 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 1387 | ] 1388 | pytest = [ 1389 | {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, 1390 | {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, 1391 | ] 1392 | pytest-cache = [ 1393 | {file = "pytest-cache-1.0.tar.gz", hash = "sha256:be7468edd4d3d83f1e844959fd6e3fd28e77a481440a7118d430130ea31b07a9"}, 1394 | ] 1395 | pytest-cov = [ 1396 | {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, 1397 | {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, 1398 | ] 1399 | pytest-pep8 = [ 1400 | {file = "pytest-pep8-1.0.6.tar.gz", hash = "sha256:032ef7e5fa3ac30f4458c73e05bb67b0f036a8a5cb418a534b3170f89f120318"}, 1401 | ] 1402 | python-dateutil = [ 1403 | {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, 1404 | {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, 1405 | ] 1406 | pyyaml = [ 1407 | {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, 1408 | {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, 1409 | {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, 1410 | {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, 1411 | {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, 1412 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, 1413 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, 1414 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, 1415 | {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, 1416 | {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, 1417 | {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, 1418 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, 1419 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, 1420 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, 1421 | {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, 1422 | {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, 1423 | {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, 1424 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, 1425 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, 1426 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, 1427 | {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, 1428 | {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, 1429 | {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, 1430 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, 1431 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, 1432 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, 1433 | {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, 1434 | {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, 1435 | {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, 1436 | ] 1437 | pyyaml-env-tag = [ 1438 | {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, 1439 | {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, 1440 | ] 1441 | requests = [ 1442 | {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, 1443 | {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, 1444 | ] 1445 | selenium = [ 1446 | {file = "selenium-3.141.0-py2.py3-none-any.whl", hash = "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c"}, 1447 | {file = "selenium-3.141.0.tar.gz", hash = "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d"}, 1448 | ] 1449 | six = [ 1450 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1451 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1452 | ] 1453 | soupsieve = [ 1454 | {file = "soupsieve-2.2.1-py3-none-any.whl", hash = "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"}, 1455 | {file = "soupsieve-2.2.1.tar.gz", hash = "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc"}, 1456 | ] 1457 | toml = [ 1458 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1459 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1460 | ] 1461 | ujson = [ 1462 | {file = "ujson-4.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:e390df0dcc7897ffb98e17eae1f4c442c39c91814c298ad84d935a3c5c7a32fa"}, 1463 | {file = "ujson-4.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:84b1dca0d53b0a8d58835f72ea2894e4d6cf7a5dd8f520ab4cbd698c81e49737"}, 1464 | {file = "ujson-4.0.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:91396a585ba51f84dc71c8da60cdc86de6b60ba0272c389b6482020a1fac9394"}, 1465 | {file = "ujson-4.0.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:eb6b25a7670c7537a5998e695fa62ff13c7f9c33faf82927adf4daa460d5f62e"}, 1466 | {file = "ujson-4.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f8aded54c2bc554ce20b397f72101737dd61ee7b81c771684a7dd7805e6cca0c"}, 1467 | {file = "ujson-4.0.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:30962467c36ff6de6161d784cd2a6aac1097f0128b522d6e9291678e34fb2b47"}, 1468 | {file = "ujson-4.0.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:fc51e545d65689c398161f07fd405104956ec27f22453de85898fa088b2cd4bb"}, 1469 | {file = "ujson-4.0.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e6e90330670c78e727d6637bb5a215d3e093d8e3570d439fd4922942f88da361"}, 1470 | {file = "ujson-4.0.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:5e1636b94c7f1f59a8ead4c8a7bab1b12cc52d4c21ababa295ffec56b445fd2a"}, 1471 | {file = "ujson-4.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e2cadeb0ddc98e3963bea266cc5b884e5d77d73adf807f0bda9eca64d1c509d5"}, 1472 | {file = "ujson-4.0.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:a214ba5a21dad71a43c0f5aef917cd56a2d70bc974d845be211c66b6742a471c"}, 1473 | {file = "ujson-4.0.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0190d26c0e990c17ad072ec8593647218fe1c675d11089cd3d1440175b568967"}, 1474 | {file = "ujson-4.0.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f273a875c0b42c2a019c337631bc1907f6fdfbc84210cc0d1fff0e2019bbfaec"}, 1475 | {file = "ujson-4.0.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d3a87888c40b5bfcf69b4030427cd666893e826e82cc8608d1ba8b4b5e04ea99"}, 1476 | {file = "ujson-4.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:7333e8bc45ea28c74ae26157eacaed5e5629dbada32e0103c23eb368f93af108"}, 1477 | {file = "ujson-4.0.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b3a6dcc660220539aa718bcc9dbd6dedf2a01d19c875d1033f028f212e36d6bb"}, 1478 | {file = "ujson-4.0.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:0ea07fe57f9157118ca689e7f6db72759395b99121c0ff038d2e38649c626fb1"}, 1479 | {file = "ujson-4.0.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4d6d061563470cac889c0a9fd367013a5dbd8efc36ad01ab3e67a57e56cad720"}, 1480 | {file = "ujson-4.0.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b5c70704962cf93ec6ea3271a47d952b75ae1980d6c56b8496cec2a722075939"}, 1481 | {file = "ujson-4.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:aad6d92f4d71e37ea70e966500f1951ecd065edca3a70d3861b37b176dd6702c"}, 1482 | {file = "ujson-4.0.2.tar.gz", hash = "sha256:c615a9e9e378a7383b756b7e7a73c38b22aeb8967a8bfbffd4741f7ffd043c4d"}, 1483 | ] 1484 | urllib3 = [ 1485 | {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, 1486 | {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, 1487 | ] 1488 | virtualenv = [ 1489 | {file = "virtualenv-20.4.7-py2.py3-none-any.whl", hash = "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"}, 1490 | {file = "virtualenv-20.4.7.tar.gz", hash = "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467"}, 1491 | ] 1492 | waitress = [ 1493 | {file = "waitress-2.0.0-py3-none-any.whl", hash = "sha256:29af5a53e9fb4e158f525367678b50053808ca6c21ba585754c77d790008c746"}, 1494 | {file = "waitress-2.0.0.tar.gz", hash = "sha256:69e1f242c7f80273490d3403c3976f3ac3b26e289856936d1f620ed48f321897"}, 1495 | ] 1496 | watchdog = [ 1497 | {file = "watchdog-2.1.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9628f3f85375a17614a2ab5eac7665f7f7be8b6b0a2a228e6f6a2e91dd4bfe26"}, 1498 | {file = "watchdog-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:acc4e2d5be6f140f02ee8590e51c002829e2c33ee199036fcd61311d558d89f4"}, 1499 | {file = "watchdog-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:85b851237cf3533fabbc034ffcd84d0fa52014b3121454e5f8b86974b531560c"}, 1500 | {file = "watchdog-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a12539ecf2478a94e4ba4d13476bb2c7a2e0a2080af2bb37df84d88b1b01358a"}, 1501 | {file = "watchdog-2.1.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6fe9c8533e955c6589cfea6f3f0a1a95fb16867a211125236c82e1815932b5d7"}, 1502 | {file = "watchdog-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d9456f0433845e7153b102fffeb767bde2406b76042f2216838af3b21707894e"}, 1503 | {file = "watchdog-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fd8c595d5a93abd441ee7c5bb3ff0d7170e79031520d113d6f401d0cf49d7c8f"}, 1504 | {file = "watchdog-2.1.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0bcfe904c7d404eb6905f7106c54873503b442e8e918cc226e1828f498bdc0ca"}, 1505 | {file = "watchdog-2.1.3-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bf84bd94cbaad8f6b9cbaeef43080920f4cb0e61ad90af7106b3de402f5fe127"}, 1506 | {file = "watchdog-2.1.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b8ddb2c9f92e0c686ea77341dcb58216fa5ff7d5f992c7278ee8a392a06e86bb"}, 1507 | {file = "watchdog-2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8805a5f468862daf1e4f4447b0ccf3acaff626eaa57fbb46d7960d1cf09f2e6d"}, 1508 | {file = "watchdog-2.1.3-py3-none-manylinux2014_armv7l.whl", hash = "sha256:3e305ea2757f81d8ebd8559d1a944ed83e3ab1bdf68bcf16ec851b97c08dc035"}, 1509 | {file = "watchdog-2.1.3-py3-none-manylinux2014_i686.whl", hash = "sha256:431a3ea70b20962e6dee65f0eeecd768cd3085ea613ccb9b53c8969de9f6ebd2"}, 1510 | {file = "watchdog-2.1.3-py3-none-manylinux2014_ppc64.whl", hash = "sha256:e4929ac2aaa2e4f1a30a36751160be391911da463a8799460340901517298b13"}, 1511 | {file = "watchdog-2.1.3-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:201cadf0b8c11922f54ec97482f95b2aafca429c4c3a4bb869a14f3c20c32686"}, 1512 | {file = "watchdog-2.1.3-py3-none-manylinux2014_s390x.whl", hash = "sha256:3a7d242a7963174684206093846537220ee37ba9986b824a326a8bb4ef329a33"}, 1513 | {file = "watchdog-2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:54e057727dd18bd01a3060dbf5104eb5a495ca26316487e0f32a394fd5fe725a"}, 1514 | {file = "watchdog-2.1.3-py3-none-win32.whl", hash = "sha256:b5fc5c127bad6983eecf1ad117ab3418949f18af9c8758bd10158be3647298a9"}, 1515 | {file = "watchdog-2.1.3-py3-none-win_amd64.whl", hash = "sha256:44acad6f642996a2b50bb9ce4fb3730dde08f23e79e20cd3d8e2a2076b730381"}, 1516 | {file = "watchdog-2.1.3-py3-none-win_ia64.whl", hash = "sha256:0bcdf7b99b56a3ae069866c33d247c9994ffde91b620eaf0306b27e099bd1ae0"}, 1517 | {file = "watchdog-2.1.3.tar.gz", hash = "sha256:e5236a8e8602ab6db4b873664c2d356c365ab3cac96fbdec4970ad616415dd45"}, 1518 | ] 1519 | webob = [ 1520 | {file = "WebOb-1.8.7-py2.py3-none-any.whl", hash = "sha256:73aae30359291c14fa3b956f8b5ca31960e420c28c1bec002547fb04928cf89b"}, 1521 | {file = "WebOb-1.8.7.tar.gz", hash = "sha256:b64ef5141be559cfade448f044fa45c2260351edcb6a8ef6b7e00c7dcef0c323"}, 1522 | ] 1523 | webtest = [ 1524 | {file = "WebTest-2.0.35-py2.py3-none-any.whl", hash = "sha256:44ddfe99b5eca4cf07675e7222c81dd624d22f9a26035d2b93dc8862dc1153c6"}, 1525 | {file = "WebTest-2.0.35.tar.gz", hash = "sha256:aac168b5b2b4f200af4e35867cf316712210e3d5db81c1cbdff38722647bb087"}, 1526 | ] 1527 | werkzeug = [ 1528 | {file = "Werkzeug-2.0.1-py3-none-any.whl", hash = "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8"}, 1529 | {file = "Werkzeug-2.0.1.tar.gz", hash = "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42"}, 1530 | ] 1531 | zipp = [ 1532 | {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, 1533 | {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, 1534 | ] 1535 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "toapi" 3 | version = "2.1.3" 4 | description = "Every web site provides APIs." 5 | authors = ["Elliot Gao "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.8" 10 | colorama = "^0.4.4" 11 | cchardet = "^2.1.7" 12 | htmlparsing = "^0.1.5" 13 | requests = "^2.25.1" 14 | htmlfetcher = "^0.0.6" 15 | flask = "^2.0.1" 16 | click = "^8.0.1" 17 | cssselect = "^1.1.0" 18 | 19 | [tool.poetry.dev-dependencies] 20 | pytest = "^6.2.4" 21 | mkdocs = "^1.2.1" 22 | pytest-pep8 = "^1.0.6" 23 | pytest-cov = "^2.12.1" 24 | webtest = "^2.0.35" 25 | codecov = "^2.1.11" 26 | mkdocs-material = "^7.1.9" 27 | ujson = "^4.0.2" 28 | pre-commit = "^2.13.0" 29 | 30 | [build-system] 31 | requires = ["poetry-core>=1.0.0"] 32 | build-backend = "poetry.core.masonry.api" 33 | 34 | [tool.poetry.scripts] 35 | toapi="toapi.cli:cli" 36 | 37 | [tool.poetry.urls] 38 | "homepage" = "https://github.com/gaojiuli/toapi" 39 | "repository" = "https://github.com/gaojiuli/toapi" 40 | "documentation" = "https://gaojiuli.github.io/toapi/" 41 | 42 | 43 | 44 | [tool.black] 45 | line-length = 79 46 | include = '\.pyi?$' 47 | exclude = ''' 48 | /( 49 | \.git 50 | | \.hg 51 | | \.mypy_cache 52 | | \.tox 53 | | \.venv 54 | | _build 55 | | buck-out 56 | | build 57 | | dist 58 | )/ 59 | ''' 60 | 61 | [tool.isort] 62 | line_length = 79 63 | use_parentheses = true 64 | include_trailing_comma = true 65 | multi_line_output = 3 66 | force_grid_wrap = 0 67 | no_lines_before = "LOCALFOLDER" 68 | -------------------------------------------------------------------------------- /tests/test_toapi.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from flask import request 3 | from htmlparsing import Attr, Text 4 | from webtest import TestApp as App 5 | 6 | from toapi import Api, Item 7 | from toapi.cli import cli 8 | 9 | 10 | def test_api(): 11 | api = Api() 12 | 13 | @api.site("https://news.ycombinator.com") 14 | @api.list(".athing") 15 | @api.route("/posts?page={page}", "/news?p={page}") 16 | @api.route("/posts", "/news?p=1") 17 | class Post(Item): 18 | url = Attr(".storylink", "href") 19 | title = Text(".storylink") 20 | 21 | @api.site("https://news.ycombinator.com") 22 | @api.route("/posts?page={page}", "/news?p={page}") 23 | @api.route("/posts", "/news?p=1") 24 | class Page(Item): 25 | next_page = Attr(".morelink", "href") 26 | 27 | def clean_next_page(self, value): 28 | return api.convert_string( 29 | "/" + value, 30 | "/news?p={page}", 31 | request.host_url.strip("/") + "/posts?page={page}", 32 | ) 33 | 34 | app = App(api.app) 35 | with pytest.raises(SystemExit): 36 | api.run(port=-1) 37 | app.get("/posts?page=1") 38 | app.get("/posts?page=1") 39 | print(cli.__dict__) 40 | 41 | 42 | def test_error(): 43 | api = Api() 44 | 45 | @api.site("https://news.ycombinator.com") 46 | @api.list(".athing") 47 | @api.route("/posts?page={page}", "/news?p={page}") 48 | @api.route("/posts", "/news?p=1") 49 | class Post(Item): 50 | url = Attr(".storylink", "no this attribute") 51 | title = Text(".storylink") 52 | 53 | app = App(api.app) 54 | with pytest.raises(Exception): 55 | app.get("/posts?page=1") 56 | -------------------------------------------------------------------------------- /toapi/__init__.py: -------------------------------------------------------------------------------- 1 | from toapi.api import Api 2 | from toapi.item import Item 3 | -------------------------------------------------------------------------------- /toapi/api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import traceback 3 | from collections import defaultdict 4 | from time import time 5 | 6 | import cchardet 7 | import requests 8 | from colorama import Fore 9 | from flask import Flask, jsonify, request 10 | from htmlfetcher import HTMLFetcher 11 | from parse import parse 12 | 13 | from toapi.log import logger 14 | 15 | 16 | class Api: 17 | def __init__(self, site: str = "", browser: str = None) -> None: 18 | self.app: Flask = Flask(__name__) 19 | self.browser = browser and HTMLFetcher(browser=browser) 20 | self._site = site.strip("/") 21 | self._routes: list = [] 22 | self._cache = defaultdict(dict) 23 | self._storage = defaultdict(str) 24 | self.__init_server() 25 | 26 | def __init_server(self) -> None: 27 | self.app.logger.setLevel(logging.ERROR) 28 | 29 | @self.app.route("/") 30 | def handler(path): 31 | try: 32 | start_time = time() 33 | full_path = request.full_path.strip("?") 34 | results = self.parse_url(full_path) 35 | end_time = time() 36 | time_usage = end_time - start_time 37 | res = jsonify(results) 38 | logger.info( 39 | Fore.GREEN, 40 | "Received", 41 | "%s %s 200 %.2fms" 42 | % (request.url, len(res.response), time_usage * 1000), 43 | ) 44 | return res 45 | except Exception as e: 46 | logger.error("Serving", f"{e}") 47 | logger.error("Serving", "%s" % str(traceback.format_exc())) 48 | return jsonify({"msg": "System Error", "code": -1}), 500 49 | 50 | def run(self, host="127.0.0.1", port=5000, **options): 51 | try: 52 | logger.info(Fore.GREEN, "Serving", f"http://{host}:{port}") 53 | self.app.run(host, port, **options) 54 | except Exception as e: 55 | logger.error("Serving", "%s" % str(e)) 56 | logger.error("Serving", "%s" % str(traceback.format_exc())) 57 | exit() 58 | 59 | def absolute_url(self, base_url, url: str) -> str: 60 | return "{}/{}".format(base_url, url.lstrip("/")) 61 | 62 | def convert_string(self, source_string, source_format, target_format): 63 | parsed_words = parse(source_format, source_string) 64 | if parsed_words is not None: 65 | target_string = target_format.format(**parsed_words.named) 66 | return target_string 67 | return None 68 | 69 | def parse_url(self, full_path: str) -> dict: 70 | results = self._cache.get(full_path) 71 | if results is not None: 72 | logger.info(Fore.YELLOW, "Cache", f"Get<{full_path}>") 73 | return results 74 | 75 | results = {} 76 | for source_format, target_format, item in self._routes: 77 | parsed_path = self.convert_string( 78 | full_path, source_format, target_format 79 | ) 80 | if parsed_path is not None: 81 | full_url = self.absolute_url(item._site, parsed_path) 82 | html = self.fetch(full_url) 83 | result = item.parse(html) 84 | logger.info( 85 | Fore.CYAN, 86 | "Parsed", 87 | f"Item<{item.__name__}[{len(result)}]>", 88 | ) 89 | results.update({item.__name__: result}) 90 | 91 | self._cache[full_path] = results 92 | logger.info(Fore.YELLOW, "Cache", f"Set<{full_path}>") 93 | 94 | return results 95 | 96 | def fetch(self, url: str) -> str: 97 | html = self._storage.get(url) 98 | if html is not None: 99 | logger.info(Fore.BLUE, "Storage", f"Get<{url}>") 100 | return html 101 | if self.browser is not None: 102 | html = self.browser.get(url) 103 | else: 104 | r = requests.get(url) 105 | content = r.content 106 | charset = cchardet.detect(content) 107 | html = content.decode(charset["encoding"] or "utf-8") 108 | logger.info(Fore.GREEN, "Sent", f"{url} {len(html)}") 109 | self._storage[url] = html 110 | logger.info(Fore.BLUE, "Storage", f"Set<{url}>") 111 | return html 112 | 113 | def route(self, source_format: str, target_format: str) -> callable: 114 | def fn(item): 115 | self._routes.append([source_format, target_format, item]) 116 | logger.info( 117 | Fore.GREEN, 118 | "Register", 119 | f"<{item.__name__}: {source_format} {target_format}>", 120 | ) 121 | 122 | return item 123 | 124 | return fn 125 | 126 | def list(self, selector: str) -> callable: 127 | def fn(item): 128 | item._list = True 129 | item._selector = selector 130 | return item 131 | 132 | return fn 133 | 134 | def site(self, site: str) -> callable: 135 | def fn(item): 136 | item._site = site or self._site 137 | item._site = item._site.strip("/") 138 | return item 139 | 140 | return fn 141 | -------------------------------------------------------------------------------- /toapi/cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from toapi import __version__ 4 | 5 | 6 | @click.group(context_settings={"help_option_names": ["-h", "--help"]}) 7 | @click.version_option(__version__, "-v", "--version") 8 | def cli(): 9 | """ 10 | Toapi - Every web site provides APIs. 11 | """ 12 | -------------------------------------------------------------------------------- /toapi/item.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from htmlparsing import HTMLParsing, Selector 4 | 5 | 6 | class ItemType(type): 7 | def __new__(cls, what, bases=None, attrdict=None): 8 | __fields__ = OrderedDict() 9 | 10 | for name, selector in attrdict.items(): 11 | if isinstance(selector, Selector): 12 | __fields__[name] = selector 13 | 14 | for name in __fields__.keys(): 15 | del attrdict[name] 16 | 17 | instance = type.__new__(cls, what, bases, attrdict) 18 | instance._list = None 19 | instance._site = None 20 | instance._selector = None 21 | instance.__fields__ = __fields__ 22 | return instance 23 | 24 | 25 | class Item(metaclass=ItemType): 26 | @classmethod 27 | def parse(cls, html: str): 28 | if cls._list: 29 | result = HTMLParsing(html).list(cls._selector, cls.__fields__) 30 | result = [cls._clean(item) for item in result] 31 | else: 32 | result = HTMLParsing(html).detail(cls.__fields__) 33 | result = cls._clean(result) 34 | return result 35 | 36 | @classmethod 37 | def _clean(cls, item): 38 | for name, selector in cls.__fields__.items(): 39 | clean_method = getattr(cls, "clean_%s" % name, None) 40 | if clean_method is not None: 41 | item[name] = clean_method(cls, item[name]) 42 | return item 43 | -------------------------------------------------------------------------------- /toapi/log.py: -------------------------------------------------------------------------------- 1 | """ 2 | logger.info(Fore.GREEN, 'Sent', 'https://fuck.com/path1 1231 200') 3 | logger.info(Fore.GREEN, 'Received', 'http://127.0.0.1/path2 231 200') 4 | logger.info(Fore.YELLOW, 'Cache', 'Set') 5 | logger.info(Fore.BLUE, 'Storage', 'Get') 6 | logger.info(Fore.CYAN, 'Parsed', 'Item') 7 | logger.error('Cache', 'Set') 8 | logger.error('Storage', 'Get') 9 | logger.error('Parse', 'Item') 10 | """ 11 | import logging 12 | 13 | import colorama 14 | from colorama import Fore, Style 15 | 16 | colorama.init(autoreset=True) 17 | 18 | 19 | class Logger: 20 | def __init__(self, name, level=logging.DEBUG): 21 | logging.basicConfig( 22 | format="%(asctime)s %(message)-10s ", datefmt="%Y/%m/%d %H:%M:%S" 23 | ) 24 | 25 | self.logger = logging.getLogger(name) 26 | self.logger.setLevel(level) 27 | 28 | def info(self, color, type, message): 29 | self.logger.info( 30 | color + "[%-8s] %-2s %s" % (type, "OK", message) + Style.RESET_ALL 31 | ) 32 | 33 | def error(self, type, message): 34 | self.logger.error( 35 | Fore.RED 36 | + "[%-8s] %-4s %s" % (type, "FAIL", message) 37 | + Style.RESET_ALL 38 | ) 39 | 40 | 41 | logger = Logger(__name__) 42 | --------------------------------------------------------------------------------