diff --git a/README.md b/README.md index fa1bebc..7aa94e2 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,20 @@ - [`Pydantic V2`](https://docs.pydantic.dev/2.4/): the most widely used data validation library for Python, now rewritten in Rust [`(5x to 50x speed improvement)`](https://docs.pydantic.dev/latest/blog/pydantic-v2-alpha/) - [`SQLAlchemy 2.0`](https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html): Python SQL toolkit and Object Relational Mapper - [`PostgreSQL`](https://www.postgresql.org): The World's Most Advanced Open Source Relational Database +- [`Redis`](https://redis.io): The open source, in-memory data store used by millions of developers as a database, cache, streaming engine, and message broker. ## 1. Features - Fully async - Pydantic V2 and SQLAlchemy 2.0 - User authentication with JWT + - Easy redis caching - Easily extendable - Flexible ### 1.1 To do -- [ ] Redis cache -- [ ] Google SSO +- [x] Redis cache - [ ] Arq job queues +- [ ] App settings (such as database connection, etc) only for what's inherited in core.config.Settings ## 2. Contents 0. [About](#0-about) @@ -29,7 +31,9 @@ 4. [Requirements](#4-requirements) 1. [Packages](#41-packages) 2. [Environment Variables](#42-environment-variables) -5. [Running PostgreSQL with docker](#5-running-postgresql-with-docker) +5. [Running Databases With Docker](#5-running-databases-with-docker) + 1. [PostgreSQL](#51-postgresql-main-database) + 2. [Redis](#52-redis-for-caching) 6. [Running the api](#6-running-the-api) 7. [Creating the first superuser](#7-creating-the-first-superuser) 8. [Database Migrations](#8-database-migrations) @@ -40,7 +44,9 @@ 4. [Alembic Migrations](#94-alembic-migration) 5. [CRUD](#95-crud) 6. [Routes](#96-routes) - 7. [Running](#97-running) + 7. [Caching](#97-caching) + 8. [More Advanced Caching](#98-more-advanced-caching) + 9. [Running](#99-running) 10. [Testing](#10-testing) 11. [Contributing](#11-contributing) 12. [References](#12-references) @@ -116,8 +122,15 @@ ADMIN_USERNAME="your_username" ADMIN_PASSWORD="your_password" ``` +Optionally, for redis caching: +``` +# ------------- redis ------------- +REDIS_CACHE_HOST="your_host" # default localhost +REDIS_CACHE_PORT=6379 +``` ___ -## 5. Running PostgreSQL with docker: +## 5. Running Databases With Docker: +### 5.1 PostgreSQL (main database) Install docker if you don't have it yet, then run: ```sh docker pull postgres @@ -145,6 +158,29 @@ docker run -d \ [`If you didn't create the .env variables yet, click here.`](#environment-variables) +### 5.2 Redis (for caching) +Install docker if you don't have it yet, then run: +```sh +docker pull redis:alpine +``` + +And pick the name and port, replacing the fields: +```sh +docker run -d \ + --name {NAME} \ + -p {PORT}:{PORT} \ +redis:alpine +``` + +Such as +```sh +docker run -d \ + --name redis \ + -p 6379:6379 \ +redis:alpine +``` + +[`If you didn't create the .env variables yet, click here.`](#environment-variables) ___ ## 6. Running the api While in the **src** folder, run to start the application with uvicorn server: @@ -290,7 +326,109 @@ router = APIRouter(prefix="/v1") # this should be there already router.include_router(entity_router) ``` -### 9.7 Running +### 9.7 Caching +The cache decorator allows you to cache the results of FastAPI endpoint functions, enhancing response times and reducing the load on your application by storing and retrieving data in a cache. + +Caching the response of an endpoint is really simple, just apply the cache decorator to the endpoint function. +Note that you should always pass request as a variable to your endpoint function. +```python +... +from app.core.cache import cache + +@app.get("/sample/{my_id}") +@cache( + key_prefix="sample_data", + expiration=3600, + resource_id_name="my_id" +) +async def sample_endpoint(request: Request, my_id: int): + # Endpoint logic here + return {"data": "my_data"} +``` + +The way it works is: +- the data is saved in redis with the following cache key: "sample_data:{my_id}" +- then the the time to expire is set as 3600 seconds (that's the default) + +Another option is not passing the resource_id_name, but passing the resource_id_type (default int): +```python +... +from app.core.cache import cache + +@app.get("/sample/{my_id}") +@cache( + key_prefix="sample_data", + resource_id_type=int +) +async def sample_endpoint(request: Request, my_id: int): + # Endpoint logic here + return {"data": "my_data"} +``` +In this case, what will happen is: +- the resource_id will be inferred from the keyword arguments (my_id in this case) +- the data is saved in redis with the following cache key: "sample_data:{my_id}" +- then the the time to expire is set as 3600 seconds (that's the default) + +Passing resource_id_name is usually preferred. + +### 9.8 More Advanced Caching +The behaviour of the `cache` decorator changes based on the request method of your endpoint. +It caches the result if you are passing it to a **GET** endpoint, and it invalidates the cache with this key_prefix and id if passed to other endpoints (**PATCH**, **DELETE**). + +If you also want to invalidate cache with a different key, you can use the decorator with the "to_invalidate_extra" variable. + +In the following example, I want to invalidate the cache for a certain user_id, since I'm deleting it, but I also want to invalidate the cache for the list of users, so it will not be out of sync. + +```python +# The cache here will be saved as "{username}_posts:{username}": +@router.get("/{username}/posts", response_model=List[PostRead]) +@cache(key_prefix="{username}_posts", resource_id_name="username") +async def read_posts( + request: Request, + username: str, + db: Annotated[AsyncSession, Depends(async_get_db)] +): + ... + +... + +# I'll invalidate the cache for the former endpoint by just passing the key_prefix and id as a dictionary: +@router.delete("/{username}/post/{id}") +@cache( + "{username}_post_cache", + resource_id_name="id", + to_invalidate_extra={"{username}_posts": "{username}"} # Now it will also invalidate the cache with id "{username}_posts:{username}" +) +async def erase_post( + request: Request, + username: str, + id: int, + current_user: Annotated[UserRead, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(async_get_db)] +): + ... + +# And now I'll also invalidate when I update the user: +@router.patch("/{username}/post/{id}", response_model=PostRead) +@cache( + "{username}_post_cache", + resource_id_name="id", + to_invalidate_extra={"{username}_posts": "{username}"} +) +async def patch_post( + request: Request, + username: str, + id: int, + values: PostUpdate, + current_user: Annotated[UserRead, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(async_get_db)] +): + ... +``` + +Note that this will not work for **GET** requests. + +### 9.9 Running While in the **src** folder, run to start the application with uvicorn server: ```sh poetry run uvicorn app.main:app --reload diff --git a/src/app/api/v1/login.py b/src/app/api/v1/login.py index 4a11e54..42d64a8 100644 --- a/src/app/api/v1/login.py +++ b/src/app/api/v1/login.py @@ -16,7 +16,7 @@ @router.post("/login", response_model=Token) async def login_for_access_token( form_data: Annotated[OAuth2PasswordRequestForm, Depends()], - db: AsyncSession = Depends(async_get_db) + db: Annotated[AsyncSession, Depends(async_get_db)] ): user = await authenticate_user( diff --git a/src/app/api/v1/posts.py b/src/app/api/v1/posts.py index 5abd000..0377a31 100644 --- a/src/app/api/v1/posts.py +++ b/src/app/api/v1/posts.py @@ -1,6 +1,6 @@ from typing import List, Annotated -from fastapi import Depends, HTTPException +from fastapi import Request, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession import fastapi @@ -11,11 +11,13 @@ from app.crud.crud_posts import crud_posts from app.crud.crud_users import crud_users from app.api.exceptions import privileges_exception +from app.core.cache import cache router = fastapi.APIRouter(tags=["posts"]) @router.post("/{username}/post", response_model=PostRead, status_code=201) async def write_post( + request: Request, username: str, post: PostCreate, current_user: Annotated[UserRead, Depends(get_current_user)], @@ -35,7 +37,9 @@ async def write_post( @router.get("/{username}/posts", response_model=List[PostRead]) +@cache(key_prefix="{username}_posts", resource_id_name="username") async def read_posts( + request: Request, username: str, db: Annotated[AsyncSession, Depends(async_get_db)] ): @@ -48,7 +52,9 @@ async def read_posts( @router.get("/{username}/post/{id}", response_model=PostRead) +@cache(key_prefix="{username}_post_cache", resource_id_name="id") async def read_post( + request: Request, username: str, id: int, db: Annotated[AsyncSession, Depends(async_get_db)] @@ -65,7 +71,13 @@ async def read_post( @router.patch("/{username}/post/{id}", response_model=PostRead) +@cache( + "{username}_post_cache", + resource_id_name="id", + to_invalidate_extra={"{username}_posts": "{username}"} +) async def patch_post( + request: Request, username: str, id: int, values: PostUpdate, @@ -87,7 +99,13 @@ async def patch_post( @router.delete("/{username}/post/{id}") +@cache( + "{username}_post_cache", + resource_id_name="id", + to_invalidate_extra={"{username}_posts": "{username}"} +) async def erase_post( + request: Request, username: str, id: int, current_user: Annotated[UserRead, Depends(get_current_user)], @@ -114,7 +132,13 @@ async def erase_post( @router.delete("/{username}/db_post/{id}", dependencies=[Depends(get_current_superuser)]) +@cache( + "{username}_post_cache", + resource_id_name="id", + to_invalidate_extra={"{username}_posts": "{username}"} +) async def erase_db_post( + request: Request, username: str, id: int, db: Annotated[AsyncSession, Depends(async_get_db)] diff --git a/src/app/api/v1/users.py b/src/app/api/v1/users.py index 856c1a2..b9ef808 100644 --- a/src/app/api/v1/users.py +++ b/src/app/api/v1/users.py @@ -2,6 +2,7 @@ from fastapi import Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession +from fastapi import Request import fastapi from app.schemas.user import UserCreate, UserCreateInternal, UserUpdate, UserRead, UserBase @@ -14,7 +15,7 @@ router = fastapi.APIRouter(tags=["users"]) @router.post("/user", response_model=UserRead, status_code=201) -async def write_user(user: UserCreate, db: AsyncSession = Depends(async_get_db)): +async def write_user(request: Request, user: UserCreate, db: Annotated[AsyncSession, Depends(async_get_db)]): db_user = await crud_users.get(db=db, email=user.email) if db_user: raise HTTPException(status_code=400, detail="Email is already registered") @@ -32,33 +33,36 @@ async def write_user(user: UserCreate, db: AsyncSession = Depends(async_get_db)) @router.get("/users", response_model=List[UserRead]) -async def read_users(db: AsyncSession = Depends(async_get_db)): +async def read_users(request: Request, db: Annotated[AsyncSession, Depends(async_get_db)]): users = await crud_users.get_multi(db=db, is_deleted=False) return users @router.get("/user/me/", response_model=UserRead) async def read_users_me( - current_user: Annotated[UserRead, Depends(get_current_user)] + request: Request, current_user: Annotated[UserRead, Depends(get_current_user)] ): return current_user +from app.core.cache import cache + @router.get("/user/{username}", response_model=UserRead) -async def read_user(username: str, db: AsyncSession = Depends(async_get_db)): +async def read_user(request: Request, username: str, db: Annotated[AsyncSession, Depends(async_get_db)]): db_user = await crud_users.get(db=db, username=username, is_deleted=False) if db_user is None: raise HTTPException(status_code=404, detail="User not found") - + return db_user @router.patch("/user/{username}", response_model=UserRead) async def patch_user( + request: Request, values: UserUpdate, username: str, current_user: Annotated[UserRead, Depends(get_current_user)], - db: AsyncSession = Depends(async_get_db) + db: Annotated[AsyncSession, Depends(async_get_db)] ): db_user = await crud_users.get(db=db, username=username) if db_user is None: @@ -83,9 +87,10 @@ async def patch_user( @router.delete("/user/{username}") async def erase_user( + request: Request, username: str, current_user: Annotated[UserRead, Depends(get_current_user)], - db: AsyncSession = Depends(async_get_db) + db: Annotated[AsyncSession, Depends(async_get_db)] ): db_user = await crud_users.get(db=db, username=username) if db_user is None: @@ -100,8 +105,9 @@ async def erase_user( @router.delete("/db_user/{username}", dependencies=[Depends(get_current_superuser)]) async def erase_db_user( + request: Request, username: str, - db: AsyncSession = Depends(async_get_db) + db: Annotated[AsyncSession, Depends(async_get_db)] ): db_user = await crud_users.get(db=db, username=username) if db_user is None: diff --git a/src/app/core/cache.py b/src/app/core/cache.py new file mode 100644 index 0000000..b9e5c99 --- /dev/null +++ b/src/app/core/cache.py @@ -0,0 +1,287 @@ +from typing import Callable, Union, List, Dict, Any +import functools +import json +from uuid import UUID +from datetime import datetime +import re + +from fastapi import Request, Response +from redis.asyncio import Redis, ConnectionPool +from sqlalchemy.orm import class_mapper, DeclarativeBase + +from app.core.exceptions import CacheIdentificationInferenceError, InvalidRequestError + +pool: ConnectionPool | None = None +client: Redis | None = None + +def _serialize_sqlalchemy_object(obj: DeclarativeBase) -> Dict[str, Any]: + """ + Serialize a SQLAlchemy DeclarativeBase object to a dictionary. + + Parameters + ---------- + obj: DeclarativeBase + The SQLAlchemy DeclarativeBase object to be serialized. + + Returns + ------- + Dict[str, Any] + A dictionary containing the serialized attributes of the object. + + Note + ---- + - Datetime objects are converted to ISO 8601 string format. + - UUID objects are converted to strings before serializing to JSON. + """ + if isinstance(obj, DeclarativeBase): + data = {} + for column in class_mapper(obj.__class__).columns: + value = getattr(obj, column.name) + + if isinstance(value, datetime): + value = value.isoformat() + + if isinstance(value, UUID): + value = str(value) + + data[column.name] = value + return data + + +def _infer_resource_id(kwargs: Dict[str, Any], resource_id_type: Union[type, str]) -> Union[None, int, str]: + """ + Infer the resource ID from a dictionary of keyword arguments. + + Parameters + ---------- + kwargs: Dict[str, Any] + A dictionary of keyword arguments. + resource_id_type: Union[type, str] + The expected type of the resource ID, which can be an integer (int) or a string (str). + + Returns + ------- + Union[None, int, str] + The inferred resource ID. If it cannot be inferred or does not match the expected type, it returns None. + + Note + ---- + - When `resource_id_type` is 'int', the function looks for an argument with the key 'id'. + - When `resource_id_type` is 'str', it attempts to infer the resource ID as a string. + """ + resource_id = None + for arg_name, arg_value in kwargs.items(): + if isinstance(arg_value, resource_id_type): + if (resource_id_type is int) and ("id" in arg_name): + resource_id = arg_value + + elif (resource_id_type is int) and ("id" not in arg_name): + pass + + elif resource_id_type is str: + resource_id = arg_value + + if resource_id is None: + raise CacheIdentificationInferenceError + + return resource_id + + +def _extract_data_inside_brackets(input_string: str) -> List[str]: + """ + Extract data inside curly brackets from a given string using regular expressions. + + Parameters + ---------- + input_string: str + The input string in which to find data enclosed within curly brackets. + + Returns + ------- + List[str] + A list of strings containing the data found inside the curly brackets within the input string. + + Example + ------- + >>> _extract_data_inside_brackets("The {quick} brown {fox} jumps over the {lazy} dog.") + ['quick', 'fox', 'lazy'] + """ + data_inside_brackets = re.findall(r'{(.*?)}', input_string) + return data_inside_brackets + + +def _construct_data_dict(data_inside_brackets: List[str], kwargs: Dict[str, Any]) -> Dict[str, Any]: + """ + Construct a dictionary based on data inside brackets and keyword arguments. + + Parameters + ---------- + data_inside_brackets: List[str] + A list of keys inside brackets. + kwargs: Dict[str, Any] + A dictionary of keyword arguments. + + Returns + ------- + Dict[str, Any]: A dictionary with keys from data_inside_brackets and corresponding values from kwargs. + """ + data_dict = {} + for key in data_inside_brackets: + data_dict[key] = kwargs[key] + return data_dict + + +def _format_prefix(prefix: str, kwargs: Dict[str, Any]) -> str: + """ + Format a prefix using keyword arguments. + + Parameters + ---------- + prefix: str + The prefix template to be formatted. + kwargs: Dict[str, Any] + A dictionary of keyword arguments. + + Returns + ------- + str: The formatted prefix. + """ + data_inside_brackets = _extract_data_inside_brackets(prefix) + data_dict = _construct_data_dict(data_inside_brackets, kwargs) + formatted_prefix = prefix.format(**data_dict) + return formatted_prefix + + +def _format_extra_data( + to_invalidate_extra: Dict[str, str], + kwargs: Dict[str, Any] +) -> Dict[str, Any]: + """ + Format extra data based on provided templates and keyword arguments. + + This function takes a dictionary of templates and their associated values and a dictionary of keyword arguments. + It formats the templates with the corresponding values from the keyword arguments and returns a dictionary + where keys are the formatted templates and values are the associated keyword argument values. + + Parameters + ---------- + to_invalidate_extra: Dict[str, str] + A dictionary where keys are templates and values are the associated values. + kwargs: Dict[str, Any] + A dictionary of keyword arguments. + + Returns + ------- + Dict[str, Any]: A dictionary where keys are formatted templates and values are associated keyword argument values. + """ + formatted_extra = {} + for prefix, id_template in to_invalidate_extra.items(): + formatted_prefix = _format_prefix(prefix, kwargs) + id = _extract_data_inside_brackets(id_template)[0] + formatted_extra[formatted_prefix] = kwargs[id] + + return formatted_extra + + +def cache( + key_prefix: str, + resource_id_name: Any = None, + expiration: int = 3600, + resource_id_type: Union[type, List[type]] = int, + to_invalidate_extra: Dict[str, Any] | None = None +) -> Callable: + """ + Cache decorator for FastAPI endpoints. + + This decorator allows you to cache the results of FastAPI endpoint functions, improving response times and + reducing the load on the application by storing and retrieving data in a cache. + + Parameters + ---------- + key_prefix: str + A unique prefix to identify the cache key. + resource_id: Any, optional + The resource ID to be used in cache key generation. If not provided, it will be inferred from the endpoint's keyword arguments. + expiration: int, optional + The expiration time for cached data in seconds. Defaults to 3600 seconds (1 hour). + resource_id_type: Union[type, List[type]], optional + The expected type of the resource ID. This can be a single type (e.g., int) or a list of types (e.g., [int, str]). Defaults to int. + + Returns + ------- + Callable + A decorator function that can be applied to FastAPI endpoints. + + Example usage + ------------- + + ```python + from fastapi import FastAPI, Request + from my_module import cache # Replace with your actual module and imports + + app = FastAPI() + + # Define a sample endpoint with caching + @app.get("/sample/{resource_id}") + @cache(key_prefix="sample_data", expiration=3600, resource_id_type=int) + async def sample_endpoint(request: Request, resource_id: int): + # Your endpoint logic here + return {"data": "your_data"} + ``` + + This decorator caches the response data of the endpoint function using a unique cache key. + The cached data is retrieved for GET requests, and the cache is invalidated for other types of requests. + + Note: + - For caching lists of objects, ensure that the response is a list of objects, and the decorator will handle caching accordingly. + - resource_id_type is used only if resource_id is not passed. + """ + def wrapper(func: Callable) -> Callable: + @functools.wraps(func) + async def inner(request: Request, *args, **kwargs) -> Response: + if resource_id_name: + resource_id = kwargs[resource_id_name] + else: + resource_id = _infer_resource_id(kwargs=kwargs, resource_id_type=resource_id_type) + + formatted_key_prefix = _format_prefix(key_prefix, kwargs) + cache_key = f"{formatted_key_prefix}:{resource_id}" + + if request.method == "GET": + if to_invalidate_extra: + raise InvalidRequestError + + cached_data = await client.get(cache_key) + if cached_data: + return json.loads(cached_data.decode()) + + result = await func(request, *args, **kwargs) + + if request.method == "GET": + if to_invalidate_extra: + raise InvalidRequestError + + if isinstance(result, list): + serialized_data = json.dumps( + [_serialize_sqlalchemy_object(obj) for obj in result] + ) + else: + serialized_data = json.dumps( + _serialize_sqlalchemy_object(result) + ) + + await client.set(cache_key, serialized_data) + await client.expire(cache_key, expiration) + else: + await client.delete(cache_key) + if to_invalidate_extra: + formatted_extra = _format_extra_data(to_invalidate_extra, kwargs) + for prefix, id in formatted_extra.items(): + extra_cache_key = f"{prefix}:{id}" + await client.delete(extra_cache_key) + + return result + + return inner + + return wrapper diff --git a/src/app/core/config.py b/src/app/core/config.py index d04a3c1..2147f66 100644 --- a/src/app/core/config.py +++ b/src/app/core/config.py @@ -1,7 +1,4 @@ -from typing import Optional - from decouple import config -from pydantic import SecretStr from pydantic_settings import BaseSettings class AppSettings(BaseSettings): @@ -37,11 +34,26 @@ class FirstUserSettings(BaseSettings): ADMIN_PASSWORD: str = config("ADMIN_PASSWORD", default="!Ch4ng3Th1sP4ssW0rd!") +class TestSettings(BaseSettings): + TEST_NAME: str = config("TEST_NAME", default="Tester User") + TEST_EMAIL: str = config("TEST_EMAIL", default="test@tester.com") + TEST_USERNAME: str = config("TEST_USERNAME", default="testeruser") + TEST_PASSWORD: str = config("TEST_PASSWORD", default="Str1ng$t") + + +class RedisCacheSettings(BaseSettings): + REDIS_CACHE_HOST: str = config("REDIS_CACHE_HOST", default="localhost") + REDIS_CACHE_PORT: int = config("REDIS_CACHE_PORT", default=6379) + REDIS_CACHE_URL: str = f"redis://{REDIS_CACHE_HOST}:{REDIS_CACHE_PORT}" + + class Settings( AppSettings, PostgresSettings, CryptSettings, - FirstUserSettings + FirstUserSettings, + TestSettings, + RedisCacheSettings ): pass diff --git a/src/app/core/exceptions.py b/src/app/core/exceptions.py new file mode 100644 index 0000000..a2b5864 --- /dev/null +++ b/src/app/core/exceptions.py @@ -0,0 +1,10 @@ +class CacheIdentificationInferenceError(Exception): + def __init__(self, message="Could not infer id for resource being cached."): + self.message = message + super().__init__(self.message) + + +class InvalidRequestError(Exception): + def __init__(self, message="Type of request not supported."): + self.message = message + super().__init__(self.message) diff --git a/src/app/main.py b/src/app/main.py index 5ec0a53..58d52da 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -1,16 +1,29 @@ from fastapi import FastAPI +import redis.asyncio as redis from app.core.database import Base - -from app.api import router from app.core.database import async_engine as engine from app.core.config import settings +from app.api import router +from app.core import cache +# -------------- database -------------- async def create_tables(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) +# -------------- cache -------------- +async def create_redis_cache_pool(): + cache.pool = redis.ConnectionPool.from_url(settings.REDIS_CACHE_URL) + cache.client = redis.Redis.from_pool(cache.pool) + + +async def close_redis_cache_pool(): + await cache.client.close() + + +# -------------- application -------------- def create_application() -> FastAPI: application = FastAPI( title=settings.APP_NAME, @@ -21,6 +34,8 @@ def create_application() -> FastAPI: application.include_router(router) application.add_event_handler("startup", create_tables) + application.add_event_handler("startup", create_redis_cache_pool) + application.add_event_handler("shutdown", close_redis_cache_pool) return application diff --git a/src/poetry.lock b/src/poetry.lock index 195641f..28f54a2 100644 --- a/src/poetry.lock +++ b/src/poetry.lock @@ -50,6 +50,36 @@ doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd- 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)"] trio = ["trio (<0.22)"] +[[package]] +name = "arq" +version = "0.25.0" +description = "Job queues in python with asyncio and redis" +optional = false +python-versions = ">=3.7" +files = [ + {file = "arq-0.25.0-py3-none-any.whl", hash = "sha256:db072d0f39c0bc06b436db67ae1f315c81abc1527563b828955670531815290b"}, + {file = "arq-0.25.0.tar.gz", hash = "sha256:d176ebadfba920c039dc578814d19b7814d67fa15f82fdccccaedb4330d65dae"}, +] + +[package.dependencies] +click = ">=8.0" +redis = {version = ">=4.2.0", extras = ["hiredis"]} +typing-extensions = ">=4.1.0" + +[package.extras] +watch = ["watchfiles (>=0.16)"] + +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + [[package]] name = "asyncpg" version = "0.28.0" @@ -223,6 +253,7 @@ files = [ {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, + {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"}, {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, @@ -231,6 +262,7 @@ files = [ {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, + {file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, @@ -260,6 +292,7 @@ files = [ {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, + {file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, @@ -268,6 +301,7 @@ files = [ {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, + {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"}, {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, @@ -295,6 +329,104 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[[package]] +name = "hiredis" +version = "2.2.3" +description = "Python wrapper for hiredis" +optional = false +python-versions = ">=3.7" +files = [ + {file = "hiredis-2.2.3-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:9a1a80a8fa767f2fdc3870316a54b84fe9fc09fa6ab6a2686783de6a228a4604"}, + {file = "hiredis-2.2.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3f006c28c885deb99b670a5a66f367a175ab8955b0374029bad7111f5357dcd4"}, + {file = "hiredis-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffaf841546905d90ff189de7397aa56413b1ce5e54547f17a98f0ebf3a3b0a3b"}, + {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cadb0ac7ba3babfd804e425946bec9717b320564a1390f163a54af9365a720a"}, + {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33bc4721632ef9708fa44e5df0066053fccc8e65410a2c48573192517a533b48"}, + {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:227c5b4bcb60f89008c275d596e4a7b6625a6b3c827b8a66ae582eace7051f71"}, + {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61995eb826009d99ed8590747bc0da683a5f4fbb4faa8788166bf3810845cd5c"}, + {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f969edc851efe23010e0f53a64269f2629a9364135e9ec81c842e8b2277d0c1"}, + {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d27e560eefb57914d742a837f1da98d3b29cb22eff013c8023b7cf52ae6e051d"}, + {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3759f4789ae1913b7df278dfc9e8749205b7a106f888cd2903d19461e24a7697"}, + {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c6cb613148422c523945cdb8b6bed617856f2602fd8750e33773ede2616e55d5"}, + {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:1d274d5c511dfc03f83f997d3238eaa9b6ee3f982640979f509373cced891e98"}, + {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3b7fe075e91b9d9cff40eba4fb6a8eff74964d3979a39be9a9ef58b1b4cb3604"}, + {file = "hiredis-2.2.3-cp310-cp310-win32.whl", hash = "sha256:77924b0d32fd1f493d3df15d9609ddf9d94c31a364022a6bf6b525ce9da75bea"}, + {file = "hiredis-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:dcb0569dd5bfe6004658cd0f229efa699a3169dcb4f77bd72e188adda302063d"}, + {file = "hiredis-2.2.3-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:d115790f18daa99b5c11a506e48923b630ef712e9e4b40482af942c3d40638b8"}, + {file = "hiredis-2.2.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c3b8be557e08b234774925622e196f0ee36fe4eab66cd19df934d3efd8f3743"}, + {file = "hiredis-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f5446068197b35a11ccc697720c41879c8657e2e761aaa8311783aac84cef20"}, + {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa17a3b22b3726d54d7af20394f65d4a1735a842a4e0f557dc67a90f6965c4bc"}, + {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7df645b6b7800e8b748c217fbd6a4ca8361bcb9a1ae6206cc02377833ec8a1aa"}, + {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fb9300959a0048138791f3d68359d61a788574ec9556bddf1fec07f2dbc5320"}, + {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d7e459fe7313925f395148d36d9b7f4f8dac65be06e45d7af356b187cef65fc"}, + {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8eceffca3941775b646cd585cd19b275d382de43cc3327d22f7c75d7b003d481"}, + {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b17baf702c6e5b4bb66e1281a3efbb1d749c9d06cdb92b665ad81e03118f78fc"}, + {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e43e2b5acaad09cf48c032f7e4926392bb3a3f01854416cf6d82ebff94d5467"}, + {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a7205497d7276a81fe92951a29616ef96562ed2f91a02066f72b6f93cb34b40e"}, + {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:126623b03c31cb6ac3e0d138feb6fcc36dd43dd34fc7da7b7a0c38b5d75bc896"}, + {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:071c5814b850574036506a8118034f97c3cbf2fe9947ff45a27b07a48da56240"}, + {file = "hiredis-2.2.3-cp311-cp311-win32.whl", hash = "sha256:d1be9e30e675f5bc1cb534633324578f6f0944a1bcffe53242cf632f554f83b6"}, + {file = "hiredis-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9a7c987e161e3c58f992c63b7e26fea7fe0777f3b975799d23d65bbb8cb5899"}, + {file = "hiredis-2.2.3-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:f2dcb8389fa3d453927b1299f46bdb38473c293c8269d5c777d33ea0e526b610"}, + {file = "hiredis-2.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2df98f5e071320c7d84e8bd07c0542acdd0a7519307fc31774d60e4b842ec4f"}, + {file = "hiredis-2.2.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61a72e4a523cdfc521762137559c08dfa360a3caef63620be58c699d1717dac1"}, + {file = "hiredis-2.2.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9b9e5bde7030cae83aa900b5bd660decc65afd2db8c400f3c568c815a47ca2a"}, + {file = "hiredis-2.2.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2614f17e261f72efc2f19f5e5ff2ee19e2296570c0dcf33409e22be30710de"}, + {file = "hiredis-2.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46525fbd84523cac75af5bf524bc74aaac848beaf31b142d2df8a787d9b4bbc4"}, + {file = "hiredis-2.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d1a4ce40ba11da9382c14da31f4f9e88c18f7d294f523decd0fadfb81f51ad18"}, + {file = "hiredis-2.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5cda592405bbd29d53942e0389dc3fa77b49c362640210d7e94a10c14a677d4d"}, + {file = "hiredis-2.2.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:5e6674a017629284ef373b50496d9fb1a89b85a20a7fa100ecd109484ec748e5"}, + {file = "hiredis-2.2.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:e62ec131816c6120eff40dffe43424e140264a15fa4ab88c301bd6a595913af3"}, + {file = "hiredis-2.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17e938d9d3ee92e1adbff361706f1c36cc60eeb3e3eeca7a3a353eae344f4c91"}, + {file = "hiredis-2.2.3-cp37-cp37m-win32.whl", hash = "sha256:95d2305fd2a7b179cacb48b10f618872fc565c175f9f62b854e8d1acac3e8a9e"}, + {file = "hiredis-2.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8f9dbe12f011a9b784f58faecc171d22465bb532c310bd588d769ba79a59ef5a"}, + {file = "hiredis-2.2.3-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:5a4bcef114fc071d5f52c386c47f35aae0a5b43673197b9288a15b584da8fa3a"}, + {file = "hiredis-2.2.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:232d0a70519865741ba56e1dfefd160a580ae78c30a1517bad47b3cf95a3bc7d"}, + {file = "hiredis-2.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9076ce8429785c85f824650735791738de7143f61f43ae9ed83e163c0ca0fa44"}, + {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec58fb7c2062f835595c12f0f02dcda76d0eb0831423cc191d1e18c9276648de"}, + {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f2b34a6444b8f9c1e9f84bd2c639388e5d14f128afd14a869dfb3d9af893aa2"}, + {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:818dfd310aa1020a13cd08ee48e116dd8c3bb2e23b8161f8ac4df587dd5093d7"}, + {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96d9ea6c8d4cbdeee2e0d43379ce2881e4af0454b00570677c59f33f2531cd38"}, + {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1eadbcd3de55ac42310ff82550d3302cb4efcd4e17d76646a17b6e7004bb42b"}, + {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:477c34c4489666dc73cb5e89dafe2617c3e13da1298917f73d55aac4696bd793"}, + {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:14824e457e4f5cda685c3345d125da13949bcf3bb1c88eb5d248c8d2c3dee08f"}, + {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9cd32326dfa6ce87edf754153b0105aca64486bebe93b9600ccff74fa0b224df"}, + {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:51341e70b467004dcbec3a6ce8c478d2d6241e0f6b01e4c56764afd5022e1e9d"}, + {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2443659c76b226267e2a04dbbb21bc2a3f91aa53bdc0c22964632753ae43a247"}, + {file = "hiredis-2.2.3-cp38-cp38-win32.whl", hash = "sha256:4e3e3e31423f888d396b1fc1f936936e52af868ac1ec17dd15e3eeba9dd4de24"}, + {file = "hiredis-2.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:20f509e3a1a20d6e5f5794fc37ceb21f70f409101fcfe7a8bde783894d51b369"}, + {file = "hiredis-2.2.3-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:d20891e3f33803b26d54c77fd5745878497091e33f4bbbdd454cf6e71aee8890"}, + {file = "hiredis-2.2.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:50171f985e17970f87d5a29e16603d1e5b03bdbf5c2691a37e6c912942a6b657"}, + {file = "hiredis-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9944a2cac25ffe049a7e89f306e11b900640837d1ef38d9be0eaa4a4e2b73a52"}, + {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a5c8019ff94988d56eb49b15de76fe83f6b42536d76edeb6565dbf7fe14b973"}, + {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a286ded34eb16501002e3713b3130c987366eee2ba0d58c33c72f27778e31676"}, + {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b3e974ad15eb32b1f537730dea70b93a4c3db7b026de3ad2b59da49c6f7454d"}, + {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08415ea74c1c29b9d6a4ca3dd0e810dc1af343c1d1d442e15ba133b11ab5be6a"}, + {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e17d04ea58ab8cf3f2dc52e875db16077c6357846006780086fff3189fb199d"}, + {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6ccdcb635dae85b006592f78e32d97f4bc7541cb27829d505f9c7fefcef48298"}, + {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69536b821dd1bc78058a6e7541743f8d82bf2d981b91280b14c4daa6cdc7faba"}, + {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:3753df5f873d473f055e1f8837bfad0bd3b277c86f3c9bf058c58f14204cd901"}, + {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6f88cafe46612b6fa68e6dea49e25bebf160598bba00101caa51cc8c1f18d597"}, + {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:33ee3ea5cad3a8cb339352cd230b411eb437a2e75d7736c4899acab32056ccdb"}, + {file = "hiredis-2.2.3-cp39-cp39-win32.whl", hash = "sha256:b4f3d06dc16671b88a13ae85d8ca92534c0b637d59e49f0558d040a691246422"}, + {file = "hiredis-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4f674e309cd055ee7a48304ceb8cf43265d859faf4d7d01d270ce45e976ae9d3"}, + {file = "hiredis-2.2.3-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8f280ab4e043b089777b43b4227bdc2035f88da5072ab36588e0ccf77d45d058"}, + {file = "hiredis-2.2.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15c2a551f3b8a26f7940d6ee10b837810201754b8d7e6f6b1391655370882c5a"}, + {file = "hiredis-2.2.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60c4e3c258eafaab21b174b17270a0cc093718d61cdbde8c03f85ec4bf835343"}, + {file = "hiredis-2.2.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc36a9dded458d4e37492fe3e619c6c83caae794d26ad925adbce61d592f8428"}, + {file = "hiredis-2.2.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:4ed68a3b1ccb4313d2a42546fd7e7439ad4745918a48b6c9bcaa61e1e3e42634"}, + {file = "hiredis-2.2.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3bf4b5bae472630c229518e4a814b1b68f10a3d9b00aeaec45f1a330f03a0251"}, + {file = "hiredis-2.2.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33a94d264e6e12a79d9bb8af333b01dc286b9f39c99072ab5fef94ce1f018e17"}, + {file = "hiredis-2.2.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fa6811a618653164f918b891a0fa07052bd71a799defa5c44d167cac5557b26"}, + {file = "hiredis-2.2.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af33f370be90b48bbaf0dab32decbdcc522b1fa95d109020a963282086518a8e"}, + {file = "hiredis-2.2.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b9953d87418ac228f508d93898ab572775e4d3b0eeb886a1a7734553bcdaf291"}, + {file = "hiredis-2.2.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5e7bb4dd524f50b71c20ef5a12bd61da9b463f8894b18a06130942fe31509881"}, + {file = "hiredis-2.2.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89a258424158eb8b3ed9f65548d68998da334ef155d09488c5637723eb1cd697"}, + {file = "hiredis-2.2.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f4a65276f6ecdebe75f2a53f578fbc40e8d2860658420d5e0611c56bbf5054c"}, + {file = "hiredis-2.2.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:334f2738700b20faa04a0d813366fb16ed17287430a6b50584161d5ad31ca6d7"}, + {file = "hiredis-2.2.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d194decd9608f11c777946f596f31d5aacad13972a0a87829ae1e6f2d26c1885"}, + {file = "hiredis-2.2.3.tar.gz", hash = "sha256:e75163773a309e56a9b58165cf5a50e0f84b755f6ff863b2c01a38918fe92daa"}, +] + [[package]] name = "httpcore" version = "0.18.0" @@ -454,6 +586,16 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -773,6 +915,25 @@ files = [ [package.extras] dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatch", "invoke (==1.7.3)", "more-itertools (==4.3.0)", "pbr (==4.3.0)", "pluggy (==1.0.0)", "py (==1.11.0)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-timeout (==2.1.0)", "pyyaml (==5.1)"] +[[package]] +name = "redis" +version = "5.0.1" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-5.0.1-py3-none-any.whl", hash = "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f"}, + {file = "redis-5.0.1.tar.gz", hash = "sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""} +hiredis = {version = ">=1.0.0", optional = true, markers = "extra == \"hiredis\""} + +[package.extras] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + [[package]] name = "rsa" version = "4.9" @@ -832,6 +993,14 @@ files = [ {file = "SQLAlchemy-2.0.21-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b69f1f754d92eb1cc6b50938359dead36b96a1dcf11a8670bff65fd9b21a4b09"}, {file = "SQLAlchemy-2.0.21-cp311-cp311-win32.whl", hash = "sha256:af520a730d523eab77d754f5cf44cc7dd7ad2d54907adeb3233177eeb22f271b"}, {file = "SQLAlchemy-2.0.21-cp311-cp311-win_amd64.whl", hash = "sha256:141675dae56522126986fa4ca713739d00ed3a6f08f3c2eb92c39c6dfec463ce"}, + {file = "SQLAlchemy-2.0.21-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:56628ca27aa17b5890391ded4e385bf0480209726f198799b7e980c6bd473bd7"}, + {file = "SQLAlchemy-2.0.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db726be58837fe5ac39859e0fa40baafe54c6d54c02aba1d47d25536170b690f"}, + {file = "SQLAlchemy-2.0.21-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7421c1bfdbb7214313919472307be650bd45c4dc2fcb317d64d078993de045b"}, + {file = "SQLAlchemy-2.0.21-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:632784f7a6f12cfa0e84bf2a5003b07660addccf5563c132cd23b7cc1d7371a9"}, + {file = "SQLAlchemy-2.0.21-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f6f7276cf26145a888f2182a98f204541b519d9ea358a65d82095d9c9e22f917"}, + {file = "SQLAlchemy-2.0.21-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2a1f7ffac934bc0ea717fa1596f938483fb8c402233f9b26679b4f7b38d6ab6e"}, + {file = "SQLAlchemy-2.0.21-cp312-cp312-win32.whl", hash = "sha256:bfece2f7cec502ec5f759bbc09ce711445372deeac3628f6fa1c16b7fb45b682"}, + {file = "SQLAlchemy-2.0.21-cp312-cp312-win_amd64.whl", hash = "sha256:526b869a0f4f000d8d8ee3409d0becca30ae73f494cbb48801da0129601f72c6"}, {file = "SQLAlchemy-2.0.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7614f1eab4336df7dd6bee05bc974f2b02c38d3d0c78060c5faa4cd1ca2af3b8"}, {file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d59cb9e20d79686aa473e0302e4a82882d7118744d30bb1dfb62d3c47141b3ec"}, {file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a95aa0672e3065d43c8aa80080cdd5cc40fe92dc873749e6c1cf23914c4b83af"}, @@ -1018,4 +1187,4 @@ test = ["Cython (>=0.29.32,<0.30.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "my [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "1d529896506678c7a75d0de010de4e8a2a20b18f422c4e446209a0c966eb5c9e" +content-hash = "90553548c41bdfae1cee5cc495200ad35ffa158a4d42d5fda165c02ebc0f72eb" diff --git a/src/pyproject.toml b/src/pyproject.toml index 1cced61..247c8d0 100644 --- a/src/pyproject.toml +++ b/src/pyproject.toml @@ -28,6 +28,8 @@ python-decouple = "^3.8" greenlet = "^2.0.2" httpx = "^0.25.0" pydantic-settings = "^2.0.3" +arq = "^0.25.0" +redis = "^5.0.1" [build-system] diff --git a/src/tests/test_user.py b/src/tests/test_user.py index fca2da3..1935e13 100644 --- a/src/tests/test_user.py +++ b/src/tests/test_user.py @@ -1,16 +1,16 @@ from fastapi.testclient import TestClient -from decouple import config from src.app.main import app +from src.app.core.config import settings from tests.helper import _get_token -test_name = config("TEST_NAME") -test_username = config("TEST_USERNAME") -test_email = config("TEST_EMAIL") -test_password = config("TEST_PASSWORD") +test_name = settings.TEST_NAME +test_username = settings.TEST_USERNAME +test_email = settings.TEST_EMAIL +test_password = settings.TEST_PASSWORD -admin_username = config("ADMIN_USERNAME") -admin_password = config("ADMIN_PASSWORD") +admin_username = settings.ADMIN_USERNAME +admin_password = settings.ADMIN_PASSWORD client = TestClient(app)