mirror of
https://github.com/revanced/revanced-polling-api.git
synced 2025-05-02 15:44:33 +02:00
commit
bc05d099cd
@ -2,17 +2,18 @@ from redis import asyncio as aioredis
|
|||||||
import app.utils.Logger as Logger
|
import app.utils.Logger as Logger
|
||||||
from app.dependencies import load_config
|
from app.dependencies import load_config
|
||||||
from app.utils.RedisConnector import RedisConnector
|
from app.utils.RedisConnector import RedisConnector
|
||||||
|
from app.models.BallotModel import BallotModel
|
||||||
|
|
||||||
config: dict = load_config()
|
config: dict = load_config()
|
||||||
|
|
||||||
class Ballot:
|
class Ballot:
|
||||||
"""Implements a ballot for ReVanced Polling API."""
|
"""Implements a ballot for ReVanced Polling API."""
|
||||||
|
|
||||||
redis = RedisConnector.connect(config['tokens']['database'])
|
redis = RedisConnector.connect(config['ballots']['database'])
|
||||||
|
|
||||||
BallotLogger = Logger.BallotLogger()
|
BallotLogger = Logger.BallotLogger()
|
||||||
|
|
||||||
async def store(self, discord_hashed_id: str, ballot: str) -> bool:
|
async def store(self, discord_hashed_id: str, ballot: BallotModel) -> bool:
|
||||||
"""Store a ballot.
|
"""Store a ballot.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -26,7 +27,12 @@ class Ballot:
|
|||||||
stored: bool = False
|
stored: bool = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.redis.set(name=discord_hashed_id, value=ballot, nx=True)
|
await self.redis.json().set(
|
||||||
|
name=discord_hashed_id,
|
||||||
|
path=".",
|
||||||
|
obj=ballot,
|
||||||
|
nx=True
|
||||||
|
)
|
||||||
await self.BallotLogger.log("STORE_BALLOT", None, discord_hashed_id)
|
await self.BallotLogger.log("STORE_BALLOT", None, discord_hashed_id)
|
||||||
stored = True
|
stored = True
|
||||||
except aioredis.RedisError as e:
|
except aioredis.RedisError as e:
|
||||||
@ -34,3 +40,24 @@ class Ballot:
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
return stored
|
return stored
|
||||||
|
|
||||||
|
async def exists(self, discord_hashed_id: str):
|
||||||
|
"""Check if the ballot exists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
discord_hashed_id (str): Discord hashed ID of the voter
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the ballot exists, False otherwise
|
||||||
|
"""
|
||||||
|
|
||||||
|
exists: bool = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if await self.redis.exists(discord_hashed_id):
|
||||||
|
exists = True
|
||||||
|
except aioredis.RedisError as e:
|
||||||
|
await self.BallotLogger.log("BALLOT_EXISTS", e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
return exists
|
||||||
|
@ -2,6 +2,7 @@ from redis import asyncio as aioredis
|
|||||||
import app.utils.Logger as Logger
|
import app.utils.Logger as Logger
|
||||||
from app.dependencies import load_config
|
from app.dependencies import load_config
|
||||||
from app.utils.RedisConnector import RedisConnector
|
from app.utils.RedisConnector import RedisConnector
|
||||||
|
import app.controllers.Ballot as Ballot
|
||||||
|
|
||||||
config: dict = load_config()
|
config: dict = load_config()
|
||||||
|
|
||||||
@ -10,6 +11,7 @@ class Clients:
|
|||||||
"""Implements a client for ReVanced Polling API."""
|
"""Implements a client for ReVanced Polling API."""
|
||||||
|
|
||||||
redis = RedisConnector.connect(config['tokens']['database'])
|
redis = RedisConnector.connect(config['tokens']['database'])
|
||||||
|
ballot = Ballot.Ballot()
|
||||||
|
|
||||||
UserLogger = Logger.UserLogger()
|
UserLogger = Logger.UserLogger()
|
||||||
|
|
||||||
@ -41,3 +43,46 @@ class Clients:
|
|||||||
|
|
||||||
return banned
|
return banned
|
||||||
|
|
||||||
|
async def is_token_banned(self, token: str) -> bool:
|
||||||
|
"""Check if the token is banned
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token (str): Token to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the token is banned, False otherwise
|
||||||
|
"""
|
||||||
|
|
||||||
|
banned: bool = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if await self.redis.exists(token):
|
||||||
|
banned = True
|
||||||
|
except aioredis.RedisError as e:
|
||||||
|
await self.UserLogger.log("IS_TOKEN_BANNED", e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
return banned
|
||||||
|
|
||||||
|
async def voted(self, token: str, discord_id: str) -> bool:
|
||||||
|
"""Check if the user already voted
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token (str): Token to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the user voted, False otherwise
|
||||||
|
"""
|
||||||
|
|
||||||
|
voted: bool = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if (await self.is_token_banned(token) or
|
||||||
|
await self.ballot.exists(discord_id)):
|
||||||
|
|
||||||
|
voted = True
|
||||||
|
except aioredis.RedisError as e:
|
||||||
|
await self.UserLogger.log("AUTH_CHECKS", e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
return voted
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from collections import deque
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
class BallotFields(BaseModel):
|
class BallotFields(BaseModel):
|
||||||
@ -9,4 +8,4 @@ class BallotFields(BaseModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
cid: str
|
cid: str
|
||||||
weight: int
|
vote: bool
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from app.models.BallotFields import BallotFields
|
||||||
|
|
||||||
class BallotModel(BaseModel):
|
class BallotModel(BaseModel):
|
||||||
"""Implements the fields for the ballots.
|
"""Implements the fields for the ballots.
|
||||||
@ -7,4 +8,4 @@ class BallotModel(BaseModel):
|
|||||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||||
"""
|
"""
|
||||||
|
|
||||||
vote: str
|
votes: list[BallotFields]
|
||||||
|
@ -10,35 +10,25 @@ class InternalServerError(BaseModel):
|
|||||||
error: str = "Internal Server Error"
|
error: str = "Internal Server Error"
|
||||||
message: str = "An internal server error occurred. Please try again later."
|
message: str = "An internal server error occurred. Please try again later."
|
||||||
|
|
||||||
class AnnouncementNotFound(BaseModel):
|
class Conflict(BaseModel):
|
||||||
"""Implements the response fields for when an item is not found.
|
"""Implements the response fields for when a conflict occurs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||||
"""
|
"""
|
||||||
|
|
||||||
error: str = "Not Found"
|
error: str = "Conflict"
|
||||||
message: str = "No announcement was found."
|
message: str = "User already voted on this ballot."
|
||||||
|
|
||||||
class ClientNotFound(BaseModel):
|
class PreconditionFailed(BaseModel):
|
||||||
"""Implements the response fields for when a client is not found.
|
"""Implements the response fields for when a precondition fails.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||||
"""
|
"""
|
||||||
|
|
||||||
error: str = "Not Found"
|
error: str = "Precondition Failed"
|
||||||
message: str = "No client matches the given ID"
|
message: str = "User is not eligible to vote on this ballot."
|
||||||
|
|
||||||
class IdNotProvided(BaseModel):
|
|
||||||
"""Implements the response fields for when the id is not provided.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
|
||||||
"""
|
|
||||||
|
|
||||||
error: str = "Bad Request"
|
|
||||||
message: str = "Missing client id"
|
|
||||||
|
|
||||||
class Unauthorized(BaseModel):
|
class Unauthorized(BaseModel):
|
||||||
"""Implements the response fields for when the client is unauthorized.
|
"""Implements the response fields for when the client is unauthorized.
|
||||||
@ -50,22 +40,3 @@ class Unauthorized(BaseModel):
|
|||||||
error: str = "Unauthorized"
|
error: str = "Unauthorized"
|
||||||
message: str = "The client is unauthorized to access this resource"
|
message: str = "The client is unauthorized to access this resource"
|
||||||
|
|
||||||
class MirrorNotFoundError(BaseModel):
|
|
||||||
"""Implements the response fields for when a mirror is not found.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
|
||||||
"""
|
|
||||||
|
|
||||||
error: str = "Not Found"
|
|
||||||
message: str = "No mirror was found for the organization, repository, and version provided."
|
|
||||||
|
|
||||||
class MirrorAlreadyExistsError(BaseModel):
|
|
||||||
"""Implements the response fields for when a mirror already exists.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
|
||||||
"""
|
|
||||||
|
|
||||||
error: str = "Conflict"
|
|
||||||
message: str = "A mirror already exists for the organization, repository, and version provided. Please use the PUT method to update the mirror."
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import os
|
import os
|
||||||
|
import hmac
|
||||||
from fastapi_paseto_auth import AuthPASETO
|
from fastapi_paseto_auth import AuthPASETO
|
||||||
from fastapi import APIRouter, Request, Response, Depends, status, HTTPException, Header
|
from fastapi import APIRouter, Request, Response, Depends, status, HTTPException, Header
|
||||||
from app.dependencies import load_config
|
from app.dependencies import load_config
|
||||||
from app.controllers.Clients import Clients
|
from app.controllers.Clients import Clients
|
||||||
|
from app.controllers.Ballot import Ballot
|
||||||
import app.models.ClientModels as ClientModels
|
import app.models.ClientModels as ClientModels
|
||||||
import app.models.GeneralErrors as GeneralErrors
|
import app.models.GeneralErrors as GeneralErrors
|
||||||
import app.models.ResponseModels as ResponseModels
|
import app.models.ResponseModels as ResponseModels
|
||||||
@ -12,6 +14,7 @@ router = APIRouter(
|
|||||||
tags=['Authentication']
|
tags=['Authentication']
|
||||||
)
|
)
|
||||||
clients = Clients()
|
clients = Clients()
|
||||||
|
ballot = Ballot()
|
||||||
config: dict = load_config()
|
config: dict = load_config()
|
||||||
|
|
||||||
@router.post('/', response_model=ResponseModels.ClientAuthTokenResponse, status_code=status.HTTP_200_OK)
|
@router.post('/', response_model=ResponseModels.ClientAuthTokenResponse, status_code=status.HTTP_200_OK)
|
||||||
@ -22,7 +25,11 @@ async def auth(request: Request, response: Response, client: ClientModels.Client
|
|||||||
access_token: auth token
|
access_token: auth token
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if client.id == os.environ['CLIENT_ID'] and client.secret == os.environ['CLIENT_SECRET']:
|
if(
|
||||||
|
hmac.compare_digest(client.id, os.environ['CLIENT_ID']) and
|
||||||
|
hmac.compare_digest(client.secret, os.environ['CLIENT_SECRET'])
|
||||||
|
):
|
||||||
|
|
||||||
authenticated: bool = True
|
authenticated: bool = True
|
||||||
|
|
||||||
if not authenticated:
|
if not authenticated:
|
||||||
@ -32,13 +39,19 @@ async def auth(request: Request, response: Response, client: ClientModels.Client
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
if not ballot.exists(client.discord_id_hash):
|
||||||
user_claims: dict[str, str] = {}
|
user_claims: dict[str, str] = {}
|
||||||
user_claims['discord_id_hash'] = client.discord_id_hash
|
user_claims['discord_id_hash'] = client.discord_id_hash
|
||||||
access_token = Authorize.create_access_token(subject=client.id,
|
access_token = Authorize.create_access_token(subject=client.id,
|
||||||
user_claims=user_claims,
|
user_claims=user_claims,
|
||||||
fresh=True)
|
fresh=True)
|
||||||
|
|
||||||
return {"access_token": access_token}
|
return {"access_token": access_token}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=412, detail={
|
||||||
|
"error": GeneralErrors.PreconditionFailed().error,
|
||||||
|
"message": GeneralErrors.PreconditionFailed().message
|
||||||
|
}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=401, detail={
|
raise HTTPException(status_code=401, detail={
|
||||||
"error": GeneralErrors.Unauthorized().error,
|
"error": GeneralErrors.Unauthorized().error,
|
||||||
@ -46,6 +59,38 @@ async def auth(request: Request, response: Response, client: ClientModels.Client
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@router.put("/exchange", response_model=ResponseModels.ClientAuthTokenResponse, status_code=status.HTTP_200_OK)
|
||||||
|
async def exchange_token(request: Request, response: Response, Authorize: AuthPASETO = Depends(), Authorization: str = Header(None)) -> dict:
|
||||||
|
"""Exchange a token for a new one.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
access_token: auth token
|
||||||
|
|
||||||
|
"""
|
||||||
|
Authorize.paseto_required()
|
||||||
|
|
||||||
|
user_claims: dict[str, str | bool] = {}
|
||||||
|
user_claims['discord_id_hash'] = Authorize.get_user_claims()['discord_id_hash']
|
||||||
|
user_claims['is_exchange_token'] = True
|
||||||
|
access_token = Authorize.create_access_token(subject=Authorize.get_subject(),
|
||||||
|
user_claims=user_claims,
|
||||||
|
fresh=True)
|
||||||
|
if not ballot.exists(Authorize.get_subject()):
|
||||||
|
if await clients.ban_token(Authorize.get_jti()):
|
||||||
|
return {"access_token": access_token}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=500, detail={
|
||||||
|
"error": GeneralErrors.InternalServerError().error,
|
||||||
|
"message": GeneralErrors.InternalServerError().message
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=412, detail={
|
||||||
|
"error": GeneralErrors.PreconditionFailed().error,
|
||||||
|
"message": GeneralErrors.PreconditionFailed().message
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@router.delete("/revoke", response_model=ResponseModels.RevokedTokenResponse, status_code=status.HTTP_200_OK)
|
@router.delete("/revoke", response_model=ResponseModels.RevokedTokenResponse, status_code=status.HTTP_200_OK)
|
||||||
async def revoke_token(request: Request, response: Response, Authorize: AuthPASETO = Depends(), Authorization: str = Header(None)) -> dict:
|
async def revoke_token(request: Request, response: Response, Authorize: AuthPASETO = Depends(), Authorization: str = Header(None)) -> dict:
|
||||||
"""Revoke a token.
|
"""Revoke a token.
|
||||||
@ -64,3 +109,4 @@ async def revoke_token(request: Request, response: Response, Authorize: AuthPASE
|
|||||||
"message": GeneralErrors.InternalServerError().message
|
"message": GeneralErrors.InternalServerError().message
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,11 +5,14 @@ from app.models.BallotModel import BallotModel
|
|||||||
import app.models.GeneralErrors as GeneralErrors
|
import app.models.GeneralErrors as GeneralErrors
|
||||||
import app.models.ResponseModels as ResponseModels
|
import app.models.ResponseModels as ResponseModels
|
||||||
import app.controllers.Ballot as Ballot
|
import app.controllers.Ballot as Ballot
|
||||||
|
import app.controllers.Clients as Clients
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
ballot_controller = Ballot.Ballot()
|
ballot_controller = Ballot.Ballot()
|
||||||
|
|
||||||
|
client = Clients.Clients()
|
||||||
|
|
||||||
config: dict = load_config()
|
config: dict = load_config()
|
||||||
|
|
||||||
@router.post('/ballot', response_model=ResponseModels.BallotCastedResponse,
|
@router.post('/ballot', response_model=ResponseModels.BallotCastedResponse,
|
||||||
@ -24,11 +27,20 @@ async def cast_ballot(request: Request, response: Response,
|
|||||||
"""
|
"""
|
||||||
Authorize.paseto_required()
|
Authorize.paseto_required()
|
||||||
|
|
||||||
discord_hashed_id: str = Authorize.get_paseto_claims()['discord_hashed_id']
|
|
||||||
|
|
||||||
stored: bool = await ballot_controller.store(discord_hashed_id, ballot.vote)
|
if (Authorize.get_paseto_claims()['is_exchange_token'] and
|
||||||
|
not client.voted(
|
||||||
|
Authorize.get_jti(),
|
||||||
|
Authorize.get_paseto_claims()['discord_hashed_id']
|
||||||
|
)):
|
||||||
|
|
||||||
|
stored: bool = await ballot_controller.store(
|
||||||
|
Authorize.get_paseto_claims()['discord_hashed_id'],
|
||||||
|
ballot
|
||||||
|
)
|
||||||
|
|
||||||
if stored:
|
if stored:
|
||||||
|
await client.ban_token(Authorize.get_jti())
|
||||||
return {"created": stored}
|
return {"created": stored}
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=500, detail={
|
raise HTTPException(status_code=500, detail={
|
||||||
@ -36,3 +48,9 @@ async def cast_ballot(request: Request, response: Response,
|
|||||||
"message": GeneralErrors.InternalServerError().message
|
"message": GeneralErrors.InternalServerError().message
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=401, detail={
|
||||||
|
"error": GeneralErrors.Unauthorized().error,
|
||||||
|
"message": GeneralErrors.Unauthorized().message
|
||||||
|
}
|
||||||
|
)
|
||||||
|
775
poetry.lock
generated
775
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -28,8 +28,8 @@ uvicorn = ">=0.18.3"
|
|||||||
gunicorn = ">=20.1.0"
|
gunicorn = ">=20.1.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
mypy = ">=0.971"
|
mypy = ">=0.991"
|
||||||
types-toml = ">=0.10.8"
|
types-toml = ">=0.10.8.1"
|
||||||
types-redis = ">=4.3.21.1"
|
types-redis = ">=4.3.21.1"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
@ -4,60 +4,59 @@ anyio==3.6.2 ; python_version >= "3.10" and python_version < "4.0"
|
|||||||
argon2-cffi-bindings==21.2.0 ; python_version >= "3.10" and python_version < "4.0"
|
argon2-cffi-bindings==21.2.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
argon2-cffi==21.3.0 ; python_version >= "3.10" and python_version < "4.0"
|
argon2-cffi==21.3.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
async-timeout==4.0.2 ; python_version >= "3.10" and python_version < "4.0"
|
async-timeout==4.0.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
attrs==21.4.0 ; python_version >= "3.10" and python_version < "4.0"
|
attrs==22.2.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
certifi==2022.9.24 ; python_version >= "3.10" and python_version < "4.0"
|
certifi==2022.12.7 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
cffi==1.15.1 ; python_version >= "3.10" and python_version < "4.0"
|
cffi==1.15.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
click==8.1.3 ; python_version >= "3.10" and python_version < "4.0"
|
click==8.1.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32" or python_version >= "3.10" and python_version < "4.0" and platform_system == "Windows"
|
colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32" or python_version >= "3.10" and python_version < "4.0" and platform_system == "Windows"
|
||||||
cryptography==37.0.4 ; python_version >= "3.10" and python_version < "4.0"
|
cryptography==37.0.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
cytoolz==0.12.0 ; python_version >= "3.10" and python_version < "4.0"
|
cytoolz==0.12.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
deprecated==1.2.13 ; python_version >= "3.10" and python_version < "4.0"
|
deprecated==1.2.13 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
fastapi-cache2==0.1.9 ; python_version >= "3.10" and python_version < "4.0"
|
fastapi-cache2==0.1.9 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
fastapi-paseto-auth==0.6.0 ; python_version >= "3.10" and python_version < "4.0"
|
fastapi-paseto-auth==0.6.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
fastapi==0.85.0 ; python_version >= "3.10" and python_version < "4.0"
|
fastapi==0.85.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
fasteners==0.17.3 ; python_version >= "3.10" and python_version < "4.0"
|
fasteners==0.17.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
gunicorn==20.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
gunicorn==20.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
h11==0.12.0 ; python_version >= "3.10" and python_version < "4.0"
|
h11==0.14.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
h2==4.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
h2==4.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
hiredis==2.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
hiredis==2.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
hpack==4.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
hpack==4.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
httpcore==0.15.0 ; python_version >= "3.10" and python_version < "4.0"
|
httpcore==0.16.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
httpx-cache==0.6.1 ; python_version >= "3.10" and python_version < "4.0"
|
httpx-cache==0.7.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
httpx==0.23.0 ; python_version >= "3.10" and python_version < "4.0"
|
httpx==0.23.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
httpx[http2]==0.23.0 ; python_version >= "3.10" and python_version < "4.0"
|
httpx[http2]==0.23.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
hypercorn[uvloop]==0.14.3 ; python_version >= "3.10" and python_version < "4.0"
|
hypercorn[uvloop]==0.14.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
hyperframe==6.0.1 ; python_version >= "3.10" and python_version < "4.0"
|
hyperframe==6.0.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
idna==3.4 ; python_version >= "3.10" and python_version < "4.0"
|
idna==3.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
iso8601==1.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
iso8601==1.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
limits==1.6 ; python_version >= "3.10" and python_version < "4.0"
|
limits==2.8.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
loguru==0.6.0 ; python_version >= "3.10" and python_version < "4.0"
|
loguru==0.6.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
msgpack==1.0.4 ; python_version >= "3.10" and python_version < "4.0"
|
msgpack==1.0.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
orjson==3.8.1 ; python_version >= "3.10" and python_version < "4.0"
|
orjson==3.8.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
packaging==21.3 ; python_version >= "3.10" and python_version < "4.0"
|
packaging==22.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
passlib[argon2]==1.7.4 ; python_version >= "3.10" and python_version < "4.0"
|
passlib[argon2]==1.7.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
pendulum==2.1.2 ; python_version >= "3.10" and python_version < "4.0"
|
pendulum==2.1.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
priority==2.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
priority==2.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
pycparser==2.21 ; python_version >= "3.10" and python_version < "4.0"
|
pycparser==2.21 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
pycryptodomex==3.15.0 ; python_version >= "3.10" and python_version < "4.0"
|
pycryptodomex==3.16.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
pydantic==1.10.2 ; python_version >= "3.10" and python_version < "4.0"
|
pydantic==1.10.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
pyparsing==3.0.9 ; python_version >= "3.10" and python_version < "4.0"
|
|
||||||
pyseto==1.6.10 ; python_version >= "3.10" and python_version < "4.0"
|
pyseto==1.6.10 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0"
|
python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
pytzdata==2020.1 ; python_version >= "3.10" and python_version < "4.0"
|
pytzdata==2020.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
redis==4.3.4 ; python_version >= "3.10" and python_version < "4.0"
|
redis==4.4.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
rfc3986[idna2008]==1.5.0 ; python_version >= "3.10" and python_version < "4.0"
|
rfc3986[idna2008]==1.5.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
sentry-sdk==1.11.0 ; python_version >= "3.10" and python_version < "4.0"
|
sentry-sdk==1.12.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
setuptools==65.5.1 ; python_version >= "3.10" and python_version < "4.0"
|
setuptools==65.6.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
six==1.16.0 ; python_version >= "3.10" and python_version < "4.0"
|
six==1.16.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
slowapi==0.1.6 ; python_version >= "3.10" and python_version < "4.0"
|
slowapi==0.1.7 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
sniffio==1.3.0 ; python_version >= "3.10" and python_version < "4.0"
|
sniffio==1.3.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
starlette==0.20.4 ; python_version >= "3.10" and python_version < "4.0"
|
starlette==0.20.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
toml==0.10.2 ; python_version >= "3.10" and python_version < "4.0"
|
toml==0.10.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
toolz==0.12.0 ; python_version >= "3.10" and python_version < "4.0"
|
toolz==0.12.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
typing-extensions==4.4.0 ; python_version >= "3.10" and python_version < "4.0"
|
typing-extensions==4.4.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
ujson==5.5.0 ; python_version >= "3.10" and python_version < "4.0"
|
ujson==5.6.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
urllib3==1.26.12 ; python_version >= "3.10" and python_version < "4"
|
urllib3==1.26.13 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
uvicorn==0.19.0 ; python_version >= "3.10" and python_version < "4.0"
|
uvicorn==0.20.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
uvloop==0.17.0 ; platform_system != "Windows" and python_version >= "3.10" and python_version < "4.0"
|
uvloop==0.17.0 ; platform_system != "Windows" and python_version >= "3.10" and python_version < "4.0"
|
||||||
win32-setctime==1.1.0 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32"
|
win32-setctime==1.1.0 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32"
|
||||||
wrapt==1.14.1 ; python_version >= "3.10" and python_version < "4.0"
|
wrapt==1.14.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user