├── .coveragerc ├── .flake8 ├── .github └── workflows │ └── tests.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── poetry.lock ├── pyproject.toml ├── pytest.ini ├── src └── slackers │ ├── __init__.py │ ├── hooks.py │ ├── models.py │ ├── py.typed │ ├── registry.py │ ├── server.py │ └── verification.py ├── tests ├── __init__.py ├── actions.py ├── conftest.py ├── events.py ├── slash_command.py └── verification.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | setup.py 4 | .eggs/* 5 | .tox/* 6 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude= 3 | .tox 4 | __pycache__ 5 | .pytest_cache 6 | venv/ 7 | env/ 8 | _sandbox/ 9 | .eggs/ 10 | redis_shelve.egg-info/ 11 | max-line-length = 88 12 | select = C,E,F,W,B,B950 13 | ignore = E203,E501,W503,F811 14 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: "Run tests." 2 | on: 3 | - push 4 | jobs: 5 | test: 6 | runs-on: "ubuntu-latest" 7 | strategy: 8 | matrix: 9 | python: 10 | - "3.7" 11 | - "3.8" 12 | - "3.9" 13 | - "3.10" 14 | - "3.11" 15 | steps: 16 | - uses: "actions/checkout@v3" 17 | - name: "Setup python" 18 | uses: "actions/setup-python@v4" 19 | with: 20 | python-version: ${{ matrix.python }} 21 | - name: "Install poetry" 22 | run: "pip install poetry==1.3.2" 23 | - name: "Install dependencies" 24 | run: "poetry install" 25 | - name: "Run tests" 26 | run: "poetry run pytest" 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .eggs 3 | .coverage 4 | *.egg-info/ 5 | .tox 6 | dist/ 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Slackers change log 2 | ## 1.0.3 3 | ### Added 4 | - Update dependencies. 5 | - Add python 3.11 to test environments. 6 | 7 | ### Removed 8 | - Removed python 3.6 from test environments. 9 | ## 1.0.2 10 | ### Added 11 | - Update python dependencies. 12 | - Add python 3.10 test environment. 13 | 14 | ## 1.0.1 15 | ### Added 16 | - Add empty py.typed file to show module supports PEP 561 (@Tenzer) 17 | 18 | ## 1.0.0 19 | ### Added 20 | - Add specific event emission for interactive messages. 21 | - Support python 3.9. 22 | 23 | ## 0.4.4 - 2020-04-30 24 | ### Fixed 25 | - Allow authed_users of events to be empty (@GH-maggio) 26 | 27 | ## 0.4.3 - 2020-03-06 28 | - Add text to command model (@kylecrawshaw) 29 | 30 | ## 0.4.2 - 2020-02-02 31 | ### Added 32 | - Tox as develop dependency. Upgrade package versions. 33 | 34 | ## 0.4.1 - 2020-02-02 35 | ### Fixed 36 | - Duplicate attribute user_id on the SlackCommand model (@skmatz). 37 | 38 | ## 0.4.0 - 2020-02-01 39 | - This change log. 40 | 41 | ### Added 42 | - Decorator `responder` adds ability to return custom responses to Slack. 43 | 44 | ## 0.3.0 45 | ### Added 46 | - Support for `view_submission` and `view_closed` actions (@jamestiotio). 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Niels van Huijstee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slackers 2 | 3 | Slack webhooks API served by FastAPI 4 | 5 | ## What is Slackers 6 | Slackers is a [FastAPI](https://fastapi.tiangolo.com) implementation to handle Slack interactions and events. 7 | It serves endpoints to receive [slash commands](https://api.slack.com/interactivity/slash-commands), 8 | [app actions](https://api.slack.com/interactivity/actions), [interactive components](https://api.slack.com/interactivity/components). 9 | It also listens for events sent to the Slack Events API [Slack Events](https://api.slack.com/events-api). 10 | 11 | ### Low maintenance 12 | :warning: This repository is in low maintenance mode. I have not used Slack for a few years 13 | and while I am willing to perform the occasional chores like dependency updates, 14 | it is unlikely that I will be adding features. Also, I am no longer actively using 15 | Slackers myself. 16 | 17 | ## Installation 18 | You can install Slackers with pip 19 | `$ pip install slackers` 20 | 21 | ## Configuration 22 | ### `SLACK_SIGNING_SECRET` 23 | You must configure the slack signing secret. This will be used to 24 | verify the incoming requests signature. 25 | `$ export SLACK_SIGNING_SECRET=your_slack_signing_secret` 26 | 27 | ## Example usage 28 | Slackers will listen for activity from the Events API on `/events`, for 29 | interactive components on `/actions` and for slash commands on `/commands`. 30 | When an interaction is received, it will emit an event. You can listen 31 | for these events as shown in the following examples. 32 | 33 | On receiving a request, Slackers will emit an event which you can handle yourself. 34 | Slackers will also respond to Slack with an (empty) http 200 response telling Slack 35 | all is well received. 36 | 37 | ### Starting the server 38 | As said, Slackers uses the excellent FastAPI to serve it's endpoints. Since you're here, 39 | I'm assuming you know what FastAPI is, but if you don't, you can learn all about 40 | how that works with [this tutorial](https://fastapi.tiangolo.com/tutorial/). 41 | 42 | Slackers offers you a router which you can include in your own FastAPI. 43 | ```python 44 | from fastapi import FastAPI 45 | from slackers.server import router 46 | 47 | app = FastAPI() 48 | app.include_router(router) 49 | 50 | # Optionally you can use a prefix 51 | app.include_router(router, prefix='/slack') 52 | ``` 53 | 54 | ### Events 55 | Once your server is running, the events endpoint is setup at `/events`, or if you use 56 | the prefix as shown above, on `/slack/events`. 57 | 58 | #### Accepting the challenge 59 | When setting up Slack to [send events](https://api.slack.com/events-api#subscribing_to_event_types), 60 | it will first send a challenge to verify your endpoint. Slackers detects when a challenge is sent. 61 | You can simply start our api and Slackers will meet the challenge automatically. 62 | 63 | #### Responding to events 64 | On receiving an event, Slackers will emit a python event, which you can act upon as shown below. 65 | ```python 66 | import logging 67 | from slackers.hooks import events 68 | 69 | log = logging.getLogger(__name__) 70 | 71 | @events.on("app_mention") 72 | def handle_mention(payload): 73 | log.info("App was mentioned.") 74 | log.debug(payload) 75 | ``` 76 | 77 | 78 | ### Actions 79 | Once your server is running, the actions endpoint is setup at `/actions`, or if you use 80 | the prefix as shown above, on `/slack/actions`. 81 | 82 | #### Responding to actions 83 | On receiving an action, Slackers will emit a python event, which you can listen for as 84 | shown below. You can listen for the action type, or more specifically for the action id 85 | or callback id linked to the action. 86 | ```python 87 | import logging 88 | from slackers.hooks import actions 89 | 90 | log = logging.getLogger(__name__) 91 | 92 | # Listening for the action type. 93 | @actions.on("block_actions") 94 | def handle_action(payload): 95 | log.info("Action started.") 96 | log.debug(payload) 97 | 98 | # Listen for an action by it's action_id 99 | @actions.on("block_actions:your_action_id") 100 | def handle_action_by_id(payload): 101 | log.info("Action started.") 102 | log.debug(payload) 103 | 104 | # Listen for an action by it's callback_id 105 | @actions.on("block_actions:your_callback_id") 106 | def handle_action_by_callback_id(payload): 107 | log.info(f"Action started.") 108 | log.debug(payload) 109 | ``` 110 | 111 | #### Interactive messages 112 | Interactive message actions do not have an `action_id`. They do have a `name` and a `type`. 113 | To act upon interactive messages, you can listen for the action type, `interactive_message` 114 | as wel as the combination of the `interactive_message` and `name`, `type` or both. 115 | ```python 116 | import logging 117 | from slackers.hooks import actions 118 | 119 | log = logging.getLogger(__name__) 120 | 121 | # Listening for the action type. 122 | @actions.on("interactive_message") 123 | def handle_action(payload): 124 | log.info("Action started.") 125 | log.debug(payload) 126 | 127 | # Listen for an action by it's name 128 | @actions.on("interactive_message:action_name") 129 | def handle_action_by_id(payload): 130 | log.info("Action started.") 131 | log.debug(payload) 132 | 133 | # Listen for an action by it's type 134 | @actions.on("interactive_message:action_type") 135 | def handle_action_by_callback_id(payload): 136 | log.info(f"Action started.") 137 | log.debug(payload) 138 | 139 | # Listen for an action by it's name and type 140 | @actions.on("interactive_message:action_name:action_type") 141 | def handle_action_by_callback_id(payload): 142 | log.info(f"Action started.") 143 | log.debug(payload) 144 | ``` 145 | 146 | #### Custom responses 147 | Slackers tries to be fast to respond to Slack. The events you are listening for with the 148 | likes of `@actions.on(...)` are scheduled as an async task in a fire and forget fashion. 149 | After scheduling these events, Slackers will by default return an empty 200 response which 150 | might happen before the events are handled. 151 | 152 | In some cases you might want to act on the payload and return a custom response to Slack. 153 | For this, you can use the slackers `responder` decorator to define your custom handler 154 | function. This function is then used as a callback instead of returning the default response. 155 | You must ensure your custom handler returns a `starlette.responses.Response` or one of it's 156 | subclasses. You must furthermore ensure that there is only one responder responding to your 157 | Slack request. 158 | 159 | Please note that the events are also emitted, so you could have both `@actions.on("block_action:xyz")` 160 | and `@responder("block_action:xyz")`. Just keep in mind that the event emissions are async and are 161 | not awaited. In other words, Slackers doesn't ensure that the response (whether your custom response 162 | or the default) is returned before or after the events are emitted. 163 | 164 | ```python 165 | from starlette.responses import JSONResponse 166 | from slackers.hooks import responder 167 | 168 | @responder("block_actions:your_callback_id") 169 | def custom_handler(payload): 170 | # handle your payload 171 | ... 172 | return JSONResponse(content={"custom": "Custom Response"}) 173 | ``` 174 | 175 | ### Slash commands 176 | Once your server is running, the commands endpoint is setup at `/commands`, or if you use 177 | the prefix as shown above, on `/slack/commands`. Slackers will emit an event with the name 178 | of the command, so if your command is `/engage`, you can listen for the event `engage` 179 | (without the slash) 180 | 181 | #### Responding to slash commands 182 | On receiving a command, Slackers will emit a python event, which you can listen for as shown below. 183 | ```python 184 | import logging 185 | from slackers.hooks import commands 186 | 187 | log = logging.getLogger(__name__) 188 | 189 | 190 | @commands.on("engage") # responds to "/engage" 191 | def handle_command(payload): 192 | log.info("Command received") 193 | log.debug(payload) 194 | ``` 195 | 196 | ### Async 197 | Since events are emitted using pyee's Async event emitter, it is possible to define your event handlers 198 | as async functions. Just keep in mind that errors are in this case emitted on the 'error' event. 199 | 200 | ```python 201 | import logging 202 | from slackers.hooks import commands 203 | 204 | log = logging.getLogger(__name__) 205 | 206 | @commands.on('error') 207 | def log_error(exc): 208 | log.error(str(exc)) 209 | 210 | 211 | @commands.on("engage") # responds to "/engage" 212 | async def handle_command(payload): 213 | ... 214 | ``` 215 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "anyio" 5 | version = "3.6.2" 6 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 7 | category = "main" 8 | optional = false 9 | python-versions = ">=3.6.2" 10 | files = [ 11 | {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, 12 | {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, 13 | ] 14 | 15 | [package.dependencies] 16 | idna = ">=2.8" 17 | sniffio = ">=1.1" 18 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 19 | 20 | [package.extras] 21 | doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 22 | test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] 23 | trio = ["trio (>=0.16,<0.22)"] 24 | 25 | [[package]] 26 | name = "appdirs" 27 | version = "1.4.4" 28 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 29 | category = "dev" 30 | optional = false 31 | python-versions = "*" 32 | files = [ 33 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 34 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 35 | ] 36 | 37 | [[package]] 38 | name = "atomicwrites" 39 | version = "1.4.1" 40 | description = "Atomic file writes." 41 | category = "dev" 42 | optional = false 43 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 44 | files = [ 45 | {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, 46 | ] 47 | 48 | [[package]] 49 | name = "attrs" 50 | version = "22.2.0" 51 | description = "Classes Without Boilerplate" 52 | category = "dev" 53 | optional = false 54 | python-versions = ">=3.6" 55 | files = [ 56 | {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, 57 | {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, 58 | ] 59 | 60 | [package.extras] 61 | cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] 62 | dev = ["attrs[docs,tests]"] 63 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] 64 | tests = ["attrs[tests-no-zope]", "zope.interface"] 65 | tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] 66 | 67 | [[package]] 68 | name = "black" 69 | version = "20.8b1" 70 | description = "The uncompromising code formatter." 71 | category = "dev" 72 | optional = false 73 | python-versions = ">=3.6" 74 | files = [ 75 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 76 | ] 77 | 78 | [package.dependencies] 79 | appdirs = "*" 80 | click = ">=7.1.2" 81 | mypy-extensions = ">=0.4.3" 82 | pathspec = ">=0.6,<1" 83 | regex = ">=2020.1.8" 84 | toml = ">=0.10.1" 85 | typed-ast = ">=1.4.0" 86 | typing-extensions = ">=3.7.4" 87 | 88 | [package.extras] 89 | colorama = ["colorama (>=0.4.3)"] 90 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 91 | 92 | [[package]] 93 | name = "certifi" 94 | version = "2022.12.7" 95 | description = "Python package for providing Mozilla's CA Bundle." 96 | category = "main" 97 | optional = false 98 | python-versions = ">=3.6" 99 | files = [ 100 | {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, 101 | {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, 102 | ] 103 | 104 | [[package]] 105 | name = "charset-normalizer" 106 | version = "3.0.1" 107 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 108 | category = "main" 109 | optional = false 110 | python-versions = "*" 111 | files = [ 112 | {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"}, 113 | {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"}, 114 | {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"}, 115 | {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"}, 116 | {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"}, 117 | {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"}, 118 | {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"}, 119 | {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"}, 120 | {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"}, 121 | {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"}, 122 | {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"}, 123 | {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"}, 124 | {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"}, 125 | {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"}, 126 | {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"}, 127 | {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"}, 128 | {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"}, 129 | {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"}, 130 | {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"}, 131 | {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"}, 132 | {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"}, 133 | {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"}, 134 | {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"}, 135 | {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"}, 136 | {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"}, 137 | {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"}, 138 | {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"}, 139 | {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"}, 140 | {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"}, 141 | {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"}, 142 | {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"}, 143 | {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"}, 144 | {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"}, 145 | {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"}, 146 | {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"}, 147 | {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"}, 148 | {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"}, 149 | {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"}, 150 | {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"}, 151 | {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"}, 152 | {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"}, 153 | {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"}, 154 | {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"}, 155 | {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"}, 156 | {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"}, 157 | {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"}, 158 | {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"}, 159 | {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"}, 160 | {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"}, 161 | {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"}, 162 | {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"}, 163 | {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"}, 164 | {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"}, 165 | {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"}, 166 | {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"}, 167 | {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"}, 168 | {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"}, 169 | {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"}, 170 | {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"}, 171 | {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"}, 172 | {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"}, 173 | {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"}, 174 | {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"}, 175 | {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"}, 176 | {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"}, 177 | {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"}, 178 | {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"}, 179 | {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"}, 180 | {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"}, 181 | {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"}, 182 | {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"}, 183 | {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"}, 184 | {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"}, 185 | {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"}, 186 | {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"}, 187 | {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"}, 188 | {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"}, 189 | {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"}, 190 | {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"}, 191 | {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"}, 192 | {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"}, 193 | {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"}, 194 | {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"}, 195 | {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"}, 196 | {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"}, 197 | {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"}, 198 | {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"}, 199 | {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"}, 200 | ] 201 | 202 | [[package]] 203 | name = "click" 204 | version = "8.1.3" 205 | description = "Composable command line interface toolkit" 206 | category = "dev" 207 | optional = false 208 | python-versions = ">=3.7" 209 | files = [ 210 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 211 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 212 | ] 213 | 214 | [package.dependencies] 215 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 216 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 217 | 218 | [[package]] 219 | name = "colorama" 220 | version = "0.4.6" 221 | description = "Cross-platform colored terminal text." 222 | category = "dev" 223 | optional = false 224 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 225 | files = [ 226 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 227 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 228 | ] 229 | 230 | [[package]] 231 | name = "coverage" 232 | version = "7.2.1" 233 | description = "Code coverage measurement for Python" 234 | category = "dev" 235 | optional = false 236 | python-versions = ">=3.7" 237 | files = [ 238 | {file = "coverage-7.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49567ec91fc5e0b15356da07a2feabb421d62f52a9fff4b1ec40e9e19772f5f8"}, 239 | {file = "coverage-7.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2ef6cae70168815ed91388948b5f4fcc69681480a0061114db737f957719f03"}, 240 | {file = "coverage-7.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3004765bca3acd9e015794e5c2f0c9a05587f5e698127ff95e9cfba0d3f29339"}, 241 | {file = "coverage-7.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cca7c0b7f5881dfe0291ef09ba7bb1582cb92ab0aeffd8afb00c700bf692415a"}, 242 | {file = "coverage-7.2.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2167d116309f564af56f9aa5e75ef710ef871c5f9b313a83050035097b56820"}, 243 | {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cb5f152fb14857cbe7f3e8c9a5d98979c4c66319a33cad6e617f0067c9accdc4"}, 244 | {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:87dc37f16fb5e3a28429e094145bf7c1753e32bb50f662722e378c5851f7fdc6"}, 245 | {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e191a63a05851f8bce77bc875e75457f9b01d42843f8bd7feed2fc26bbe60833"}, 246 | {file = "coverage-7.2.1-cp310-cp310-win32.whl", hash = "sha256:e3ea04b23b114572b98a88c85379e9e9ae031272ba1fb9b532aa934c621626d4"}, 247 | {file = "coverage-7.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:0cf557827be7eca1c38a2480484d706693e7bb1929e129785fe59ec155a59de6"}, 248 | {file = "coverage-7.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:570c21a29493b350f591a4b04c158ce1601e8d18bdcd21db136fbb135d75efa6"}, 249 | {file = "coverage-7.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e872b082b32065ac2834149dc0adc2a2e6d8203080501e1e3c3c77851b466f9"}, 250 | {file = "coverage-7.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fac6343bae03b176e9b58104a9810df3cdccd5cfed19f99adfa807ffbf43cf9b"}, 251 | {file = "coverage-7.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abacd0a738e71b20e224861bc87e819ef46fedba2fb01bc1af83dfd122e9c319"}, 252 | {file = "coverage-7.2.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9256d4c60c4bbfec92721b51579c50f9e5062c21c12bec56b55292464873508"}, 253 | {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:80559eaf6c15ce3da10edb7977a1548b393db36cbc6cf417633eca05d84dd1ed"}, 254 | {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0bd7e628f6c3ec4e7d2d24ec0e50aae4e5ae95ea644e849d92ae4805650b4c4e"}, 255 | {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09643fb0df8e29f7417adc3f40aaf379d071ee8f0350ab290517c7004f05360b"}, 256 | {file = "coverage-7.2.1-cp311-cp311-win32.whl", hash = "sha256:1b7fb13850ecb29b62a447ac3516c777b0e7a09ecb0f4bb6718a8654c87dfc80"}, 257 | {file = "coverage-7.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:617a94ada56bbfe547aa8d1b1a2b8299e2ec1ba14aac1d4b26a9f7d6158e1273"}, 258 | {file = "coverage-7.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8649371570551d2fd7dee22cfbf0b61f1747cdfb2b7587bb551e4beaaa44cb97"}, 259 | {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d2b9b5e70a21474c105a133ba227c61bc95f2ac3b66861143ce39a5ea4b3f84"}, 260 | {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae82c988954722fa07ec5045c57b6d55bc1a0890defb57cf4a712ced65b26ddd"}, 261 | {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:861cc85dfbf55a7a768443d90a07e0ac5207704a9f97a8eb753292a7fcbdfcfc"}, 262 | {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0339dc3237c0d31c3b574f19c57985fcbe494280153bbcad33f2cdf469f4ac3e"}, 263 | {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5928b85416a388dd557ddc006425b0c37e8468bd1c3dc118c1a3de42f59e2a54"}, 264 | {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8d3843ca645f62c426c3d272902b9de90558e9886f15ddf5efe757b12dd376f5"}, 265 | {file = "coverage-7.2.1-cp37-cp37m-win32.whl", hash = "sha256:6a034480e9ebd4e83d1aa0453fd78986414b5d237aea89a8fdc35d330aa13bae"}, 266 | {file = "coverage-7.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6fce673f79a0e017a4dc35e18dc7bb90bf6d307c67a11ad5e61ca8d42b87cbff"}, 267 | {file = "coverage-7.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f099da6958ddfa2ed84bddea7515cb248583292e16bb9231d151cd528eab657"}, 268 | {file = "coverage-7.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:97a3189e019d27e914ecf5c5247ea9f13261d22c3bb0cfcfd2a9b179bb36f8b1"}, 269 | {file = "coverage-7.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a81dbcf6c6c877986083d00b834ac1e84b375220207a059ad45d12f6e518a4e3"}, 270 | {file = "coverage-7.2.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2c3dde4c0b9be4b02067185136b7ee4681978228ad5ec1278fa74f5ca3e99"}, 271 | {file = "coverage-7.2.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a209d512d157379cc9ab697cbdbb4cfd18daa3e7eebaa84c3d20b6af0037384"}, 272 | {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f3d07edb912a978915576a776756069dede66d012baa503022d3a0adba1b6afa"}, 273 | {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8dca3c1706670297851bca1acff9618455122246bdae623be31eca744ade05ec"}, 274 | {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b1991a6d64231a3e5bbe3099fb0dd7c9aeaa4275ad0e0aeff4cb9ef885c62ba2"}, 275 | {file = "coverage-7.2.1-cp38-cp38-win32.whl", hash = "sha256:22c308bc508372576ffa3d2dbc4824bb70d28eeb4fcd79d4d1aed663a06630d0"}, 276 | {file = "coverage-7.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:b0c0d46de5dd97f6c2d1b560bf0fcf0215658097b604f1840365296302a9d1fb"}, 277 | {file = "coverage-7.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4dd34a935de268a133e4741827ae951283a28c0125ddcdbcbba41c4b98f2dfef"}, 278 | {file = "coverage-7.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f8318ed0f3c376cfad8d3520f496946977abde080439d6689d7799791457454"}, 279 | {file = "coverage-7.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:834c2172edff5a08d78e2f53cf5e7164aacabeb66b369f76e7bb367ca4e2d993"}, 280 | {file = "coverage-7.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4d70c853f0546855f027890b77854508bdb4d6a81242a9d804482e667fff6e6"}, 281 | {file = "coverage-7.2.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a6450da4c7afc4534305b2b7d8650131e130610cea448ff240b6ab73d7eab63"}, 282 | {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:99f4dd81b2bb8fc67c3da68b1f5ee1650aca06faa585cbc6818dbf67893c6d58"}, 283 | {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bdd3f2f285ddcf2e75174248b2406189261a79e7fedee2ceeadc76219b6faa0e"}, 284 | {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f29351393eb05e6326f044a7b45ed8e38cb4dcc38570d12791f271399dc41431"}, 285 | {file = "coverage-7.2.1-cp39-cp39-win32.whl", hash = "sha256:e2b50ebc2b6121edf352336d503357321b9d8738bb7a72d06fc56153fd3f4cd8"}, 286 | {file = "coverage-7.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:bd5a12239c0006252244f94863f1c518ac256160cd316ea5c47fb1a11b25889a"}, 287 | {file = "coverage-7.2.1-pp37.pp38.pp39-none-any.whl", hash = "sha256:436313d129db7cf5b4ac355dd2bd3f7c7e5294af077b090b85de75f8458b8616"}, 288 | {file = "coverage-7.2.1.tar.gz", hash = "sha256:c77f2a9093ccf329dd523a9b2b3c854c20d2a3d968b6def3b820272ca6732242"}, 289 | ] 290 | 291 | [package.extras] 292 | toml = ["tomli"] 293 | 294 | [[package]] 295 | name = "distlib" 296 | version = "0.3.6" 297 | description = "Distribution utilities" 298 | category = "dev" 299 | optional = false 300 | python-versions = "*" 301 | files = [ 302 | {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, 303 | {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, 304 | ] 305 | 306 | [[package]] 307 | name = "fastapi" 308 | version = "0.92.0" 309 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 310 | category = "main" 311 | optional = false 312 | python-versions = ">=3.7" 313 | files = [ 314 | {file = "fastapi-0.92.0-py3-none-any.whl", hash = "sha256:ae7b97c778e2f2ec3fb3cb4fb14162129411d99907fb71920f6d69a524340ebf"}, 315 | {file = "fastapi-0.92.0.tar.gz", hash = "sha256:023a0f5bd2c8b2609014d3bba1e14a1d7df96c6abea0a73070621c9862b9a4de"}, 316 | ] 317 | 318 | [package.dependencies] 319 | pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" 320 | starlette = ">=0.25.0,<0.26.0" 321 | 322 | [package.extras] 323 | all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] 324 | dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"] 325 | doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer[all] (>=0.6.1,<0.8.0)"] 326 | test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.10.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.6.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] 327 | 328 | [[package]] 329 | name = "filelock" 330 | version = "3.9.0" 331 | description = "A platform independent file lock." 332 | category = "dev" 333 | optional = false 334 | python-versions = ">=3.7" 335 | files = [ 336 | {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, 337 | {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, 338 | ] 339 | 340 | [package.extras] 341 | docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] 342 | testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] 343 | 344 | [[package]] 345 | name = "h11" 346 | version = "0.14.0" 347 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 348 | category = "dev" 349 | optional = false 350 | python-versions = ">=3.7" 351 | files = [ 352 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 353 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 354 | ] 355 | 356 | [package.dependencies] 357 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 358 | 359 | [[package]] 360 | name = "httpcore" 361 | version = "0.16.3" 362 | description = "A minimal low-level HTTP client." 363 | category = "dev" 364 | optional = false 365 | python-versions = ">=3.7" 366 | files = [ 367 | {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, 368 | {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, 369 | ] 370 | 371 | [package.dependencies] 372 | anyio = ">=3.0,<5.0" 373 | certifi = "*" 374 | h11 = ">=0.13,<0.15" 375 | sniffio = ">=1.0.0,<2.0.0" 376 | 377 | [package.extras] 378 | http2 = ["h2 (>=3,<5)"] 379 | socks = ["socksio (>=1.0.0,<2.0.0)"] 380 | 381 | [[package]] 382 | name = "httpx" 383 | version = "0.23.3" 384 | description = "The next generation HTTP client." 385 | category = "dev" 386 | optional = false 387 | python-versions = ">=3.7" 388 | files = [ 389 | {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, 390 | {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, 391 | ] 392 | 393 | [package.dependencies] 394 | certifi = "*" 395 | httpcore = ">=0.15.0,<0.17.0" 396 | rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} 397 | sniffio = "*" 398 | 399 | [package.extras] 400 | brotli = ["brotli", "brotlicffi"] 401 | cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] 402 | http2 = ["h2 (>=3,<5)"] 403 | socks = ["socksio (>=1.0.0,<2.0.0)"] 404 | 405 | [[package]] 406 | name = "idna" 407 | version = "3.4" 408 | description = "Internationalized Domain Names in Applications (IDNA)" 409 | category = "main" 410 | optional = false 411 | python-versions = ">=3.5" 412 | files = [ 413 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 414 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 415 | ] 416 | 417 | [[package]] 418 | name = "importlib-metadata" 419 | version = "6.0.0" 420 | description = "Read metadata from Python packages" 421 | category = "dev" 422 | optional = false 423 | python-versions = ">=3.7" 424 | files = [ 425 | {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, 426 | {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, 427 | ] 428 | 429 | [package.dependencies] 430 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 431 | zipp = ">=0.5" 432 | 433 | [package.extras] 434 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 435 | perf = ["ipython"] 436 | testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] 437 | 438 | [[package]] 439 | name = "iniconfig" 440 | version = "2.0.0" 441 | description = "brain-dead simple config-ini parsing" 442 | category = "dev" 443 | optional = false 444 | python-versions = ">=3.7" 445 | files = [ 446 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 447 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 448 | ] 449 | 450 | [[package]] 451 | name = "mypy-extensions" 452 | version = "1.0.0" 453 | description = "Type system extensions for programs checked with the mypy type checker." 454 | category = "dev" 455 | optional = false 456 | python-versions = ">=3.5" 457 | files = [ 458 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 459 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 460 | ] 461 | 462 | [[package]] 463 | name = "packaging" 464 | version = "23.0" 465 | description = "Core utilities for Python packages" 466 | category = "dev" 467 | optional = false 468 | python-versions = ">=3.7" 469 | files = [ 470 | {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, 471 | {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, 472 | ] 473 | 474 | [[package]] 475 | name = "pathspec" 476 | version = "0.11.0" 477 | description = "Utility library for gitignore style pattern matching of file paths." 478 | category = "dev" 479 | optional = false 480 | python-versions = ">=3.7" 481 | files = [ 482 | {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, 483 | {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, 484 | ] 485 | 486 | [[package]] 487 | name = "platformdirs" 488 | version = "3.1.0" 489 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 490 | category = "dev" 491 | optional = false 492 | python-versions = ">=3.7" 493 | files = [ 494 | {file = "platformdirs-3.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"}, 495 | {file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"}, 496 | ] 497 | 498 | [package.dependencies] 499 | typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} 500 | 501 | [package.extras] 502 | docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] 503 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] 504 | 505 | [[package]] 506 | name = "pluggy" 507 | version = "1.0.0" 508 | description = "plugin and hook calling mechanisms for python" 509 | category = "dev" 510 | optional = false 511 | python-versions = ">=3.6" 512 | files = [ 513 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 514 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 515 | ] 516 | 517 | [package.dependencies] 518 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 519 | 520 | [package.extras] 521 | dev = ["pre-commit", "tox"] 522 | testing = ["pytest", "pytest-benchmark"] 523 | 524 | [[package]] 525 | name = "py" 526 | version = "1.11.0" 527 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 528 | category = "dev" 529 | optional = false 530 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 531 | files = [ 532 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 533 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 534 | ] 535 | 536 | [[package]] 537 | name = "pydantic" 538 | version = "1.10.5" 539 | description = "Data validation and settings management using python type hints" 540 | category = "main" 541 | optional = false 542 | python-versions = ">=3.7" 543 | files = [ 544 | {file = "pydantic-1.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5920824fe1e21cbb3e38cf0f3dd24857c8959801d1031ce1fac1d50857a03bfb"}, 545 | {file = "pydantic-1.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3bb99cf9655b377db1a9e47fa4479e3330ea96f4123c6c8200e482704bf1eda2"}, 546 | {file = "pydantic-1.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2185a3b3d98ab4506a3f6707569802d2d92c3a7ba3a9a35683a7709ea6c2aaa2"}, 547 | {file = "pydantic-1.10.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f582cac9d11c227c652d3ce8ee223d94eb06f4228b52a8adaafa9fa62e73d5c9"}, 548 | {file = "pydantic-1.10.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c9e5b778b6842f135902e2d82624008c6a79710207e28e86966cd136c621bfee"}, 549 | {file = "pydantic-1.10.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72ef3783be8cbdef6bca034606a5de3862be6b72415dc5cb1fb8ddbac110049a"}, 550 | {file = "pydantic-1.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:45edea10b75d3da43cfda12f3792833a3fa70b6eee4db1ed6aed528cef17c74e"}, 551 | {file = "pydantic-1.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:63200cd8af1af2c07964546b7bc8f217e8bda9d0a2ef0ee0c797b36353914984"}, 552 | {file = "pydantic-1.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:305d0376c516b0dfa1dbefeae8c21042b57b496892d721905a6ec6b79494a66d"}, 553 | {file = "pydantic-1.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fd326aff5d6c36f05735c7c9b3d5b0e933b4ca52ad0b6e4b38038d82703d35b"}, 554 | {file = "pydantic-1.10.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bb0452d7b8516178c969d305d9630a3c9b8cf16fcf4713261c9ebd465af0d73"}, 555 | {file = "pydantic-1.10.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9a9d9155e2a9f38b2eb9374c88f02fd4d6851ae17b65ee786a87d032f87008f8"}, 556 | {file = "pydantic-1.10.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f836444b4c5ece128b23ec36a446c9ab7f9b0f7981d0d27e13a7c366ee163f8a"}, 557 | {file = "pydantic-1.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:8481dca324e1c7b715ce091a698b181054d22072e848b6fc7895cd86f79b4449"}, 558 | {file = "pydantic-1.10.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:87f831e81ea0589cd18257f84386bf30154c5f4bed373b7b75e5cb0b5d53ea87"}, 559 | {file = "pydantic-1.10.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ce1612e98c6326f10888df951a26ec1a577d8df49ddcaea87773bfbe23ba5cc"}, 560 | {file = "pydantic-1.10.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58e41dd1e977531ac6073b11baac8c013f3cd8706a01d3dc74e86955be8b2c0c"}, 561 | {file = "pydantic-1.10.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6a4b0aab29061262065bbdede617ef99cc5914d1bf0ddc8bcd8e3d7928d85bd6"}, 562 | {file = "pydantic-1.10.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:36e44a4de37b8aecffa81c081dbfe42c4d2bf9f6dff34d03dce157ec65eb0f15"}, 563 | {file = "pydantic-1.10.5-cp37-cp37m-win_amd64.whl", hash = "sha256:261f357f0aecda005934e413dfd7aa4077004a174dafe414a8325e6098a8e419"}, 564 | {file = "pydantic-1.10.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b429f7c457aebb7fbe7cd69c418d1cd7c6fdc4d3c8697f45af78b8d5a7955760"}, 565 | {file = "pydantic-1.10.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:663d2dd78596c5fa3eb996bc3f34b8c2a592648ad10008f98d1348be7ae212fb"}, 566 | {file = "pydantic-1.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51782fd81f09edcf265823c3bf43ff36d00db246eca39ee765ef58dc8421a642"}, 567 | {file = "pydantic-1.10.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c428c0f64a86661fb4873495c4fac430ec7a7cef2b8c1c28f3d1a7277f9ea5ab"}, 568 | {file = "pydantic-1.10.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:76c930ad0746c70f0368c4596020b736ab65b473c1f9b3872310a835d852eb19"}, 569 | {file = "pydantic-1.10.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3257bd714de9db2102b742570a56bf7978e90441193acac109b1f500290f5718"}, 570 | {file = "pydantic-1.10.5-cp38-cp38-win_amd64.whl", hash = "sha256:f5bee6c523d13944a1fdc6f0525bc86dbbd94372f17b83fa6331aabacc8fd08e"}, 571 | {file = "pydantic-1.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:532e97c35719f137ee5405bd3eeddc5c06eb91a032bc755a44e34a712420daf3"}, 572 | {file = "pydantic-1.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ca9075ab3de9e48b75fa8ccb897c34ccc1519177ad8841d99f7fd74cf43be5bf"}, 573 | {file = "pydantic-1.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd46a0e6296346c477e59a954da57beaf9c538da37b9df482e50f836e4a7d4bb"}, 574 | {file = "pydantic-1.10.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3353072625ea2a9a6c81ad01b91e5c07fa70deb06368c71307529abf70d23325"}, 575 | {file = "pydantic-1.10.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3f9d9b2be177c3cb6027cd67fbf323586417868c06c3c85d0d101703136e6b31"}, 576 | {file = "pydantic-1.10.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b473d00ccd5c2061fd896ac127b7755baad233f8d996ea288af14ae09f8e0d1e"}, 577 | {file = "pydantic-1.10.5-cp39-cp39-win_amd64.whl", hash = "sha256:5f3bc8f103b56a8c88021d481410874b1f13edf6e838da607dcb57ecff9b4594"}, 578 | {file = "pydantic-1.10.5-py3-none-any.whl", hash = "sha256:7c5b94d598c90f2f46b3a983ffb46ab806a67099d118ae0da7ef21a2a4033b28"}, 579 | {file = "pydantic-1.10.5.tar.gz", hash = "sha256:9e337ac83686645a46db0e825acceea8e02fca4062483f40e9ae178e8bd1103a"}, 580 | ] 581 | 582 | [package.dependencies] 583 | typing-extensions = ">=4.2.0" 584 | 585 | [package.extras] 586 | dotenv = ["python-dotenv (>=0.10.4)"] 587 | email = ["email-validator (>=1.0.3)"] 588 | 589 | [[package]] 590 | name = "pyee" 591 | version = "6.0.0" 592 | description = "A port of node.js's EventEmitter to python." 593 | category = "main" 594 | optional = false 595 | python-versions = "*" 596 | files = [ 597 | {file = "pyee-6.0.0-py2.py3-none-any.whl", hash = "sha256:dbe44f61c40a995d2bdfd83d9fcb87ae025882d2c7f366513325e3daa09d7ede"}, 598 | {file = "pyee-6.0.0.tar.gz", hash = "sha256:a9c9b60e8693a260dd942ef5a71358cfcbba15792d5e72caf0e3c891c4e91c3b"}, 599 | ] 600 | 601 | [[package]] 602 | name = "pytest" 603 | version = "6.2.5" 604 | description = "pytest: simple powerful testing with Python" 605 | category = "dev" 606 | optional = false 607 | python-versions = ">=3.6" 608 | files = [ 609 | {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, 610 | {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, 611 | ] 612 | 613 | [package.dependencies] 614 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 615 | attrs = ">=19.2.0" 616 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 617 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 618 | iniconfig = "*" 619 | packaging = "*" 620 | pluggy = ">=0.12,<2.0" 621 | py = ">=1.8.2" 622 | toml = "*" 623 | 624 | [package.extras] 625 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 626 | 627 | [[package]] 628 | name = "pytest-cov" 629 | version = "2.12.1" 630 | description = "Pytest plugin for measuring coverage." 631 | category = "dev" 632 | optional = false 633 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 634 | files = [ 635 | {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, 636 | {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, 637 | ] 638 | 639 | [package.dependencies] 640 | coverage = ">=5.2.1" 641 | pytest = ">=4.6" 642 | toml = "*" 643 | 644 | [package.extras] 645 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 646 | 647 | [[package]] 648 | name = "pytest-mock" 649 | version = "1.13.0" 650 | description = "Thin-wrapper around the mock package for easier use with py.test" 651 | category = "dev" 652 | optional = false 653 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 654 | files = [ 655 | {file = "pytest-mock-1.13.0.tar.gz", hash = "sha256:e24a911ec96773022ebcc7030059b57cd3480b56d4f5d19b7c370ec635e6aed5"}, 656 | {file = "pytest_mock-1.13.0-py2.py3-none-any.whl", hash = "sha256:67e414b3caef7bff6fc6bd83b22b5bc39147e4493f483c2679bc9d4dc485a94d"}, 657 | ] 658 | 659 | [package.dependencies] 660 | pytest = ">=2.7" 661 | 662 | [package.extras] 663 | dev = ["pre-commit", "tox"] 664 | 665 | [[package]] 666 | name = "python-multipart" 667 | version = "0.0.5" 668 | description = "A streaming multipart parser for Python" 669 | category = "main" 670 | optional = false 671 | python-versions = "*" 672 | files = [ 673 | {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, 674 | ] 675 | 676 | [package.dependencies] 677 | six = ">=1.4.0" 678 | 679 | [[package]] 680 | name = "regex" 681 | version = "2022.10.31" 682 | description = "Alternative regular expression module, to replace re." 683 | category = "dev" 684 | optional = false 685 | python-versions = ">=3.6" 686 | files = [ 687 | {file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"}, 688 | {file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"}, 689 | {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"}, 690 | {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"}, 691 | {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"}, 692 | {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"}, 693 | {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"}, 694 | {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"}, 695 | {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"}, 696 | {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"}, 697 | {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"}, 698 | {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"}, 699 | {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"}, 700 | {file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"}, 701 | {file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"}, 702 | {file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"}, 703 | {file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"}, 704 | {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"}, 705 | {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"}, 706 | {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"}, 707 | {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"}, 708 | {file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"}, 709 | {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"}, 710 | {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"}, 711 | {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"}, 712 | {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"}, 713 | {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"}, 714 | {file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"}, 715 | {file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"}, 716 | {file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"}, 717 | {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"}, 718 | {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"}, 719 | {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"}, 720 | {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"}, 721 | {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"}, 722 | {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"}, 723 | {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"}, 724 | {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"}, 725 | {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"}, 726 | {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"}, 727 | {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"}, 728 | {file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"}, 729 | {file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"}, 730 | {file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"}, 731 | {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"}, 732 | {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"}, 733 | {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"}, 734 | {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"}, 735 | {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"}, 736 | {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"}, 737 | {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"}, 738 | {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"}, 739 | {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"}, 740 | {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"}, 741 | {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"}, 742 | {file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"}, 743 | {file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"}, 744 | {file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"}, 745 | {file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"}, 746 | {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"}, 747 | {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"}, 748 | {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"}, 749 | {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"}, 750 | {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"}, 751 | {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"}, 752 | {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"}, 753 | {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"}, 754 | {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"}, 755 | {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"}, 756 | {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"}, 757 | {file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"}, 758 | {file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"}, 759 | {file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"}, 760 | {file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"}, 761 | {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"}, 762 | {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"}, 763 | {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"}, 764 | {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"}, 765 | {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"}, 766 | {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"}, 767 | {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"}, 768 | {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"}, 769 | {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"}, 770 | {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"}, 771 | {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"}, 772 | {file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"}, 773 | {file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"}, 774 | {file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"}, 775 | ] 776 | 777 | [[package]] 778 | name = "requests" 779 | version = "2.28.2" 780 | description = "Python HTTP for Humans." 781 | category = "main" 782 | optional = false 783 | python-versions = ">=3.7, <4" 784 | files = [ 785 | {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, 786 | {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, 787 | ] 788 | 789 | [package.dependencies] 790 | certifi = ">=2017.4.17" 791 | charset-normalizer = ">=2,<4" 792 | idna = ">=2.5,<4" 793 | urllib3 = ">=1.21.1,<1.27" 794 | 795 | [package.extras] 796 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 797 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 798 | 799 | [[package]] 800 | name = "rfc3986" 801 | version = "1.5.0" 802 | description = "Validating URI References per RFC 3986" 803 | category = "dev" 804 | optional = false 805 | python-versions = "*" 806 | files = [ 807 | {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, 808 | {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, 809 | ] 810 | 811 | [package.dependencies] 812 | idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} 813 | 814 | [package.extras] 815 | idna2008 = ["idna"] 816 | 817 | [[package]] 818 | name = "six" 819 | version = "1.16.0" 820 | description = "Python 2 and 3 compatibility utilities" 821 | category = "main" 822 | optional = false 823 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 824 | files = [ 825 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 826 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 827 | ] 828 | 829 | [[package]] 830 | name = "sniffio" 831 | version = "1.3.0" 832 | description = "Sniff out which async library your code is running under" 833 | category = "main" 834 | optional = false 835 | python-versions = ">=3.7" 836 | files = [ 837 | {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, 838 | {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, 839 | ] 840 | 841 | [[package]] 842 | name = "starlette" 843 | version = "0.25.0" 844 | description = "The little ASGI library that shines." 845 | category = "main" 846 | optional = false 847 | python-versions = ">=3.7" 848 | files = [ 849 | {file = "starlette-0.25.0-py3-none-any.whl", hash = "sha256:774f1df1983fd594b9b6fb3ded39c2aa1979d10ac45caac0f4255cbe2acb8628"}, 850 | {file = "starlette-0.25.0.tar.gz", hash = "sha256:854c71e73736c429c2bdb07801f2c76c9cba497e7c3cf4988fde5e95fe4cdb3c"}, 851 | ] 852 | 853 | [package.dependencies] 854 | anyio = ">=3.4.0,<5" 855 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 856 | 857 | [package.extras] 858 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] 859 | 860 | [[package]] 861 | name = "toml" 862 | version = "0.10.2" 863 | description = "Python Library for Tom's Obvious, Minimal Language" 864 | category = "dev" 865 | optional = false 866 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 867 | files = [ 868 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 869 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 870 | ] 871 | 872 | [[package]] 873 | name = "tomli" 874 | version = "2.0.1" 875 | description = "A lil' TOML parser" 876 | category = "dev" 877 | optional = false 878 | python-versions = ">=3.7" 879 | files = [ 880 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 881 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 882 | ] 883 | 884 | [[package]] 885 | name = "tox" 886 | version = "3.28.0" 887 | description = "tox is a generic virtualenv management and test command line tool" 888 | category = "dev" 889 | optional = false 890 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 891 | files = [ 892 | {file = "tox-3.28.0-py2.py3-none-any.whl", hash = "sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea"}, 893 | {file = "tox-3.28.0.tar.gz", hash = "sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640"}, 894 | ] 895 | 896 | [package.dependencies] 897 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 898 | filelock = ">=3.0.0" 899 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 900 | packaging = ">=14" 901 | pluggy = ">=0.12.0" 902 | py = ">=1.4.17" 903 | six = ">=1.14.0" 904 | tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} 905 | virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" 906 | 907 | [package.extras] 908 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 909 | testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] 910 | 911 | [[package]] 912 | name = "typed-ast" 913 | version = "1.5.4" 914 | description = "a fork of Python 2 and 3 ast modules with type comment support" 915 | category = "dev" 916 | optional = false 917 | python-versions = ">=3.6" 918 | files = [ 919 | {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, 920 | {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, 921 | {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, 922 | {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, 923 | {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, 924 | {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, 925 | {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, 926 | {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, 927 | {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, 928 | {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, 929 | {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, 930 | {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, 931 | {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, 932 | {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, 933 | {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, 934 | {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, 935 | {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, 936 | {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, 937 | {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, 938 | {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, 939 | {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, 940 | {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, 941 | {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, 942 | {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, 943 | ] 944 | 945 | [[package]] 946 | name = "typing-extensions" 947 | version = "4.5.0" 948 | description = "Backported and Experimental Type Hints for Python 3.7+" 949 | category = "main" 950 | optional = false 951 | python-versions = ">=3.7" 952 | files = [ 953 | {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, 954 | {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, 955 | ] 956 | 957 | [[package]] 958 | name = "urllib3" 959 | version = "1.26.14" 960 | description = "HTTP library with thread-safe connection pooling, file post, and more." 961 | category = "main" 962 | optional = false 963 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 964 | files = [ 965 | {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, 966 | {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, 967 | ] 968 | 969 | [package.extras] 970 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 971 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 972 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 973 | 974 | [[package]] 975 | name = "virtualenv" 976 | version = "20.20.0" 977 | description = "Virtual Python Environment builder" 978 | category = "dev" 979 | optional = false 980 | python-versions = ">=3.7" 981 | files = [ 982 | {file = "virtualenv-20.20.0-py3-none-any.whl", hash = "sha256:3c22fa5a7c7aa106ced59934d2c20a2ecb7f49b4130b8bf444178a16b880fa45"}, 983 | {file = "virtualenv-20.20.0.tar.gz", hash = "sha256:a8a4b8ca1e28f864b7514a253f98c1d62b64e31e77325ba279248c65fb4fcef4"}, 984 | ] 985 | 986 | [package.dependencies] 987 | distlib = ">=0.3.6,<1" 988 | filelock = ">=3.4.1,<4" 989 | importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} 990 | platformdirs = ">=2.4,<4" 991 | 992 | [package.extras] 993 | docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] 994 | test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] 995 | 996 | [[package]] 997 | name = "zipp" 998 | version = "3.15.0" 999 | description = "Backport of pathlib-compatible object wrapper for zip files" 1000 | category = "dev" 1001 | optional = false 1002 | python-versions = ">=3.7" 1003 | files = [ 1004 | {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, 1005 | {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, 1006 | ] 1007 | 1008 | [package.extras] 1009 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 1010 | testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 1011 | 1012 | [extras] 1013 | uvicorn = [] 1014 | 1015 | [metadata] 1016 | lock-version = "2.0" 1017 | python-versions = "^3.7" 1018 | content-hash = "9a93f33979bd1b35dd308640a8bf8efdb4ad3345890da9c6725f2accdc72adeb" 1019 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "slackers" 3 | version = "1.0.3" 4 | readme = "README.md" 5 | homepage = "https://github.com/uhavin/slackers" 6 | repository = "https://github.com/uhavin/slackers" 7 | description = "Slack webhooks API served by FastAPI" 8 | authors = ["Niels van Huijstee "] 9 | license = "MIT" 10 | classifiers = [ 11 | "Development Status :: 5 - Production/Stable", 12 | "Environment :: Web Environment", 13 | "Intended Audience :: Developers", 14 | "License :: OSI Approved :: MIT License", 15 | "Programming Language :: Python :: 3", 16 | "Programming Language :: Python :: 3.7", 17 | "Programming Language :: Python :: 3.8", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Topic :: Communications :: Chat", 22 | "Topic :: Office/Business", 23 | ] 24 | 25 | [tool.poetry.dependencies] 26 | python = "^3.7" 27 | pyee = "^6.0" 28 | python-multipart = "^0.0.5" 29 | requests = "^2.22" 30 | fastapi = "^0" 31 | 32 | [tool.poetry.extras] 33 | uvicorn=["uvicorn"] 34 | 35 | [tool.poetry.group.dev.dependencies] 36 | black = "^20.8b1" 37 | 38 | 39 | [tool.poetry.group.test.dependencies] 40 | httpx = "^0.23.3" 41 | pytest = "^6.2.5" 42 | pytest-cov = "^2.8" 43 | pytest-mock = "^1.11" 44 | tox = "^3.24.5" 45 | 46 | [build-system] 47 | requires = ["poetry>=0.12"] 48 | build-backend = "poetry.masonry.api" 49 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = 3 | --verbosity=-1 4 | --cov . 5 | --cov-branch 6 | --cov-report term-missing 7 | --cov-branch 8 | python_files= 9 | tests/*.py 10 | python_functions= 11 | test_* 12 | *_should_* 13 | -------------------------------------------------------------------------------- /src/slackers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhavin/slackers/2d866d3e2f73fd36d02b737111262590c207d3f0/src/slackers/__init__.py -------------------------------------------------------------------------------- /src/slackers/hooks.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from pyee import AsyncIOEventEmitter 5 | from fastapi.encoders import jsonable_encoder 6 | 7 | from .registry import R 8 | 9 | 10 | def responder(event): 11 | def wrapper(original): 12 | R.add(event, original) 13 | return original 14 | 15 | return wrapper 16 | 17 | 18 | class NamedEventEmitter(AsyncIOEventEmitter): 19 | def __init__(self, name, *args, **kwargs): 20 | self.name = name 21 | 22 | AsyncIOEventEmitter.__init__(self, *args, **kwargs) 23 | 24 | 25 | events = NamedEventEmitter(name="events") 26 | actions = NamedEventEmitter(name="actions") 27 | commands = NamedEventEmitter(name="commands") 28 | 29 | 30 | def emit(emitter: NamedEventEmitter, event, payload): 31 | async def _emit_async(): 32 | emitter.emit(event, jsonable_payload) 33 | 34 | jsonable_payload = jsonable_encoder(payload) 35 | log = logging.getLogger(__name__) 36 | log.info(f"Emitting '{event}' using emitter '{emitter.name}'") 37 | log.debug(jsonable_payload) 38 | 39 | loop = asyncio.get_event_loop() 40 | loop.create_task(_emit_async()) 41 | -------------------------------------------------------------------------------- /src/slackers/models.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class SlackBase(BaseModel): 7 | token: str 8 | 9 | 10 | class SlackChallenge(SlackBase): 11 | challenge: str 12 | type: str 13 | 14 | 15 | class SlackEnvelope(SlackBase): 16 | team_id: str 17 | api_app_id: str 18 | event: dict 19 | type: str 20 | authed_users: Optional[List[str]] 21 | event_id: str 22 | event_time: int 23 | 24 | 25 | class SlackAction(SlackBase): 26 | class Config: 27 | extra = "allow" 28 | 29 | type: str 30 | 31 | actions: list = None 32 | api_app_id: str = None 33 | callback_id: str = None 34 | channel: dict = None 35 | container: dict = None 36 | hash: str = None 37 | is_cleared: bool = None 38 | message: dict = None 39 | response_url: str = None 40 | team: dict = None 41 | trigger_id: str = None 42 | user: dict = None 43 | view: dict = None 44 | 45 | 46 | class SlackCommand(SlackBase): 47 | command: str 48 | response_url: str 49 | trigger_id: str 50 | user_id: str 51 | user_name: str 52 | team_id: str 53 | channel_id: str 54 | text: str 55 | -------------------------------------------------------------------------------- /src/slackers/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhavin/slackers/2d866d3e2f73fd36d02b737111262590c207d3f0/src/slackers/py.typed -------------------------------------------------------------------------------- /src/slackers/registry.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from starlette.responses import Response 4 | 5 | 6 | class R: 7 | callbacks = dict() 8 | 9 | @classmethod 10 | def add(cls, event, handler: typing.Callable[[dict], Response]): 11 | cls.callbacks[event] = handler 12 | 13 | @classmethod 14 | def handle(cls, event: str, payload: dict): 15 | return cls.callbacks[event](payload) 16 | -------------------------------------------------------------------------------- /src/slackers/server.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import typing 4 | 5 | from fastapi import APIRouter, Depends 6 | from starlette.requests import Request 7 | from starlette.responses import Response 8 | from starlette.status import HTTP_200_OK 9 | 10 | from slackers.hooks import actions, commands, emit, events 11 | from slackers.models import SlackAction, SlackChallenge, SlackCommand, SlackEnvelope 12 | from slackers.registry import R 13 | from slackers.verification import check_timeout, verify_signature 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | router = APIRouter() 18 | 19 | 20 | @router.post( 21 | "/events", 22 | status_code=HTTP_200_OK, 23 | dependencies=[Depends(verify_signature), Depends(check_timeout)], 24 | ) 25 | async def post_events(message: typing.Union[SlackEnvelope, SlackChallenge]): 26 | if isinstance(message, SlackChallenge): 27 | return message.challenge 28 | 29 | emit(events, message.event["type"], payload=message) 30 | return Response() 31 | 32 | 33 | @router.post( 34 | "/actions", 35 | status_code=HTTP_200_OK, 36 | dependencies=[Depends(verify_signature), Depends(check_timeout)], 37 | ) 38 | async def post_actions(request: Request) -> Response: 39 | form = await request.form() 40 | form_data = json.loads(form["payload"]) 41 | 42 | # have the convenience of pydantic validation 43 | action = SlackAction(**form_data) 44 | _events = [action.type] 45 | if action.actions: 46 | _events.extend(_add_action_triggers(action)) 47 | if action.callback_id: 48 | _events.append(f"{action.type}:{action.callback_id}") 49 | if action.view: 50 | view_callback_id = action.view.get("callback_id") 51 | if view_callback_id: 52 | _events.append(f"{action.type}:{view_callback_id}") 53 | 54 | for _event in _events: 55 | emit(actions, _event, payload=action) 56 | 57 | registered_handlers = set(R.callbacks.keys()) 58 | registered_events = set(_events) 59 | handlers_to_call = registered_handlers.intersection(registered_events) 60 | if len(handlers_to_call) > 1: 61 | raise ValueError(f"Multiple response handlers found.") 62 | 63 | if handlers_to_call: 64 | handle = handlers_to_call.pop() 65 | response = R.handle(handle, action.dict()) 66 | assert isinstance( 67 | response, Response 68 | ), "Please return a starlette.responses.Response" 69 | return response 70 | return Response() 71 | 72 | 73 | def _add_action_triggers(action: SlackAction) -> list: 74 | gathered_events = [ 75 | f"{action.type}:{triggered_action['action_id']}" 76 | for triggered_action in action.actions 77 | if "action_id" in triggered_action 78 | ] 79 | gathered_events.extend( 80 | [ 81 | f"{action.type}:{triggered_action['name']}" 82 | for triggered_action in action.actions 83 | if "name" in triggered_action 84 | ] 85 | ) 86 | gathered_events.extend( 87 | [ 88 | f"{action.type}:{triggered_action['type']}" 89 | for triggered_action in action.actions 90 | if "type" in triggered_action 91 | ] 92 | ) 93 | gathered_events.extend( 94 | [ 95 | f"{action.type}:{triggered_action['name']}:{triggered_action['type']}" 96 | for triggered_action in action.actions 97 | if "name" in triggered_action and "type" in triggered_action 98 | ] 99 | ) 100 | 101 | return gathered_events 102 | 103 | 104 | @router.post( 105 | "/commands", 106 | status_code=HTTP_200_OK, 107 | dependencies=[Depends(verify_signature), Depends(check_timeout)], 108 | ) 109 | async def post_commands(request: Request): 110 | form = await request.form() 111 | command = SlackCommand(**form) 112 | emit(commands, command.command.lstrip("/"), command) 113 | 114 | return Response() 115 | -------------------------------------------------------------------------------- /src/slackers/verification.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import math 3 | import os 4 | import time 5 | import logging 6 | 7 | from hashlib import sha256 8 | 9 | from fastapi import Header 10 | from starlette.status import HTTP_403_FORBIDDEN 11 | from starlette.requests import Request 12 | from starlette.exceptions import HTTPException 13 | 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | 18 | async def verify_signature( 19 | request: Request, 20 | x_slack_signature: str = Header(...), 21 | x_slack_request_timestamp: str = Header(...), 22 | ): 23 | 24 | log.debug("Starting verification") 25 | 26 | body = await request.body() 27 | to_verify = str.encode("v0:" + str(x_slack_request_timestamp) + ":") + body 28 | our_hash = hmac.new( 29 | os.environ.get("SLACK_SIGNING_SECRET").encode(), to_verify, sha256 30 | ).hexdigest() 31 | our_signature = "v0=" + our_hash 32 | 33 | if not hmac.compare_digest(x_slack_signature, our_signature): 34 | log.info("Slack verification failed") 35 | raise HTTPException(HTTP_403_FORBIDDEN, "Forbidden") 36 | 37 | log.debug("Verification successful") 38 | 39 | 40 | def check_timeout(x_slack_request_timestamp: str = Header(...)): 41 | timeout = 60 * 5 # 5 minutes 42 | request_timeout_time = int(x_slack_request_timestamp) + timeout 43 | current_time = math.ceil(time.time()) 44 | 45 | if current_time > request_timeout_time: 46 | log.info("Slack request timestamp reached timeout") 47 | raise HTTPException(HTTP_403_FORBIDDEN, "Forbidden") 48 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhavin/slackers/2d866d3e2f73fd36d02b737111262590c207d3f0/tests/__init__.py -------------------------------------------------------------------------------- /tests/actions.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | from starlette.status import HTTP_200_OK 5 | from starlette.testclient import TestClient 6 | 7 | from slackers.hooks import actions 8 | from slackers.models import SlackAction 9 | from slackers.registry import R 10 | 11 | 12 | @pytest.fixture(autouse=True) 13 | def reset_registry(): 14 | R.callbacks = {} 15 | 16 | 17 | @pytest.fixture 18 | def action_defaults(): 19 | action_defaults = SlackAction(type="...", token="...") 20 | return action_defaults.dict() 21 | 22 | 23 | @pytest.fixture 24 | def message_action(action_defaults): 25 | action_defaults.update( 26 | { 27 | "token": "TOKEN", 28 | "callback_id": "CALLBACK_ID", 29 | "trigger_id": "TRIGGER_ID", 30 | "response_url": "https://example.com/response", 31 | "type": "message_action", 32 | "user": {"id": "USER_ID", "name": "USER_NAME"}, 33 | "message": {}, 34 | "channel": {"id": "CHANNEL_ID", "name": "CHANNEL_NAME"}, 35 | "team": {"id": "TEAM_ID", "domain": "TEAM_DOMAIN"}, 36 | "actions": [], 37 | "view": {}, 38 | } 39 | ) 40 | return action_defaults 41 | 42 | 43 | @pytest.fixture 44 | def interactive_message(message_action): 45 | message_action.update( 46 | { 47 | "type": "interactive_message", 48 | "actions": [ 49 | {"name": "ACTION_1_NAME", "type": "ACTION_1_TYPE"}, 50 | {"name": "ACTION_2_NAME", "type": "ACTION_2_TYPE"}, 51 | ], 52 | } 53 | ) 54 | return message_action 55 | 56 | 57 | @pytest.fixture 58 | def block_actions(action_defaults): 59 | action_defaults.update( 60 | { 61 | "token": "TOKEN", 62 | "trigger_id": "TRIGGER_ID", 63 | "response_url": "https://example.com/response", 64 | "type": "block_actions", 65 | "user": {"id": "USER_ID", "name": "USER_NAME"}, 66 | "message": {}, 67 | "channel": {"id": "CHANNEL_ID", "name": "CHANNEL_NAME"}, 68 | "team": {"id": "TEAM_ID", "domain": "TEAM_DOMAIN"}, 69 | "actions": [{"action_id": "ACTION_ID_1"}, {"action_id": "ACTION_ID_2"}], 70 | "view": {}, 71 | } 72 | ) 73 | return action_defaults 74 | 75 | 76 | @pytest.fixture 77 | def view_submission(action_defaults): 78 | action_defaults.update( 79 | { 80 | "type": "view_submission", 81 | "team": {}, 82 | "user": {}, 83 | "view": { 84 | "id": "VIEW_ID", 85 | "type": "modal", 86 | "title": {}, 87 | "submit": {}, 88 | "blocks": [], 89 | "private_metadata": "private!", 90 | "callback_id": "VIEW_CALLBACK_ID", 91 | "state": { 92 | "values": { 93 | "multi-line": { 94 | "ml-value": { 95 | "type": "plain_text_input", 96 | "value": "This is my example inputted value", 97 | } 98 | } 99 | } 100 | }, 101 | "hash": "156663117.cd33ad1f", 102 | }, 103 | } 104 | ) 105 | return action_defaults 106 | 107 | 108 | @pytest.fixture 109 | def view_closed(action_defaults): 110 | action_defaults.update( 111 | { 112 | "type": "view_closed", 113 | "team": {"id": "TXXXXXX", "domain": "coverbands"}, 114 | "user": {"id": "UXXXXXX", "name": "dreamweaver"}, 115 | "view": {"callback_id": "VIEW_CLOSED_CALLBACK_ID"}, 116 | "api_app_id": "AXXXXXX", 117 | "is_cleared": False, 118 | } 119 | ) 120 | return action_defaults 121 | 122 | 123 | @pytest.mark.usefixtures("pass_header_verification") 124 | def post_message_actions_should_emit_actions_event_with_payload( 125 | mocker, client: TestClient, test_headers, message_action 126 | ): 127 | action_payload = json.dumps(message_action) 128 | base_event_callee = mocker.Mock() 129 | 130 | @actions.on("message_action") 131 | def on_message_action(payload): 132 | base_event_callee(payload=payload) 133 | 134 | response = client.post( 135 | url="/actions", data={"payload": action_payload}, headers=test_headers 136 | ) 137 | 138 | assert HTTP_200_OK == response.status_code 139 | base_event_callee.assert_called_once_with(payload=message_action) 140 | 141 | 142 | @pytest.mark.usefixtures("pass_header_verification") 143 | def post_message_actions_should_emit_callback_id_event_with_payload( 144 | mocker, client: TestClient, test_headers, message_action 145 | ): 146 | specific_event_callee = mocker.Mock() 147 | action_payload = json.dumps(message_action) 148 | 149 | @actions.on("message_action:CALLBACK_ID") 150 | def on_message_action_callback_id(payload): 151 | specific_event_callee(payload=payload) 152 | 153 | response = client.post( 154 | url="/actions", data={"payload": action_payload}, headers=test_headers 155 | ) 156 | 157 | assert HTTP_200_OK == response.status_code 158 | specific_event_callee.assert_called_once_with(payload=message_action) 159 | 160 | 161 | @pytest.mark.usefixtures("pass_header_verification") 162 | def post_block_actions_should_emit_actions_event_with_payload( 163 | mocker, client: TestClient, test_headers, block_actions 164 | ): 165 | action_payload = json.dumps(block_actions) 166 | base_event_callee = mocker.Mock() 167 | 168 | @actions.on("block_actions") 169 | def on_foo(payload): 170 | base_event_callee(payload=payload) 171 | 172 | response = client.post( 173 | url="/actions", data={"payload": action_payload}, headers=test_headers 174 | ) 175 | 176 | assert HTTP_200_OK == response.status_code 177 | base_event_callee.assert_called_once_with(payload=block_actions) 178 | 179 | 180 | @pytest.mark.usefixtures("pass_header_verification") 181 | def post_block_actions_should_emit_action_event_with_payload( 182 | mocker, client: TestClient, test_headers, block_actions 183 | ): 184 | action_payload = json.dumps(block_actions) 185 | specific_event_callee_1 = mocker.Mock() 186 | specific_event_callee_2 = mocker.Mock() 187 | 188 | @actions.on("block_actions:ACTION_ID_1") 189 | def on_block_actions_action_id_1(payload): 190 | specific_event_callee_1(payload=payload) 191 | 192 | @actions.on("block_actions:ACTION_ID_2") 193 | def on_block_actions_action_id_2(payload): 194 | specific_event_callee_2(payload=payload) 195 | 196 | response = client.post( 197 | url="/actions", data={"payload": action_payload}, headers=test_headers 198 | ) 199 | 200 | assert HTTP_200_OK == response.status_code 201 | specific_event_callee_1.assert_called_once_with(payload=block_actions) 202 | specific_event_callee_2.assert_called_once_with(payload=block_actions) 203 | 204 | 205 | @pytest.mark.usefixtures("pass_header_verification") 206 | def post_view_submission_should_emit_submission_event_with_payload( 207 | mocker, client: TestClient, test_headers, view_submission 208 | ): 209 | # test that callback_id is not required 210 | view_submission["view"].pop("callback_id") 211 | action_payload = json.dumps(view_submission) 212 | base_event_callee = mocker.Mock() 213 | 214 | @actions.on("view_submission") 215 | def on_view_submission_callback_id(payload): 216 | base_event_callee(payload=payload) 217 | 218 | response = client.post( 219 | url="/actions", data={"payload": action_payload}, headers=test_headers 220 | ) 221 | 222 | assert HTTP_200_OK == response.status_code 223 | base_event_callee.assert_called_once_with(payload=view_submission) 224 | 225 | 226 | @pytest.mark.usefixtures("pass_header_verification") 227 | def post_view_submission_should_emit_selected_action_event_with_payload( 228 | mocker, client: TestClient, test_headers, view_submission 229 | ): 230 | action_payload = json.dumps(view_submission) 231 | specific_event_callee = mocker.Mock() 232 | 233 | @actions.on("view_submission:VIEW_CALLBACK_ID") 234 | def on_view_submission_callback_id(payload): 235 | specific_event_callee(payload=payload) 236 | 237 | response = client.post( 238 | url="/actions", data={"payload": action_payload}, headers=test_headers 239 | ) 240 | 241 | assert HTTP_200_OK == response.status_code 242 | specific_event_callee.assert_called_once_with(payload=view_submission) 243 | 244 | 245 | @pytest.mark.usefixtures("pass_header_verification") 246 | def post_block_actions_should_return_a_custom_response( 247 | client: TestClient, test_headers, block_actions 248 | ): 249 | action_payload = json.dumps(block_actions) 250 | from slackers.hooks import responder 251 | 252 | @responder("block_actions:ACTION_ID_1") 253 | def custom_response(actual_payload): 254 | from starlette.responses import JSONResponse 255 | 256 | assert actual_payload == block_actions 257 | return JSONResponse(content={"custom": "Custom Response"}) 258 | 259 | response = client.post( 260 | url="/actions", data={"payload": action_payload}, headers=test_headers 261 | ) 262 | 263 | assert HTTP_200_OK == response.status_code 264 | assert {"custom": "Custom Response"} == response.json() 265 | 266 | 267 | @pytest.mark.usefixtures("pass_header_verification") 268 | def post_interactive_message_should_emit_interactive_message_event_with_payload( 269 | mocker, client: TestClient, test_headers, interactive_message 270 | ): 271 | interactive_message_payload = json.dumps(interactive_message) 272 | base_event_callee = mocker.Mock() 273 | 274 | @actions.on("interactive_message") 275 | def on_foo(payload): 276 | base_event_callee(payload=payload) 277 | 278 | response = client.post( 279 | url="/actions", 280 | data={"payload": interactive_message_payload}, 281 | headers=test_headers, 282 | ) 283 | 284 | assert HTTP_200_OK == response.status_code 285 | base_event_callee.assert_called_once_with(payload=interactive_message) 286 | 287 | 288 | @pytest.mark.usefixtures("pass_header_verification") 289 | def post_interactive_message_should_emit_interactive_message_event_names_with_payload( 290 | mocker, client: TestClient, test_headers, interactive_message 291 | ): 292 | interactive_message_payload = json.dumps(interactive_message) 293 | base_event_callee_1 = mocker.Mock() 294 | base_event_callee_2 = mocker.Mock() 295 | 296 | @actions.on("interactive_message:ACTION_1_NAME") 297 | def on_foo(payload): 298 | base_event_callee_1(payload=payload) 299 | 300 | @actions.on("interactive_message:ACTION_2_NAME") 301 | def on_foo(payload): 302 | base_event_callee_2(payload=payload) 303 | 304 | response = client.post( 305 | url="/actions", 306 | data={"payload": interactive_message_payload}, 307 | headers=test_headers, 308 | ) 309 | 310 | assert HTTP_200_OK == response.status_code 311 | base_event_callee_1.assert_called_once_with(payload=interactive_message) 312 | base_event_callee_2.assert_called_once_with(payload=interactive_message) 313 | 314 | 315 | @pytest.mark.usefixtures("pass_header_verification") 316 | def post_interactive_message_should_emit_interactive_message_event_types_with_payload( 317 | mocker, client: TestClient, test_headers, interactive_message 318 | ): 319 | interactive_message_payload = json.dumps(interactive_message) 320 | base_event_callee_1 = mocker.Mock() 321 | base_event_callee_2 = mocker.Mock() 322 | 323 | @actions.on("interactive_message:ACTION_1_TYPE") 324 | def on_foo(payload): 325 | base_event_callee_1(payload=payload) 326 | 327 | @actions.on("interactive_message:ACTION_2_TYPE") 328 | def on_foo(payload): 329 | base_event_callee_2(payload=payload) 330 | 331 | response = client.post( 332 | url="/actions", 333 | data={"payload": interactive_message_payload}, 334 | headers=test_headers, 335 | ) 336 | 337 | assert HTTP_200_OK == response.status_code 338 | base_event_callee_1.assert_called_once_with(payload=interactive_message) 339 | base_event_callee_2.assert_called_once_with(payload=interactive_message) 340 | 341 | 342 | @pytest.mark.usefixtures("pass_header_verification") 343 | def post_interactive_message_should_emit_interactive_message_event_name_type_combo_with_payload( 344 | mocker, client: TestClient, test_headers, interactive_message 345 | ): 346 | interactive_message_payload = json.dumps(interactive_message) 347 | base_event_callee_1 = mocker.Mock() 348 | base_event_callee_2 = mocker.Mock() 349 | 350 | @actions.on("interactive_message:ACTION_1_NAME:ACTION_1_TYPE") 351 | def on_foo(payload): 352 | base_event_callee_1(payload=payload) 353 | 354 | @actions.on("interactive_message:ACTION_2_NAME:ACTION_2_TYPE") 355 | def on_foo(payload): 356 | base_event_callee_2(payload=payload) 357 | 358 | response = client.post( 359 | url="/actions", 360 | data={"payload": interactive_message_payload}, 361 | headers=test_headers, 362 | ) 363 | 364 | assert HTTP_200_OK == response.status_code 365 | base_event_callee_1.assert_called_once_with(payload=interactive_message) 366 | base_event_callee_2.assert_called_once_with(payload=interactive_message) 367 | 368 | 369 | @pytest.mark.usefixtures("pass_header_verification") 370 | def post_interactive_message_should_be_able_to_return_custom_response( 371 | client: TestClient, test_headers, interactive_message 372 | ): 373 | 374 | from slackers.hooks import responder 375 | 376 | interactive_message_payload = json.dumps(interactive_message) 377 | 378 | @responder("interactive_message") 379 | def custom_response(actual_payload): 380 | from starlette.responses import JSONResponse 381 | 382 | assert actual_payload == interactive_message 383 | return JSONResponse(content={"custom": "Custom Response"}) 384 | 385 | response = client.post( 386 | url="/actions", 387 | data={"payload": interactive_message_payload}, 388 | headers=test_headers, 389 | ) 390 | 391 | assert HTTP_200_OK == response.status_code 392 | assert {"custom": "Custom Response"} == response.json() 393 | 394 | 395 | @pytest.mark.usefixtures("pass_header_verification") 396 | def post_view_submission_should_return_a_custom_response( 397 | client: TestClient, test_headers, view_submission 398 | ): 399 | action_payload = json.dumps(view_submission) 400 | from slackers.hooks import responder 401 | 402 | @responder("view_submission:VIEW_CALLBACK_ID") 403 | def custom_response(actual_payload): 404 | from starlette.responses import JSONResponse 405 | 406 | assert actual_payload == view_submission 407 | return JSONResponse(content={"custom": "Custom Response"}) 408 | 409 | response = client.post( 410 | url="/actions", data={"payload": action_payload}, headers=test_headers 411 | ) 412 | 413 | assert HTTP_200_OK == response.status_code 414 | assert {"custom": "Custom Response"} == response.json() 415 | 416 | 417 | @pytest.mark.usefixtures("pass_header_verification") 418 | def max_one_custom_response_should_be_possible( 419 | client: TestClient, test_headers, view_submission 420 | ): 421 | action_payload = json.dumps(view_submission) 422 | from slackers.hooks import responder 423 | 424 | @responder("view_submission") 425 | def custom_response(payload): 426 | ... # pragma: no cover, exception raised before calling function 427 | 428 | @responder("view_submission:VIEW_CALLBACK_ID") 429 | def custom_response(payload): 430 | ... # pragma: no cover, exception raised before calling function 431 | 432 | with pytest.raises(ValueError, match="Multiple response handlers found"): 433 | client.post( 434 | url="/actions", data={"payload": action_payload}, headers=test_headers 435 | ) 436 | 437 | 438 | @pytest.mark.usefixtures("pass_header_verification") 439 | def handler_should_return_starlette_response( 440 | client: TestClient, test_headers, view_submission 441 | ): 442 | action_payload = json.dumps(view_submission) 443 | from slackers.hooks import responder 444 | 445 | @responder("view_submission:VIEW_CALLBACK_ID") 446 | def custom_response(payload): 447 | from requests import Response 448 | 449 | return Response() 450 | 451 | with pytest.raises( 452 | AssertionError, match="Please return a starlette.responses.Response" 453 | ): 454 | client.post( 455 | url="/actions", data={"payload": action_payload}, headers=test_headers 456 | ) 457 | 458 | 459 | @pytest.mark.usefixtures("pass_header_verification") 460 | def post_view_closed_should_emit_closed_event( 461 | mocker, client: TestClient, test_headers, view_closed 462 | ): 463 | action_payload = json.dumps(view_closed) 464 | specific_event_callee = mocker.Mock() 465 | 466 | @actions.on("view_closed") 467 | def on_view_closed(payload): 468 | specific_event_callee(payload=payload) 469 | 470 | response = client.post( 471 | url="/actions", data={"payload": action_payload}, headers=test_headers 472 | ) 473 | 474 | assert HTTP_200_OK == response.status_code 475 | specific_event_callee.assert_called_once_with(payload=view_closed) 476 | 477 | 478 | @pytest.mark.usefixtures("pass_header_verification") 479 | def post_view_closed_should_emit_closed_event_callback_id( 480 | mocker, client: TestClient, test_headers, view_closed 481 | ): 482 | # This might be a nonexistent use case, but even so, if a callback id 483 | # is in a view_closed body, the callback_id event will be emitted as 484 | # a side effect anyway 485 | 486 | action_payload = json.dumps(view_closed) 487 | specific_event_callee = mocker.Mock() 488 | 489 | @actions.on("view_closed:VIEW_CLOSED_CALLBACK_ID") 490 | def on_view_closed_callback_id(payload): 491 | specific_event_callee(payload=payload) 492 | 493 | response = client.post( 494 | url="/actions", data={"payload": action_payload}, headers=test_headers 495 | ) 496 | 497 | assert HTTP_200_OK == response.status_code 498 | specific_event_callee.assert_called_once_with(payload=view_closed) 499 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | import pytest 5 | from fastapi import FastAPI 6 | 7 | from starlette.testclient import TestClient 8 | 9 | from slackers.server import router 10 | 11 | 12 | @pytest.fixture 13 | def client(): 14 | app = FastAPI() 15 | app.include_router(router) 16 | return TestClient(app=app) 17 | 18 | 19 | @pytest.fixture 20 | def test_headers(): 21 | recent_timestamp = str(round(time.time())) 22 | return { 23 | "X-Slack-Request-Timestamp": recent_timestamp, 24 | "X-Slack-Signature": "FAKE_SIG", 25 | } 26 | 27 | 28 | @pytest.fixture 29 | def pass_header_verification(mocker): 30 | hmac = mocker.patch("slackers.verification.hmac") 31 | hmac.compare_digest.return_value = True 32 | 33 | 34 | @pytest.fixture(autouse=True, scope="session") 35 | def test_config(): 36 | os.environ["SLACK_SIGNING_SECRET"] = "SLACK_SIGNING_SECRET" 37 | -------------------------------------------------------------------------------- /tests/events.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import pytest 3 | 4 | from starlette.status import HTTP_200_OK 5 | from starlette.testclient import TestClient 6 | 7 | from slackers.hooks import events 8 | 9 | 10 | @pytest.mark.usefixtures("pass_header_verification") 11 | def post_events_should_accept_challenge(mocker, client: TestClient, test_headers): 12 | @events.on("foo") 13 | def on_challenge(payload): 14 | inspection(payload=payload) 15 | 16 | inspection = mocker.Mock() 17 | slack_challenge = { 18 | "token": "TOKEN", 19 | "challenge": "CHALLENGE ACCEPTED", 20 | "type": "challenge", 21 | } 22 | response = client.post(url="/events", json=slack_challenge, headers=test_headers) 23 | 24 | assert 200 == response.status_code 25 | assert '"CHALLENGE ACCEPTED"' == response.text 26 | assert inspection.called is False 27 | 28 | 29 | @pytest.mark.usefixtures("pass_header_verification") 30 | def post_events_should_emit_events_event_with_payload( 31 | mocker, client: TestClient, test_headers 32 | ): 33 | slack_envelope = { 34 | "token": "TOKEN", 35 | "team_id": "TEAM_ID", 36 | "api_app_id": "API_APP_ID", 37 | "event": {"type": "foo"}, 38 | "type": "TYPE", 39 | "authed_users": [], 40 | "event_id": "EVENT_ID", 41 | "event_time": 1, 42 | } 43 | 44 | @events.on("foo") 45 | def on_foo(payload): 46 | inspection(payload=payload) 47 | 48 | inspection = mocker.Mock() 49 | 50 | response = client.post(url="/events", json=slack_envelope, headers=test_headers) 51 | 52 | assert HTTP_200_OK == response.status_code 53 | inspection.assert_called_once_with(payload=slack_envelope) 54 | 55 | 56 | @pytest.mark.usefixtures("pass_header_verification") 57 | def post_events_should_accept_envelope_without_authed_users( 58 | mocker, client: TestClient, test_headers 59 | ): 60 | slack_envelope = { 61 | "token": "TOKEN", 62 | "team_id": "TEAM_ID", 63 | "api_app_id": "API_APP_ID", 64 | "event": {"type": "foo"}, 65 | "type": "TYPE", 66 | "event_id": "EVENT_ID", 67 | "event_time": 1, 68 | } 69 | 70 | # Optional field will be filled with None 71 | expected_payload = copy.copy(slack_envelope) 72 | expected_payload["authed_users"] = None 73 | 74 | @events.on("foo") 75 | def on_foo(payload): 76 | import json 77 | 78 | print(json.dumps(payload)) 79 | inspection(payload=payload) 80 | 81 | inspection = mocker.Mock() 82 | 83 | response = client.post(url="/events", json=slack_envelope, headers=test_headers) 84 | 85 | assert HTTP_200_OK == response.status_code 86 | inspection.assert_called_once_with(payload=expected_payload) 87 | -------------------------------------------------------------------------------- /tests/slash_command.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from starlette.status import HTTP_200_OK 4 | from starlette.testclient import TestClient 5 | 6 | from slackers.hooks import commands 7 | 8 | 9 | @pytest.mark.usefixtures("pass_header_verification") 10 | def post_commands_should_emit_commands_event_with_payload( 11 | mocker, client: TestClient, test_headers 12 | ): 13 | @commands.on("foo") 14 | def on_foo(payload): 15 | inspection(payload=payload) 16 | 17 | inspection = mocker.Mock() 18 | command = { 19 | "token": "SLACK_TOKEN", 20 | "user_id": "USER_ID", 21 | "command": "/foo", 22 | "response_url": "https://example.com/response_url", 23 | "trigger_id": "TRIGGER_ID", 24 | "user_name": "USER_NAME", 25 | "team_id": "TEAM_ID", 26 | "channel_id": "CHANNEL_ID", 27 | "text": "hello from slack", 28 | } 29 | 30 | response = client.post(url="/commands", data=command, headers=test_headers) 31 | 32 | assert HTTP_200_OK == response.status_code 33 | inspection.assert_called_once_with(payload=command) 34 | -------------------------------------------------------------------------------- /tests/verification.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import itertools 4 | 5 | import pytest 6 | 7 | from starlette.status import ( 8 | HTTP_200_OK, 9 | HTTP_403_FORBIDDEN, 10 | HTTP_422_UNPROCESSABLE_ENTITY, 11 | ) 12 | from starlette.testclient import TestClient 13 | 14 | paths = ("/events", "/actions", "/commands") 15 | incomplete_headers = ( 16 | {}, 17 | {"X-Slack-Request-Timestamp": str(round(time.time()))}, 18 | {"X-Slack-Signature": "FAKE_SIG"}, 19 | ) 20 | 21 | 22 | @pytest.fixture(autouse=True) 23 | def set_slack_test_secret(): 24 | os.environ["SLACK_SIGNING_SECRET"] = "TEST_SECRET" 25 | 26 | 27 | @pytest.mark.parametrize("path", paths) 28 | def post_commands_should_verify_headers(path: str, client: TestClient): 29 | complete_but_invalid_headers = { 30 | "X-Slack-Request-Timestamp": "0", 31 | "X-Slack-Signature": "INVALID_SIGNATURE", 32 | } 33 | response = client.post(url=path, data={}, headers=complete_but_invalid_headers) 34 | assert HTTP_403_FORBIDDEN == response.status_code 35 | 36 | 37 | @pytest.mark.parametrize("path,headers", itertools.product(paths, incomplete_headers)) 38 | def post_commands_should_require_headers(path: str, headers: dict, client: TestClient): 39 | response = client.post(url=path, data={}, headers=headers) 40 | assert HTTP_422_UNPROCESSABLE_ENTITY == response.status_code 41 | 42 | 43 | def headers_should_be_verified(client: TestClient, mocker): 44 | timestamp_jan_1_2019_noon = 1546340400 45 | time = mocker.patch("slackers.verification.time") 46 | time.time.return_value = timestamp_jan_1_2019_noon + (5 * 60) - 1 47 | signature = "v0=66c758f7c180af608f5984e07c562ad8033c2ccce8b21771655fa7dd8d480ebe" 48 | valid_headers = { 49 | "X-Slack-Request-Timestamp": str(timestamp_jan_1_2019_noon), 50 | "X-Slack-Signature": signature, 51 | } 52 | challenge = { 53 | "token": "SLACK_TOKEN", 54 | "challenge": "A REAL CHALLENGE", 55 | "type": "url_verification", 56 | } 57 | response = client.post("/events", json=challenge, headers=valid_headers) 58 | assert HTTP_200_OK == response.status_code 59 | 60 | 61 | def timestamp_should_not_exceed_timeout(client: TestClient, mocker): 62 | timestamp_jan_1_2019_noon = 1546340400 63 | time = mocker.patch("slackers.verification.time") 64 | time.time.return_value = timestamp_jan_1_2019_noon + (5 * 60) + 1 65 | os.environ["SLACK_SIGNING_SECRET"] = "TEST_SECRET" 66 | signature = "v0=66c758f7c180af608f5984e07c562ad8033c2ccce8b21771655fa7dd8d480ebe" 67 | valid_headers = { 68 | "X-Slack-Request-Timestamp": str(timestamp_jan_1_2019_noon), 69 | "X-Slack-Signature": signature, 70 | } 71 | challenge = { 72 | "token": "SLACK_TOKEN", 73 | "challenge": "A REAL CHALLENGE", 74 | "type": "url_verification", 75 | } 76 | response = client.post("/events", json=challenge, headers=valid_headers) 77 | assert HTTP_403_FORBIDDEN == response.status_code 78 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37,py38,py39,py310,py311 3 | isolated_build = true 4 | 5 | [testenv] 6 | whitelist_externals = poetry 7 | commands = 8 | poetry install -v 9 | poetry run pytest 10 | --------------------------------------------------------------------------------