├── .coveragerc ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version ├── Dockerfile ├── LICENSE.txt ├── Makefile ├── README.md ├── app ├── __init__.py └── main.py ├── docker-compose.yaml ├── poetry.lock ├── pyproject.toml ├── tests ├── __init__.py ├── conftest.py ├── fixtures │ └── sentiment_response.json └── test_bitcoin_api.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | include = 3 | app/* 4 | omit= 5 | tests/* 6 | scripts/* 7 | app/core/logger.py 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # Redis data 141 | data/appendonly.aof 142 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - id: check-docstring-first 9 | - id: debug-statements 10 | - id: double-quote-string-fixer 11 | exclude: (tests/utils) 12 | - repo: https://gitlab.com/pycqa/flake8 13 | rev: 3.8.0 14 | hooks: 15 | - id: flake8 16 | exclude: (.git|__pycache__|__init__.py|core/application.py|app/main.py|app/dao/base.py|data) 17 | additional_dependencies: ['flake8-typing-imports==1.7.0'] 18 | - repo: https://github.com/pre-commit/mirrors-autopep8 19 | rev: v1.5.2 20 | hooks: 21 | - id: autopep8 22 | - repo: https://github.com/asottile/reorder_python_imports 23 | rev: v2.3.0 24 | hooks: 25 | - id: reorder-python-imports 26 | args: [--py3-plus] 27 | - repo: https://github.com/asottile/add-trailing-comma 28 | rev: v2.0.1 29 | hooks: 30 | - id: add-trailing-comma 31 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10.12 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.10 2 | 3 | WORKDIR /app 4 | 5 | # Install Poetry 6 | RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python && \ 7 | cd /usr/local/bin && \ 8 | ln -s /opt/poetry/bin/poetry && \ 9 | poetry config virtualenvs.create false 10 | 11 | # Copy poetry.lock* in case it doesn't exist in the repo 12 | COPY ./pyproject.toml ./poetry.lock* /app/ 13 | 14 | RUN bash -c "poetry install --no-root" 15 | 16 | COPY ./app /app 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2020 Redis Labs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | .PHONY: test 4 | test: 5 | docker-compose run --entrypoint=pytest test -s 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastAPI Redis Example 2 | 3 | This is an example API that demonstrates how to use Redis with FastAPI to build 4 | a fully async web service in Python. 5 | 6 | The API is called *IsBitcoinLit*. Readers outside the U.S. who are unfamiliar with 7 | the slang term "lit" might enjoy this [Merriam-Webster 8 | etymology](https://www.merriam-webster.com/words-at-play/lit-meaning-origin#:~:text=Lit%20has%20been%20used%20as,is%20%22exciting%20or%20excellent.%22). 9 | 10 | The IsBitcoinLit API tracks Bitcoin sentiment and prices over time, rolling 11 | these up into hourly averages of averages using the [RedisTimeSeries 12 | module](https://oss.redislabs.com/redistimeseries/). You can use the API to get 13 | average Bitcoin price and sentiment for each of the last three hours, with a 14 | quick indication of price and sentiment movement. 15 | 16 | 17 | ## Setup 18 | 19 | This project is designed to run as a set of Docker containers. You will need to 20 | [install Docker](https://www.docker.com/) to complete the setup tasks. 21 | 22 | First, clone this repo and build the Docker images for the project: 23 | 24 | $ git clone https://github.com/redis-developer/fastapi-redis-tutorial.git 25 | $ cd fastapi-redis-tutorial 26 | $ docker-compose build 27 | 28 | Running the API involves starting the app server and Redis. You'll do those steps 29 | next! 30 | 31 | 32 | ## Running the API 33 | 34 | The `docker-compose.yaml` file in this project configures containers for a Redis 35 | instance with the RedisTimeSeries module, the Python app for the example API, 36 | and a test runner. 37 | 38 | Use this command to run all three containers: 39 | 40 | $ docker-compose up 41 | 42 | This command starts Redis and the API server and runs the tests. 43 | 44 | 45 | ### Ingesting Price and Sentiment Data 46 | 47 | A `/refresh` endpoint exists in the app that ingests the last 24 hours of 48 | Bitcoin price and sentiment data. 49 | 50 | The app assumes a scheduler (cron job, Google Cloud Scheduler, etc.) will hit 51 | the `/refresh` endpoint on a regular basis to keep data fresh. 52 | 53 | **NOTE** : We can refresh data as often as we want without getting 54 | duplicate data. This is because RedisTimeSeries allows [configuring 55 | rules](https://oss.redislabs.com/redistimeseries/configuration/#duplicate_policy) 56 | to ignore duplicate sample and timestamp pairs. 57 | 58 | After you first start the API, use the `/refresh` API to ingest data: 59 | 60 | $ curl -X POST localhost:8080/refresh 61 | 62 | Now you can use the `/is-bitcoin-lit` endpoint to see a summary of Bitcoin price 63 | and sentiment data. Continue reading to see how to use that endpoint. 64 | 65 | **NOTE**: We've used the free [SentiCrypt](https://senticrypt.com) API to pull 66 | Bitcoin sentiment and price. We are not affiliated with SentiCrypt and this is **in no way** 67 | a recommendation to use the API for crypto price and sentiment tracking in real applications. 68 | 69 | 70 | ### Getting Summary Price and Sentiment Data from the API 71 | 72 | Use the `/is-bitcoin-lit` endpoint to get an hourly summary of Bitcoin price and 73 | sentiment data for the last three hours: 74 | 75 | $ curl localhost:8080/is-bitcoin-lit | jq 76 | 77 | ```json 78 | { 79 | "hourly_average_of_averages": [ 80 | { 81 | "price": "32928.345", 82 | "sentiment": "0.22", 83 | "time": "2021-07-08T17:00:00+00:00" 84 | }, 85 | { 86 | "price": "32834.2910891089", 87 | "sentiment": "0.224257425742574", 88 | "time": "2021-07-08T18:00:00+00:00" 89 | }, 90 | { 91 | "price": "32871.3406666667", 92 | "sentiment": "0.208666666666667", 93 | "time": "2021-07-08T19:00:00+00:00" 94 | } 95 | ], 96 | "sentiment_direction": "rising", 97 | "price_direction": "rising" 98 | } 99 | ``` 100 | 101 | As you can see, the response includes the key `hourly_average_of_averages`. This 102 | key contains hourly averages derived from the Bitcoin sentiment API's data, 103 | which is itself averaged over 30-second periods. (Thus, these are *averages of 104 | averages*.) 105 | 106 | The API also returns the *direction* that the price and sentiment are moving. 107 | The directions are: 108 | 109 | Value | Meaning 110 | ---------|---------- 111 | Rising | The price has risen over the past three hours. 112 | Falling | The price has fallen over the past three hours. 113 | Neutral | The price stayed the same for three hours (unlikely!) 114 | 115 | So, *is Bitcoin lit* in this example? Yes, it's lit: the price and sentiment are 116 | rising. See how that works? 117 | 118 | 119 | ### Running Tests 120 | 121 | You can run the app's test suite using docker-compose: 122 | 123 | $ docker-compose up test 124 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/fastapi-redis-tutorial/bd72d601031312ff69fbaec702934c3ea4beb222/app/__init__.py -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import json 3 | import logging 4 | from datetime import datetime 5 | from datetime import timedelta 6 | from datetime import timezone 7 | from typing import Dict 8 | from typing import Iterable 9 | from typing import List 10 | from typing import Tuple 11 | from typing import Union 12 | 13 | import aioredis 14 | import httpx 15 | from aioredis.exceptions import ResponseError 16 | from fastapi import BackgroundTasks 17 | from fastapi import Depends 18 | from fastapi import FastAPI 19 | from pydantic import BaseSettings 20 | 21 | DEFAULT_KEY_PREFIX = 'is-bitcoin-lit' 22 | SENTIMENT_API_URL = 'https://api.senticrypt.com/v1/bitcoin.json' 23 | TWO_MINUTES = 60 + 60 24 | HOURLY_BUCKET = '3600000' 25 | 26 | BitcoinSentiments = List[Dict[str, Union[str, float]]] 27 | 28 | 29 | def prefixed_key(f): 30 | """ 31 | A method decorator that prefixes return values. 32 | 33 | Prefixes any string that the decorated method `f` returns with the value of 34 | the `prefix` attribute on the owner object `self`. 35 | """ 36 | 37 | def prefixed_method(*args, **kwargs): 38 | self = args[0] 39 | key = f(*args, **kwargs) 40 | return f'{self.prefix}:{key}' 41 | 42 | return prefixed_method 43 | 44 | 45 | class Keys: 46 | """Methods to generate key names for Redis data structures.""" 47 | 48 | def __init__(self, prefix: str = DEFAULT_KEY_PREFIX): 49 | self.prefix = prefix 50 | 51 | @prefixed_key 52 | def timeseries_sentiment_key(self) -> str: 53 | """A time series containing 30-second snapshots of BTC sentiment.""" 54 | return f'sentiment:mean:30s' 55 | 56 | @prefixed_key 57 | def timeseries_price_key(self) -> str: 58 | """A time series containing 30-second snapshots of BTC price.""" 59 | return f'price:mean:30s' 60 | 61 | @prefixed_key 62 | def cache_key(self) -> str: 63 | return f'cache' 64 | 65 | 66 | class Config(BaseSettings): 67 | # The default URL expects the app to run using Docker and docker-compose. 68 | redis_url: str = 'redis://redis:6379' 69 | 70 | 71 | log = logging.getLogger(__name__) 72 | config = Config() 73 | app = FastAPI(title='FastAPI Redis Tutorial') 74 | redis = aioredis.from_url(config.redis_url, decode_responses=True) 75 | 76 | 77 | async def add_many_to_timeseries( 78 | key_pairs: Iterable[Tuple[str, str]], 79 | data: BitcoinSentiments 80 | ): 81 | """ 82 | Add many samples to a single timeseries key. 83 | 84 | `key_pairs` is an iteratble of tuples containing in the 0th position the 85 | timestamp key into which to insert entries and the 1th position the name 86 | of the key within th `data` dict to find the sample. 87 | """ 88 | partial = functools.partial(redis.execute_command, 'TS.MADD') 89 | for datapoint in data: 90 | for timeseries_key, sample_key in key_pairs: 91 | partial = functools.partial( 92 | partial, timeseries_key, int( 93 | float(datapoint['timestamp']) * 1000, 94 | ), 95 | datapoint[sample_key], 96 | ) 97 | return await partial() 98 | 99 | 100 | def make_keys(): 101 | return Keys() 102 | 103 | 104 | async def persist(keys: Keys, data: BitcoinSentiments): 105 | ts_sentiment_key = keys.timeseries_sentiment_key() 106 | ts_price_key = keys.timeseries_price_key() 107 | await add_many_to_timeseries( 108 | ( 109 | (ts_price_key, 'btc_price'), 110 | (ts_sentiment_key, 'mean'), 111 | ), data, 112 | ) 113 | 114 | async def get_latest_timestamp(ts_key: str): 115 | response = await redis.execute_command( 116 | 'TS.GET', ts_key 117 | ) 118 | 119 | # Returns a list of the structure [timestamp, value] 120 | return response 121 | 122 | async def get_hourly_average(ts_key: str, top_of_the_hour: int): 123 | response = await redis.execute_command( 124 | 'TS.RANGE', ts_key, top_of_the_hour, '+', 125 | 'AGGREGATION', 'avg', HOURLY_BUCKET, 126 | ) 127 | # Returns a list of the structure [timestamp, average]. 128 | return response 129 | 130 | 131 | def datetime_parser(dct): 132 | for k, v in dct.items(): 133 | if isinstance(v, str) and v.endswith('+00:00'): 134 | try: 135 | dct[k] = datetime.datetime.fromisoformat(v) 136 | except: 137 | pass 138 | return dct 139 | 140 | 141 | async def get_cache(keys: Keys): 142 | current_hour_cache_key = keys.cache_key() 143 | current_hour_stats = await redis.get(current_hour_cache_key) 144 | 145 | if current_hour_stats: 146 | return json.loads(current_hour_stats, object_hook=datetime_parser) 147 | 148 | 149 | async def set_cache(data, keys: Keys): 150 | def serialize_dates(v): 151 | return v.isoformat() if isinstance(v, datetime) else v 152 | 153 | await redis.set( 154 | keys.cache_key(), 155 | json.dumps(data, default=serialize_dates), 156 | ex=TWO_MINUTES, 157 | ) 158 | 159 | 160 | def get_direction(last_three_hours, key: str): 161 | if last_three_hours[0][key] < last_three_hours[-1][key]: 162 | return 'rising' 163 | elif last_three_hours[0][key] > last_three_hours[-1][key]: 164 | return 'falling' 165 | else: 166 | return 'flat' 167 | 168 | 169 | def now(): 170 | """Wrap call to utcnow, so that we can mock this function in tests.""" 171 | return datetime.utcnow() 172 | 173 | 174 | async def calculate_three_hours_of_data(keys: Keys) -> Dict[str, str]: 175 | sentiment_key = keys.timeseries_sentiment_key() 176 | price_key = keys.timeseries_price_key() 177 | latest_data = await get_latest_timestamp(sentiment_key) 178 | #three_hours_ago_ms = int((now() - timedelta(hours=3)).timestamp() * 1000) 179 | three_hours_ago_ms = latest_data[0] - (1000 * 60 * 60 * 2) 180 | 181 | print(three_hours_ago_ms) 182 | 183 | sentiment = await get_hourly_average(sentiment_key, three_hours_ago_ms) 184 | price = await get_hourly_average(price_key, three_hours_ago_ms) 185 | 186 | last_three_hours = [{ 187 | 'price': data[0][1], 'sentiment': data[1][1], 188 | 'time': datetime.fromtimestamp(data[0][0] / 1000, tz=timezone.utc), 189 | } 190 | for data in zip(price, sentiment)] 191 | 192 | return { 193 | 'hourly_average_of_averages': last_three_hours, 194 | 'sentiment_direction': get_direction(last_three_hours, 'sentiment'), 195 | 'price_direction': get_direction(last_three_hours, 'price'), 196 | } 197 | 198 | 199 | @app.post('/refresh') 200 | async def refresh(background_tasks: BackgroundTasks, keys: Keys = Depends(make_keys)): 201 | async with httpx.AsyncClient() as client: 202 | data = await client.get(SENTIMENT_API_URL) 203 | await persist(keys, data.json()) 204 | data = await calculate_three_hours_of_data(keys) 205 | background_tasks.add_task(set_cache, data, keys) 206 | 207 | 208 | @app.get('/is-bitcoin-lit') 209 | async def bitcoin(background_tasks: BackgroundTasks, keys: Keys = Depends(make_keys)): 210 | data = await get_cache(keys) 211 | 212 | if not data: 213 | data = await calculate_three_hours_of_data(keys) 214 | background_tasks.add_task(set_cache, data, keys) 215 | 216 | return data 217 | 218 | 219 | async def make_timeseries(key): 220 | """ 221 | Create a timeseries with the Redis key `key`. 222 | 223 | We'll use the duplicate policy known as "first," which ignores 224 | duplicate pairs of timestamp and values if we add them. 225 | 226 | Because of this, we don't worry about handling this logic 227 | ourselves -- but note that there is a performance cost to writes 228 | using this policy. 229 | """ 230 | try: 231 | await redis.execute_command( 232 | 'TS.CREATE', key, 233 | 'DUPLICATE_POLICY', 'first', 234 | ) 235 | except ResponseError as e: 236 | # Time series probably already exists 237 | log.info('Could not create timeseries %s, error: %s', key, e) 238 | 239 | 240 | async def initialize_redis(keys: Keys): 241 | await make_timeseries(keys.timeseries_sentiment_key()) 242 | await make_timeseries(keys.timeseries_price_key()) 243 | 244 | 245 | @app.on_event('startup') 246 | async def startup_event(): 247 | keys = Keys() 248 | await initialize_redis(keys) 249 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | image: redis/redis-stack 4 | ports: 5 | - "16379:6379" 6 | volumes: 7 | - $PWD/data:/data 8 | 9 | app: 10 | restart: always 11 | build: . 12 | ports: 13 | - "8080:80" 14 | volumes: 15 | - $PWD/app:/app 16 | depends_on: 17 | - redis 18 | command: /start-reload.sh 19 | 20 | test: 21 | build: . 22 | volumes: 23 | - $PWD:/app 24 | depends_on: 25 | - redis 26 | entrypoint: "pytest -s" 27 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "aioredis" 5 | version = "2.0.1" 6 | description = "asyncio (PEP 3156) Redis support" 7 | optional = false 8 | python-versions = ">=3.6" 9 | files = [ 10 | {file = "aioredis-2.0.1-py3-none-any.whl", hash = "sha256:9ac0d0b3b485d293b8ca1987e6de8658d7dafcca1cddfcd1d506cae8cdebfdd6"}, 11 | {file = "aioredis-2.0.1.tar.gz", hash = "sha256:eaa51aaf993f2d71f54b70527c440437ba65340588afeb786cd87c55c89cd98e"}, 12 | ] 13 | 14 | [package.dependencies] 15 | async-timeout = "*" 16 | typing-extensions = "*" 17 | 18 | [package.extras] 19 | hiredis = ["hiredis (>=1.0)"] 20 | 21 | [[package]] 22 | name = "anyio" 23 | version = "3.7.1" 24 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 25 | optional = false 26 | python-versions = ">=3.7" 27 | files = [ 28 | {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, 29 | {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, 30 | ] 31 | 32 | [package.dependencies] 33 | exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} 34 | idna = ">=2.8" 35 | sniffio = ">=1.1" 36 | 37 | [package.extras] 38 | doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] 39 | test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 40 | trio = ["trio (<0.22)"] 41 | 42 | [[package]] 43 | name = "asgi-lifespan" 44 | version = "1.0.1" 45 | description = "Programmatic startup/shutdown of ASGI apps." 46 | optional = false 47 | python-versions = ">=3.6" 48 | files = [ 49 | {file = "asgi-lifespan-1.0.1.tar.gz", hash = "sha256:9a33e7da2073c4764bc79bd6136501d6c42f60e3d2168ba71235e84122eadb7f"}, 50 | {file = "asgi_lifespan-1.0.1-py3-none-any.whl", hash = "sha256:9ea969dc5eb5cf08e52c08dce6f61afcadd28112e72d81c972b1d8eb8691ab53"}, 51 | ] 52 | 53 | [package.dependencies] 54 | sniffio = "*" 55 | 56 | [[package]] 57 | name = "asgiref" 58 | version = "3.8.1" 59 | description = "ASGI specs, helper code, and adapters" 60 | optional = false 61 | python-versions = ">=3.8" 62 | files = [ 63 | {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, 64 | {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, 65 | ] 66 | 67 | [package.dependencies] 68 | typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} 69 | 70 | [package.extras] 71 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 72 | 73 | [[package]] 74 | name = "asttokens" 75 | version = "2.4.1" 76 | description = "Annotate AST trees with source code positions" 77 | optional = false 78 | python-versions = "*" 79 | files = [ 80 | {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, 81 | {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, 82 | ] 83 | 84 | [package.dependencies] 85 | six = ">=1.12.0" 86 | 87 | [package.extras] 88 | astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] 89 | test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] 90 | 91 | [[package]] 92 | name = "async-timeout" 93 | version = "4.0.3" 94 | description = "Timeout context manager for asyncio programs" 95 | optional = false 96 | python-versions = ">=3.7" 97 | files = [ 98 | {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, 99 | {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, 100 | ] 101 | 102 | [[package]] 103 | name = "certifi" 104 | version = "2024.6.2" 105 | description = "Python package for providing Mozilla's CA Bundle." 106 | optional = false 107 | python-versions = ">=3.6" 108 | files = [ 109 | {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, 110 | {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, 111 | ] 112 | 113 | [[package]] 114 | name = "cfgv" 115 | version = "3.4.0" 116 | description = "Validate configuration and produce human readable error messages." 117 | optional = false 118 | python-versions = ">=3.8" 119 | files = [ 120 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 121 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 122 | ] 123 | 124 | [[package]] 125 | name = "charset-normalizer" 126 | version = "3.3.2" 127 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 128 | optional = false 129 | python-versions = ">=3.7.0" 130 | files = [ 131 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 132 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 133 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 134 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 135 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 136 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 137 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 138 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 139 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 140 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 141 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 142 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 143 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 144 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 145 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 146 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 147 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 148 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 149 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 150 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 151 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 152 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 153 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 154 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 155 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 156 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 157 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 158 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 159 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 160 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 161 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 162 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 163 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 164 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 165 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 166 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 167 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 168 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 169 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 170 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 171 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 172 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 173 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 174 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 175 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 176 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 177 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 178 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 179 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 180 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 181 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 182 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 183 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 184 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 185 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 186 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 187 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 188 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 189 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 190 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 191 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 192 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 193 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 194 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 195 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 196 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 197 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 198 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 199 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 200 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 201 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 202 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 203 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 204 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 205 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 206 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 207 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 208 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 209 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 210 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 211 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 212 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 213 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 214 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 215 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 216 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 217 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 218 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 219 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 220 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 221 | ] 222 | 223 | [[package]] 224 | name = "click" 225 | version = "8.1.7" 226 | description = "Composable command line interface toolkit" 227 | optional = false 228 | python-versions = ">=3.7" 229 | files = [ 230 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 231 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 232 | ] 233 | 234 | [package.dependencies] 235 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 236 | 237 | [[package]] 238 | name = "colorama" 239 | version = "0.4.6" 240 | description = "Cross-platform colored terminal text." 241 | optional = false 242 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 243 | files = [ 244 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 245 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 246 | ] 247 | 248 | [[package]] 249 | name = "coverage" 250 | version = "7.5.4" 251 | description = "Code coverage measurement for Python" 252 | optional = false 253 | python-versions = ">=3.8" 254 | files = [ 255 | {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"}, 256 | {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"}, 257 | {file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"}, 258 | {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"}, 259 | {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"}, 260 | {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"}, 261 | {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"}, 262 | {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"}, 263 | {file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"}, 264 | {file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"}, 265 | {file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"}, 266 | {file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"}, 267 | {file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"}, 268 | {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"}, 269 | {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"}, 270 | {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"}, 271 | {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"}, 272 | {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"}, 273 | {file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"}, 274 | {file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"}, 275 | {file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"}, 276 | {file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"}, 277 | {file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"}, 278 | {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"}, 279 | {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"}, 280 | {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"}, 281 | {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"}, 282 | {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"}, 283 | {file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"}, 284 | {file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"}, 285 | {file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"}, 286 | {file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"}, 287 | {file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"}, 288 | {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"}, 289 | {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"}, 290 | {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"}, 291 | {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"}, 292 | {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"}, 293 | {file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"}, 294 | {file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"}, 295 | {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"}, 296 | {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"}, 297 | {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"}, 298 | {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"}, 299 | {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"}, 300 | {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"}, 301 | {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"}, 302 | {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"}, 303 | {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"}, 304 | {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"}, 305 | {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"}, 306 | {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"}, 307 | ] 308 | 309 | [package.dependencies] 310 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 311 | 312 | [package.extras] 313 | toml = ["tomli"] 314 | 315 | [[package]] 316 | name = "decorator" 317 | version = "5.1.1" 318 | description = "Decorators for Humans" 319 | optional = false 320 | python-versions = ">=3.5" 321 | files = [ 322 | {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, 323 | {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, 324 | ] 325 | 326 | [[package]] 327 | name = "distlib" 328 | version = "0.3.8" 329 | description = "Distribution utilities" 330 | optional = false 331 | python-versions = "*" 332 | files = [ 333 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 334 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 335 | ] 336 | 337 | [[package]] 338 | name = "exceptiongroup" 339 | version = "1.2.1" 340 | description = "Backport of PEP 654 (exception groups)" 341 | optional = false 342 | python-versions = ">=3.7" 343 | files = [ 344 | {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, 345 | {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, 346 | ] 347 | 348 | [package.extras] 349 | test = ["pytest (>=6)"] 350 | 351 | [[package]] 352 | name = "executing" 353 | version = "2.0.1" 354 | description = "Get the currently executing AST node of a frame, and other information" 355 | optional = false 356 | python-versions = ">=3.5" 357 | files = [ 358 | {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, 359 | {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, 360 | ] 361 | 362 | [package.extras] 363 | tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] 364 | 365 | [[package]] 366 | name = "fastapi" 367 | version = "0.61.2" 368 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 369 | optional = false 370 | python-versions = ">=3.6" 371 | files = [ 372 | {file = "fastapi-0.61.2-py3-none-any.whl", hash = "sha256:8c8517680a221e69eb34073adf46c503092db2f24845b7bdc7f85b54f24ff0df"}, 373 | {file = "fastapi-0.61.2.tar.gz", hash = "sha256:9e0494fcbba98f85b8cc9b2606bb6b625246e1b12f79ca61f508b0b00843eca6"}, 374 | ] 375 | 376 | [package.dependencies] 377 | pydantic = ">=1.0.0,<2.0.0" 378 | starlette = "0.13.6" 379 | 380 | [package.extras] 381 | all = ["aiofiles (>=0.5.0,<0.6.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)", "email_validator (>=1.1.1,<2.0.0)", "graphene (>=2.1.8,<3.0.0)", "itsdangerous (>=1.1.0,<2.0.0)", "jinja2 (>=2.11.2,<3.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=3.0.0,<4.0.0)", "uvicorn (>=0.11.5,<0.12.0)"] 382 | dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "graphene (>=2.1.8,<3.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "uvicorn (>=0.11.5,<0.12.0)"] 383 | doc = ["markdown-include (>=0.5.1,<0.6.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.2.0)", "mkdocs-material (>=5.5.0,<6.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "typer (>=0.3.0,<0.4.0)", "typer-cli (>=0.0.9,<0.0.10)"] 384 | test = ["aiofiles (>=0.5.0,<0.6.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)", "black (==19.10b0)", "databases[sqlite] (>=0.3.2,<0.4.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "flask (>=1.1.2,<2.0.0)", "httpx (>=0.14.0,<0.15.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.782)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (==5.4.3)", "pytest-asyncio (>=0.14.0,<0.15.0)", "pytest-cov (==2.10.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<2.0.0)"] 385 | 386 | [[package]] 387 | name = "filelock" 388 | version = "3.15.4" 389 | description = "A platform independent file lock." 390 | optional = false 391 | python-versions = ">=3.8" 392 | files = [ 393 | {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, 394 | {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, 395 | ] 396 | 397 | [package.extras] 398 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 399 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] 400 | typing = ["typing-extensions (>=4.8)"] 401 | 402 | [[package]] 403 | name = "h11" 404 | version = "0.12.0" 405 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 406 | optional = false 407 | python-versions = ">=3.6" 408 | files = [ 409 | {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, 410 | {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, 411 | ] 412 | 413 | [[package]] 414 | name = "httpcore" 415 | version = "0.13.7" 416 | description = "A minimal low-level HTTP client." 417 | optional = false 418 | python-versions = ">=3.6" 419 | files = [ 420 | {file = "httpcore-0.13.7-py3-none-any.whl", hash = "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0"}, 421 | {file = "httpcore-0.13.7.tar.gz", hash = "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3"}, 422 | ] 423 | 424 | [package.dependencies] 425 | anyio = "==3.*" 426 | h11 = ">=0.11,<0.13" 427 | sniffio = "==1.*" 428 | 429 | [package.extras] 430 | http2 = ["h2 (>=3,<5)"] 431 | 432 | [[package]] 433 | name = "httpx" 434 | version = "0.18.2" 435 | description = "The next generation HTTP client." 436 | optional = false 437 | python-versions = ">=3.6" 438 | files = [ 439 | {file = "httpx-0.18.2-py3-none-any.whl", hash = "sha256:979afafecb7d22a1d10340bafb403cf2cb75aff214426ff206521fc79d26408c"}, 440 | {file = "httpx-0.18.2.tar.gz", hash = "sha256:9f99c15d33642d38bce8405df088c1c4cfd940284b4290cacbfb02e64f4877c6"}, 441 | ] 442 | 443 | [package.dependencies] 444 | certifi = "*" 445 | httpcore = ">=0.13.3,<0.14.0" 446 | rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} 447 | sniffio = "*" 448 | 449 | [package.extras] 450 | brotli = ["brotlicffi (==1.*)"] 451 | http2 = ["h2 (==3.*)"] 452 | 453 | [[package]] 454 | name = "identify" 455 | version = "2.5.36" 456 | description = "File identification library for Python" 457 | optional = false 458 | python-versions = ">=3.8" 459 | files = [ 460 | {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, 461 | {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, 462 | ] 463 | 464 | [package.extras] 465 | license = ["ukkonen"] 466 | 467 | [[package]] 468 | name = "idna" 469 | version = "3.7" 470 | description = "Internationalized Domain Names in Applications (IDNA)" 471 | optional = false 472 | python-versions = ">=3.5" 473 | files = [ 474 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 475 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 476 | ] 477 | 478 | [[package]] 479 | name = "iniconfig" 480 | version = "2.0.0" 481 | description = "brain-dead simple config-ini parsing" 482 | optional = false 483 | python-versions = ">=3.7" 484 | files = [ 485 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 486 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 487 | ] 488 | 489 | [[package]] 490 | name = "ipdb" 491 | version = "0.13.13" 492 | description = "IPython-enabled pdb" 493 | optional = false 494 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 495 | files = [ 496 | {file = "ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4"}, 497 | {file = "ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726"}, 498 | ] 499 | 500 | [package.dependencies] 501 | decorator = {version = "*", markers = "python_version > \"3.6\""} 502 | ipython = {version = ">=7.31.1", markers = "python_version > \"3.6\""} 503 | tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\""} 504 | 505 | [[package]] 506 | name = "ipython" 507 | version = "8.26.0" 508 | description = "IPython: Productive Interactive Computing" 509 | optional = false 510 | python-versions = ">=3.10" 511 | files = [ 512 | {file = "ipython-8.26.0-py3-none-any.whl", hash = "sha256:e6b347c27bdf9c32ee9d31ae85defc525755a1869f14057e900675b9e8d6e6ff"}, 513 | {file = "ipython-8.26.0.tar.gz", hash = "sha256:1cec0fbba8404af13facebe83d04436a7434c7400e59f47acf467c64abd0956c"}, 514 | ] 515 | 516 | [package.dependencies] 517 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 518 | decorator = "*" 519 | exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} 520 | jedi = ">=0.16" 521 | matplotlib-inline = "*" 522 | pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} 523 | prompt-toolkit = ">=3.0.41,<3.1.0" 524 | pygments = ">=2.4.0" 525 | stack-data = "*" 526 | traitlets = ">=5.13.0" 527 | typing-extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} 528 | 529 | [package.extras] 530 | all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] 531 | black = ["black"] 532 | doc = ["docrepr", "exceptiongroup", "intersphinx-registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing-extensions"] 533 | kernel = ["ipykernel"] 534 | matplotlib = ["matplotlib"] 535 | nbconvert = ["nbconvert"] 536 | nbformat = ["nbformat"] 537 | notebook = ["ipywidgets", "notebook"] 538 | parallel = ["ipyparallel"] 539 | qtconsole = ["qtconsole"] 540 | test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] 541 | test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] 542 | 543 | [[package]] 544 | name = "jedi" 545 | version = "0.19.1" 546 | description = "An autocompletion tool for Python that can be used for text editors." 547 | optional = false 548 | python-versions = ">=3.6" 549 | files = [ 550 | {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, 551 | {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, 552 | ] 553 | 554 | [package.dependencies] 555 | parso = ">=0.8.3,<0.9.0" 556 | 557 | [package.extras] 558 | docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] 559 | qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] 560 | testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] 561 | 562 | [[package]] 563 | name = "matplotlib-inline" 564 | version = "0.1.7" 565 | description = "Inline Matplotlib backend for Jupyter" 566 | optional = false 567 | python-versions = ">=3.8" 568 | files = [ 569 | {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, 570 | {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, 571 | ] 572 | 573 | [package.dependencies] 574 | traitlets = "*" 575 | 576 | [[package]] 577 | name = "nodeenv" 578 | version = "1.9.1" 579 | description = "Node.js virtual environment builder" 580 | optional = false 581 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 582 | files = [ 583 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 584 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 585 | ] 586 | 587 | [[package]] 588 | name = "packaging" 589 | version = "24.1" 590 | description = "Core utilities for Python packages" 591 | optional = false 592 | python-versions = ">=3.8" 593 | files = [ 594 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 595 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 596 | ] 597 | 598 | [[package]] 599 | name = "parso" 600 | version = "0.8.4" 601 | description = "A Python Parser" 602 | optional = false 603 | python-versions = ">=3.6" 604 | files = [ 605 | {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, 606 | {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, 607 | ] 608 | 609 | [package.extras] 610 | qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] 611 | testing = ["docopt", "pytest"] 612 | 613 | [[package]] 614 | name = "pexpect" 615 | version = "4.9.0" 616 | description = "Pexpect allows easy control of interactive console applications." 617 | optional = false 618 | python-versions = "*" 619 | files = [ 620 | {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, 621 | {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, 622 | ] 623 | 624 | [package.dependencies] 625 | ptyprocess = ">=0.5" 626 | 627 | [[package]] 628 | name = "platformdirs" 629 | version = "4.2.2" 630 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 631 | optional = false 632 | python-versions = ">=3.8" 633 | files = [ 634 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 635 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 636 | ] 637 | 638 | [package.extras] 639 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 640 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 641 | type = ["mypy (>=1.8)"] 642 | 643 | [[package]] 644 | name = "pluggy" 645 | version = "1.5.0" 646 | description = "plugin and hook calling mechanisms for python" 647 | optional = false 648 | python-versions = ">=3.8" 649 | files = [ 650 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 651 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 652 | ] 653 | 654 | [package.extras] 655 | dev = ["pre-commit", "tox"] 656 | testing = ["pytest", "pytest-benchmark"] 657 | 658 | [[package]] 659 | name = "pre-commit" 660 | version = "2.21.0" 661 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 662 | optional = false 663 | python-versions = ">=3.7" 664 | files = [ 665 | {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, 666 | {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, 667 | ] 668 | 669 | [package.dependencies] 670 | cfgv = ">=2.0.0" 671 | identify = ">=1.0.0" 672 | nodeenv = ">=0.11.1" 673 | pyyaml = ">=5.1" 674 | virtualenv = ">=20.10.0" 675 | 676 | [[package]] 677 | name = "prompt-toolkit" 678 | version = "3.0.47" 679 | description = "Library for building powerful interactive command lines in Python" 680 | optional = false 681 | python-versions = ">=3.7.0" 682 | files = [ 683 | {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, 684 | {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, 685 | ] 686 | 687 | [package.dependencies] 688 | wcwidth = "*" 689 | 690 | [[package]] 691 | name = "ptyprocess" 692 | version = "0.7.0" 693 | description = "Run a subprocess in a pseudo terminal" 694 | optional = false 695 | python-versions = "*" 696 | files = [ 697 | {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, 698 | {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, 699 | ] 700 | 701 | [[package]] 702 | name = "pure-eval" 703 | version = "0.2.2" 704 | description = "Safely evaluate AST nodes without side effects" 705 | optional = false 706 | python-versions = "*" 707 | files = [ 708 | {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, 709 | {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, 710 | ] 711 | 712 | [package.extras] 713 | tests = ["pytest"] 714 | 715 | [[package]] 716 | name = "py" 717 | version = "1.11.0" 718 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 719 | optional = false 720 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 721 | files = [ 722 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 723 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 724 | ] 725 | 726 | [[package]] 727 | name = "pydantic" 728 | version = "1.10.17" 729 | description = "Data validation and settings management using python type hints" 730 | optional = false 731 | python-versions = ">=3.7" 732 | files = [ 733 | {file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"}, 734 | {file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"}, 735 | {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"}, 736 | {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"}, 737 | {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"}, 738 | {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"}, 739 | {file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"}, 740 | {file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"}, 741 | {file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"}, 742 | {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"}, 743 | {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"}, 744 | {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"}, 745 | {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"}, 746 | {file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"}, 747 | {file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"}, 748 | {file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"}, 749 | {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"}, 750 | {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"}, 751 | {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"}, 752 | {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"}, 753 | {file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"}, 754 | {file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"}, 755 | {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"}, 756 | {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"}, 757 | {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"}, 758 | {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"}, 759 | {file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"}, 760 | {file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"}, 761 | {file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"}, 762 | {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"}, 763 | {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"}, 764 | {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"}, 765 | {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"}, 766 | {file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"}, 767 | {file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"}, 768 | {file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"}, 769 | {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"}, 770 | {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"}, 771 | {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"}, 772 | {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"}, 773 | {file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"}, 774 | {file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"}, 775 | {file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"}, 776 | ] 777 | 778 | [package.dependencies] 779 | typing-extensions = ">=4.2.0" 780 | 781 | [package.extras] 782 | dotenv = ["python-dotenv (>=0.10.4)"] 783 | email = ["email-validator (>=1.0.3)"] 784 | 785 | [[package]] 786 | name = "pygments" 787 | version = "2.18.0" 788 | description = "Pygments is a syntax highlighting package written in Python." 789 | optional = false 790 | python-versions = ">=3.8" 791 | files = [ 792 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 793 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 794 | ] 795 | 796 | [package.extras] 797 | windows-terminal = ["colorama (>=0.4.6)"] 798 | 799 | [[package]] 800 | name = "pytest" 801 | version = "8.2.2" 802 | description = "pytest: simple powerful testing with Python" 803 | optional = false 804 | python-versions = ">=3.8" 805 | files = [ 806 | {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, 807 | {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, 808 | ] 809 | 810 | [package.dependencies] 811 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 812 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 813 | iniconfig = "*" 814 | packaging = "*" 815 | pluggy = ">=1.5,<2.0" 816 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 817 | 818 | [package.extras] 819 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 820 | 821 | [[package]] 822 | name = "pytest-asyncio" 823 | version = "0.23.7" 824 | description = "Pytest support for asyncio" 825 | optional = false 826 | python-versions = ">=3.8" 827 | files = [ 828 | {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"}, 829 | {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"}, 830 | ] 831 | 832 | [package.dependencies] 833 | pytest = ">=7.0.0,<9" 834 | 835 | [package.extras] 836 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] 837 | testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] 838 | 839 | [[package]] 840 | name = "pytest-cov" 841 | version = "5.0.0" 842 | description = "Pytest plugin for measuring coverage." 843 | optional = false 844 | python-versions = ">=3.8" 845 | files = [ 846 | {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, 847 | {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, 848 | ] 849 | 850 | [package.dependencies] 851 | coverage = {version = ">=5.2.1", extras = ["toml"]} 852 | pytest = ">=4.6" 853 | 854 | [package.extras] 855 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 856 | 857 | [[package]] 858 | name = "python-dateutil" 859 | version = "2.9.0.post0" 860 | description = "Extensions to the standard Python datetime module" 861 | optional = false 862 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 863 | files = [ 864 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 865 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 866 | ] 867 | 868 | [package.dependencies] 869 | six = ">=1.5" 870 | 871 | [[package]] 872 | name = "pyyaml" 873 | version = "6.0.1" 874 | description = "YAML parser and emitter for Python" 875 | optional = false 876 | python-versions = ">=3.6" 877 | files = [ 878 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 879 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 880 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 881 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 882 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 883 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 884 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 885 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 886 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 887 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 888 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 889 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 890 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 891 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 892 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 893 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 894 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 895 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 896 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, 897 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 898 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 899 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 900 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 901 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 902 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 903 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 904 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 905 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 906 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 907 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 908 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 909 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 910 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 911 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 912 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 913 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 914 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 915 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 916 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 917 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 918 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 919 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 920 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 921 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 922 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 923 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 924 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 925 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 926 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 927 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 928 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 929 | ] 930 | 931 | [[package]] 932 | name = "requests" 933 | version = "2.32.3" 934 | description = "Python HTTP for Humans." 935 | optional = false 936 | python-versions = ">=3.8" 937 | files = [ 938 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 939 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 940 | ] 941 | 942 | [package.dependencies] 943 | certifi = ">=2017.4.17" 944 | charset-normalizer = ">=2,<4" 945 | idna = ">=2.5,<4" 946 | urllib3 = ">=1.21.1,<3" 947 | 948 | [package.extras] 949 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 950 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 951 | 952 | [[package]] 953 | name = "rfc3986" 954 | version = "1.5.0" 955 | description = "Validating URI References per RFC 3986" 956 | optional = false 957 | python-versions = "*" 958 | files = [ 959 | {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, 960 | {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, 961 | ] 962 | 963 | [package.dependencies] 964 | idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} 965 | 966 | [package.extras] 967 | idna2008 = ["idna"] 968 | 969 | [[package]] 970 | name = "six" 971 | version = "1.16.0" 972 | description = "Python 2 and 3 compatibility utilities" 973 | optional = false 974 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 975 | files = [ 976 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 977 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 978 | ] 979 | 980 | [[package]] 981 | name = "sniffio" 982 | version = "1.3.1" 983 | description = "Sniff out which async library your code is running under" 984 | optional = false 985 | python-versions = ">=3.7" 986 | files = [ 987 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 988 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 989 | ] 990 | 991 | [[package]] 992 | name = "stack-data" 993 | version = "0.6.3" 994 | description = "Extract data from python stack frames and tracebacks for informative displays" 995 | optional = false 996 | python-versions = "*" 997 | files = [ 998 | {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, 999 | {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, 1000 | ] 1001 | 1002 | [package.dependencies] 1003 | asttokens = ">=2.1.0" 1004 | executing = ">=1.2.0" 1005 | pure-eval = "*" 1006 | 1007 | [package.extras] 1008 | tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] 1009 | 1010 | [[package]] 1011 | name = "starlette" 1012 | version = "0.13.6" 1013 | description = "The little ASGI library that shines." 1014 | optional = false 1015 | python-versions = ">=3.6" 1016 | files = [ 1017 | {file = "starlette-0.13.6-py3-none-any.whl", hash = "sha256:bd2ffe5e37fb75d014728511f8e68ebf2c80b0fa3d04ca1479f4dc752ae31ac9"}, 1018 | {file = "starlette-0.13.6.tar.gz", hash = "sha256:ebe8ee08d9be96a3c9f31b2cb2a24dbdf845247b745664bd8a3f9bd0c977fdbc"}, 1019 | ] 1020 | 1021 | [package.extras] 1022 | full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"] 1023 | 1024 | [[package]] 1025 | name = "tomli" 1026 | version = "2.0.1" 1027 | description = "A lil' TOML parser" 1028 | optional = false 1029 | python-versions = ">=3.7" 1030 | files = [ 1031 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1032 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1033 | ] 1034 | 1035 | [[package]] 1036 | name = "tox" 1037 | version = "3.28.0" 1038 | description = "tox is a generic virtualenv management and test command line tool" 1039 | optional = false 1040 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 1041 | files = [ 1042 | {file = "tox-3.28.0-py2.py3-none-any.whl", hash = "sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea"}, 1043 | {file = "tox-3.28.0.tar.gz", hash = "sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640"}, 1044 | ] 1045 | 1046 | [package.dependencies] 1047 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 1048 | filelock = ">=3.0.0" 1049 | packaging = ">=14" 1050 | pluggy = ">=0.12.0" 1051 | py = ">=1.4.17" 1052 | six = ">=1.14.0" 1053 | tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} 1054 | 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" 1055 | 1056 | [package.extras] 1057 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 1058 | 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)"] 1059 | 1060 | [[package]] 1061 | name = "traitlets" 1062 | version = "5.14.3" 1063 | description = "Traitlets Python configuration system" 1064 | optional = false 1065 | python-versions = ">=3.8" 1066 | files = [ 1067 | {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, 1068 | {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, 1069 | ] 1070 | 1071 | [package.extras] 1072 | docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] 1073 | test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] 1074 | 1075 | [[package]] 1076 | name = "typing-extensions" 1077 | version = "4.12.2" 1078 | description = "Backported and Experimental Type Hints for Python 3.8+" 1079 | optional = false 1080 | python-versions = ">=3.8" 1081 | files = [ 1082 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 1083 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 1084 | ] 1085 | 1086 | [[package]] 1087 | name = "urllib3" 1088 | version = "2.2.2" 1089 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1090 | optional = false 1091 | python-versions = ">=3.8" 1092 | files = [ 1093 | {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, 1094 | {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, 1095 | ] 1096 | 1097 | [package.extras] 1098 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 1099 | h2 = ["h2 (>=4,<5)"] 1100 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1101 | zstd = ["zstandard (>=0.18.0)"] 1102 | 1103 | [[package]] 1104 | name = "uvicorn" 1105 | version = "0.14.0" 1106 | description = "The lightning-fast ASGI server." 1107 | optional = false 1108 | python-versions = "*" 1109 | files = [ 1110 | {file = "uvicorn-0.14.0-py3-none-any.whl", hash = "sha256:2a76bb359171a504b3d1c853409af3adbfa5cef374a4a59e5881945a97a93eae"}, 1111 | {file = "uvicorn-0.14.0.tar.gz", hash = "sha256:45ad7dfaaa7d55cab4cd1e85e03f27e9d60bc067ddc59db52a2b0aeca8870292"}, 1112 | ] 1113 | 1114 | [package.dependencies] 1115 | asgiref = ">=3.3.4" 1116 | click = ">=7" 1117 | h11 = ">=0.8" 1118 | 1119 | [package.extras] 1120 | standard = ["PyYAML (>=5.1)", "colorama (>=0.4)", "httptools (==0.2.*)", "python-dotenv (>=0.13)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchgod (>=0.6)", "websockets (>=9.1)"] 1121 | 1122 | [[package]] 1123 | name = "virtualenv" 1124 | version = "20.26.3" 1125 | description = "Virtual Python Environment builder" 1126 | optional = false 1127 | python-versions = ">=3.7" 1128 | files = [ 1129 | {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, 1130 | {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, 1131 | ] 1132 | 1133 | [package.dependencies] 1134 | distlib = ">=0.3.7,<1" 1135 | filelock = ">=3.12.2,<4" 1136 | platformdirs = ">=3.9.1,<5" 1137 | 1138 | [package.extras] 1139 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 1140 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 1141 | 1142 | [[package]] 1143 | name = "wcwidth" 1144 | version = "0.2.13" 1145 | description = "Measures the displayed width of unicode strings in a terminal" 1146 | optional = false 1147 | python-versions = "*" 1148 | files = [ 1149 | {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, 1150 | {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, 1151 | ] 1152 | 1153 | [[package]] 1154 | name = "yapf" 1155 | version = "0.31.0" 1156 | description = "A formatter for Python code." 1157 | optional = false 1158 | python-versions = "*" 1159 | files = [ 1160 | {file = "yapf-0.31.0-py2.py3-none-any.whl", hash = "sha256:e3a234ba8455fe201eaa649cdac872d590089a18b661e39bbac7020978dd9c2e"}, 1161 | {file = "yapf-0.31.0.tar.gz", hash = "sha256:408fb9a2b254c302f49db83c59f9aa0b4b0fd0ec25be3a5c51181327922ff63d"}, 1162 | ] 1163 | 1164 | [metadata] 1165 | lock-version = "2.0" 1166 | python-versions = "^3.10" 1167 | content-hash = "4ffe33125ed92ceba36316dc25663069f2718ef4680db9bc8a5c16f7b89fde39" 1168 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fastapi-starter-template" 3 | version = "0.1.0" 4 | description = "" 5 | authors = [""] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.10" 9 | fastapi = "^0.61.0" 10 | uvicorn = "0.14.0" 11 | requests = "^2.24.0" 12 | asgi-lifespan = "^1.0.1" 13 | python-dateutil = "^2.8.1" 14 | httpx = "^0.18.2" 15 | aioredis = "^2.0.0" 16 | 17 | [tool.poetry.dev-dependencies] 18 | pytest = "^8.2" 19 | pre-commit = "^2.6.0" 20 | tox = "^3.19.0" 21 | pytest-cov = "^5.0.0" 22 | pytest-asyncio = "^0.23.7" 23 | httpx = "^0.18.1" 24 | ipdb = "^0.13.9" 25 | yapf = "^0.31.0" 26 | 27 | [build-system] 28 | requires = ["poetry>=0.12"] 29 | build-backend = "poetry.masonry.api" 30 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/fastapi-redis-tutorial/bd72d601031312ff69fbaec702934c3ea4beb222/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Generator 3 | 4 | import aioredis 5 | import pytest 6 | from asgi_lifespan import LifespanManager 7 | from httpx import AsyncClient 8 | 9 | from app.main import app 10 | from app.main import config 11 | from app.main import initialize_redis 12 | from app.main import Keys 13 | from app.main import make_keys 14 | 15 | TEST_PREFIX = 'test:is-bitcoin-lit' 16 | 17 | 18 | @pytest.fixture(scope='module') 19 | def redis() -> Generator: 20 | yield aioredis.from_url(config.redis_url, decode_responses=True) 21 | 22 | 23 | @pytest.fixture(scope='session') 24 | def event_loop(request): 25 | """Create an instance of the default event loop for each test case.""" 26 | loop = asyncio.get_event_loop_policy().new_event_loop() 27 | yield loop 28 | loop.close() 29 | 30 | 31 | @pytest.fixture 32 | async def keys(redis: aioredis.Redis): 33 | def make_test_keys(): 34 | return Keys(TEST_PREFIX) 35 | app.dependency_overrides[make_keys] = make_test_keys 36 | keys = make_test_keys() 37 | 38 | yield keys 39 | 40 | # Cleanup any test keys the test run created 41 | keys = await redis.keys(f'{TEST_PREFIX}*') 42 | if keys: 43 | await redis.delete(*keys) 44 | 45 | 46 | @pytest.fixture(scope='function') 47 | async def client(keys): 48 | async with AsyncClient(app=app, base_url='http://test') as client, \ 49 | LifespanManager(app): 50 | yield client 51 | 52 | 53 | @pytest.fixture(scope='function', autouse=True) 54 | @pytest.mark.asyncio 55 | async def setup_redis(request, keys): 56 | await initialize_redis(keys) 57 | -------------------------------------------------------------------------------- /tests/test_bitcoin_api.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import os.path 4 | from unittest import mock 5 | 6 | import pytest 7 | from fastapi.testclient import TestClient 8 | 9 | from app.main import app 10 | 11 | 12 | REFRESH_URL = '/refresh' 13 | URL = '/is-bitcoin-lit' 14 | EXPECTED_FIELDS = ( 15 | 'hourly_average_of_averages', 16 | 'sentiment_direction', 17 | 'price_direction', 18 | ) 19 | JSON_FIXTURE = os.path.join( 20 | os.path.dirname(os.path.realpath(__file__)), 21 | 'fixtures', 22 | 'sentiment_response.json', 23 | ) 24 | 25 | 26 | @pytest.fixture 27 | def mock_bitcoin_api(): 28 | with mock.patch('app.main.now') as mock_utcnow: 29 | with mock.patch('httpx.AsyncClient.get') as mock_get: 30 | with mock.patch('httpx.Response') as mock_response: 31 | mock_utcnow.return_value = datetime.datetime( 32 | 2021, 7, 7, 10, 30, 0, 0, # 2020-07-07 10:30:00 UTC 33 | ) 34 | 35 | # Mock out Response.json() 36 | m = mock.MagicMock() 37 | with open(JSON_FIXTURE) as f: 38 | m.return_value = json.loads(f.read()) 39 | mock_response.json = m 40 | 41 | # Make get() return our fake Response. 42 | mock_get.return_value = mock_response 43 | 44 | yield mock_get 45 | 46 | 47 | client = TestClient(app) 48 | 49 | 50 | def test_api(mock_bitcoin_api: mock.MagicMock): 51 | client.post(REFRESH_URL) 52 | res = client.get(URL) 53 | summary = res.json() 54 | 55 | assert res.status_code == 200 56 | 57 | for field in EXPECTED_FIELDS: 58 | assert field in summary 59 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | isolated_build = True 3 | envlist = py38,pre-commit 4 | 5 | # tell tox not to require a setup.py file 6 | skipsdist = True 7 | 8 | [testenv] 9 | whitelist_externals = poetry 10 | commands = 11 | poetry install -v 12 | poetry run pytest --cov --cov-report=term-missing --cov-fail-under=100 13 | 14 | [testenv:pre-commit] 15 | skip_install = True 16 | deps = pre-commit 17 | commands = pre-commit run --all-files --show-diff-on-failure 18 | --------------------------------------------------------------------------------