mirror of
https://github.com/revanced/revanced-polling-api.git
synced 2025-04-29 14:14:26 +02:00
INITIAL COMMIT
This commit is contained in:
parent
333b6b3758
commit
d4e43feae3
31
.gitignore
vendored
31
.gitignore
vendored
@ -20,7 +20,6 @@ parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
@ -50,6 +49,7 @@ coverage.xml
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
@ -72,6 +72,7 @@ instance/
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
@ -82,7 +83,9 @@ profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
@ -91,6 +94,13 @@ ipython_config.py
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
@ -127,3 +137,20 @@ dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# PROJECT SPECIFIC
|
||||
setup_env.sh
|
||||
admin_info.json
|
||||
|
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
13
app/controllers/Auth.py
Normal file
13
app/controllers/Auth.py
Normal file
@ -0,0 +1,13 @@
|
||||
from datetime import timedelta
|
||||
import os
|
||||
import toml
|
||||
from datetime import timedelta
|
||||
from pydantic import BaseModel
|
||||
from fastapi_paseto_auth import AuthPASETO
|
||||
|
||||
config: dict = toml.load("config.toml")
|
||||
|
||||
class PasetoSettings(BaseModel):
|
||||
authpaseto_secret_key: str = os.environ['SECRET_KEY']
|
||||
authpaseto_access_token_expires: int | bool = config['auth']['access_token_expires']
|
||||
authpaseto_denylist_enabled: bool = True
|
47
app/controllers/Clients.py
Normal file
47
app/controllers/Clients.py
Normal file
@ -0,0 +1,47 @@
|
||||
from time import sleep
|
||||
from redis import asyncio as aioredis
|
||||
import uvloop
|
||||
|
||||
|
||||
import app.utils.Logger as Logger
|
||||
from app.dependencies import load_config
|
||||
from app.utils.RedisConnector import RedisConnector
|
||||
|
||||
config: dict = load_config()
|
||||
|
||||
class Clients:
|
||||
|
||||
"""Implements a client for ReVanced Polling API."""
|
||||
|
||||
redis = RedisConnector.connect(config['tokens']['database'])
|
||||
|
||||
UserLogger = Logger.UserLogger()
|
||||
|
||||
async def ban_token(self, token: str) -> bool:
|
||||
"""Ban a token
|
||||
|
||||
Args:
|
||||
token (str): Token to ban
|
||||
|
||||
Returns:
|
||||
bool: True if the token was banned successfully, False otherwise
|
||||
"""
|
||||
|
||||
banned: bool = False
|
||||
|
||||
try:
|
||||
if type(config['auth']['access_token_expires']) is bool:
|
||||
await self.redis.set(name=token, value="", nx=True)
|
||||
else:
|
||||
await self.redis.set(name=token,
|
||||
value="",
|
||||
nx=True,
|
||||
ex=config['auth']['access_token_expires'])
|
||||
await self.UserLogger.log("BAN_TOKEN", None, token)
|
||||
banned = True
|
||||
except aioredis.RedisError as e:
|
||||
await self.UserLogger.log("BAN_TOKEN", e)
|
||||
raise e
|
||||
|
||||
return banned
|
||||
|
0
app/controllers/__init__.py
Normal file
0
app/controllers/__init__.py
Normal file
9
app/dependencies.py
Normal file
9
app/dependencies.py
Normal file
@ -0,0 +1,9 @@
|
||||
import toml
|
||||
|
||||
def load_config() -> dict:
|
||||
"""Loads the config.toml file.
|
||||
|
||||
Returns:
|
||||
dict: the config.toml file as a dict
|
||||
"""
|
||||
return toml.load("config.toml")
|
142
app/main.py
Normal file
142
app/main.py
Normal file
@ -0,0 +1,142 @@
|
||||
import os
|
||||
import toml
|
||||
import binascii
|
||||
from redis import Redis
|
||||
|
||||
from fastapi import FastAPI, Request, status
|
||||
from fastapi.responses import JSONResponse, UJSONResponse
|
||||
|
||||
from slowapi.util import get_remote_address
|
||||
from slowapi.middleware import SlowAPIMiddleware
|
||||
from slowapi import Limiter, _rate_limit_exceeded_handler
|
||||
|
||||
from fastapi_cache import FastAPICache
|
||||
from fastapi_cache.decorator import cache
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from fastapi_cache.backends.redis import RedisBackend
|
||||
|
||||
from fastapi_paseto_auth import AuthPASETO
|
||||
from fastapi_paseto_auth.exceptions import AuthPASETOException
|
||||
|
||||
import app.controllers.Auth as Auth
|
||||
|
||||
from app.dependencies import load_config
|
||||
|
||||
from app.utils.RedisConnector import RedisConnector
|
||||
|
||||
import app.models.GeneralErrors as GeneralErrors
|
||||
|
||||
from app.routers import root
|
||||
from app.routers import auth
|
||||
|
||||
"""Implements an API for our polling app"""
|
||||
|
||||
# Load config
|
||||
|
||||
config: dict = load_config()
|
||||
|
||||
# Create FastAPI instance
|
||||
|
||||
app = FastAPI(title=config['docs']['title'],
|
||||
description=config['docs']['description'],
|
||||
version=config['docs']['version'],
|
||||
license_info={"name": config['license']['name'],
|
||||
"url": config['license']['url']
|
||||
},
|
||||
default_response_class=UJSONResponse
|
||||
)
|
||||
|
||||
# Hook up rate limiter
|
||||
limiter = Limiter(key_func=get_remote_address,
|
||||
default_limits=[
|
||||
config['slowapi']['limit']
|
||||
],
|
||||
headers_enabled=True,
|
||||
storage_uri=f"redis://{os.environ['REDIS_URL']}:{os.environ['REDIS_PORT']}/{config['slowapi']['database']}"
|
||||
)
|
||||
app.state.limiter = limiter
|
||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||
app.add_middleware(SlowAPIMiddleware)
|
||||
|
||||
# Setup routes
|
||||
|
||||
app.include_router(root.router)
|
||||
app.include_router(auth.router)
|
||||
|
||||
# Setup cache
|
||||
|
||||
@cache()
|
||||
async def get_cache() -> int:
|
||||
"""Get cache TTL from config.
|
||||
Returns:
|
||||
int: Cache TTL
|
||||
"""
|
||||
return 1
|
||||
|
||||
# Setup PASETO
|
||||
|
||||
@AuthPASETO.load_config
|
||||
def get_config() -> Auth.PasetoSettings:
|
||||
"""Get PASETO config from Auth module
|
||||
Returns:
|
||||
PasetoSettings: PASETO config
|
||||
"""
|
||||
return Auth.PasetoSettings()
|
||||
|
||||
@AuthPASETO.token_in_denylist_loader
|
||||
def check_if_token_in_denylist(decrypted_token):
|
||||
redis = Redis(host=os.environ['REDIS_URL'],
|
||||
port=os.environ['REDIS_PORT'],
|
||||
db=config['tokens']['database'],
|
||||
decode_responses=True)
|
||||
|
||||
return redis.exists(decrypted_token["jti"])
|
||||
|
||||
# Setup custom error handlers
|
||||
|
||||
@app.exception_handler(AuthPASETOException)
|
||||
async def authpaseto_exception_handler(request: Request, exc: AuthPASETOException) -> JSONResponse:
|
||||
"""Handle AuthPASETOException
|
||||
Args:
|
||||
request (Request): Request
|
||||
exc (AuthPASETOException): Exception
|
||||
Returns:
|
||||
JSONResponse: Response
|
||||
"""
|
||||
return JSONResponse(status_code=exc.status_code, content={"detail": exc.message})
|
||||
|
||||
@app.exception_handler(AttributeError)
|
||||
async def validation_exception_handler(request, exc) -> JSONResponse:
|
||||
"""Handle AttributeError
|
||||
Args:
|
||||
request (Request): Request
|
||||
exc (AttributeError): Exception
|
||||
Returns:
|
||||
JSONResponse: Response
|
||||
"""
|
||||
return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content={
|
||||
"error": "Unprocessable Entity"
|
||||
})
|
||||
|
||||
@app.exception_handler(binascii.Error)
|
||||
async def invalid_token_exception_handler(request, exc) -> JSONResponse:
|
||||
"""Handle binascii.Error
|
||||
Args:
|
||||
request (Request): Request
|
||||
exc (binascii.Error): Exception
|
||||
Returns:
|
||||
JSONResponse: Response
|
||||
"""
|
||||
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
})
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup() -> None:
|
||||
"""Startup event handler"""
|
||||
|
||||
FastAPICache.init(RedisBackend(RedisConnector.connect(config['cache']['database'])),
|
||||
prefix="fastapi-cache")
|
||||
|
||||
return None
|
12
app/models/BallotFields.py
Normal file
12
app/models/BallotFields.py
Normal file
@ -0,0 +1,12 @@
|
||||
from collections import deque
|
||||
from pydantic import BaseModel
|
||||
|
||||
class BallotFields(BaseModel):
|
||||
"""Implements the fields for the ballots.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
cid: str
|
||||
weight: int
|
12
app/models/BallotModel.py
Normal file
12
app/models/BallotModel.py
Normal file
@ -0,0 +1,12 @@
|
||||
from pydantic import BaseModel
|
||||
from app.models.BallotFields import BallotFields
|
||||
|
||||
class BallotModel(BaseModel):
|
||||
"""Implements the fields for the ballots.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
discord_id_hash: str
|
||||
ballot: list[BallotFields]
|
23
app/models/ClientModels.py
Normal file
23
app/models/ClientModels.py
Normal file
@ -0,0 +1,23 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ClientModel(BaseModel):
|
||||
"""Implements the fields for the clients.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
id: str
|
||||
secret: str
|
||||
discord_id_hash: str
|
||||
|
||||
class ClientAuthModel(BaseModel):
|
||||
"""Implements the fields for client authentication.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
id: str
|
||||
secret: str
|
||||
discord_id_hash: str
|
71
app/models/GeneralErrors.py
Normal file
71
app/models/GeneralErrors.py
Normal file
@ -0,0 +1,71 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class InternalServerError(BaseModel):
|
||||
"""Implements the response fields for when an internal server error occurs.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
error: str = "Internal Server Error"
|
||||
message: str = "An internal server error occurred. Please try again later."
|
||||
|
||||
class AnnouncementNotFound(BaseModel):
|
||||
"""Implements the response fields for when an item is not found.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
error: str = "Not Found"
|
||||
message: str = "No announcement was found."
|
||||
|
||||
class ClientNotFound(BaseModel):
|
||||
"""Implements the response fields for when a client is not found.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
error: str = "Not Found"
|
||||
message: str = "No client matches the given ID"
|
||||
|
||||
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):
|
||||
"""Implements the response fields for when the client is unauthorized.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
error: str = "Unauthorized"
|
||||
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."
|
77
app/models/ResponseFields.py
Normal file
77
app/models/ResponseFields.py
Normal file
@ -0,0 +1,77 @@
|
||||
from typing import Any
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ToolsResponseFields(BaseModel):
|
||||
"""Implements the fields for the /tools endpoint.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
repository: str
|
||||
version: str
|
||||
timestamp: str
|
||||
name: str
|
||||
size: str | None = None
|
||||
browser_download_url: str
|
||||
content_type: str
|
||||
class CompatiblePackagesResponseFields(BaseModel):
|
||||
"""Implements the fields for compatible packages in the PatchesResponseFields class.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
name: str
|
||||
versions: list[ str ] | None
|
||||
|
||||
class PatchesOptionsResponseFields(BaseModel):
|
||||
key: str
|
||||
title: str
|
||||
description: str
|
||||
required: bool
|
||||
choices: list[ Any ] | None
|
||||
|
||||
class PatchesResponseFields(BaseModel):
|
||||
"""Implements the fields for the /patches endpoint.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
name: str
|
||||
description: str
|
||||
version: str
|
||||
excluded: bool
|
||||
deprecated: bool
|
||||
dependencies: list[ str ] | None
|
||||
options: list[ PatchesOptionsResponseFields ] | None
|
||||
compatiblePackages: list[ CompatiblePackagesResponseFields ]
|
||||
|
||||
class ContributorFields(BaseModel):
|
||||
"""Implements the fields for each contributor in the /contributors endpoint.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
login: str
|
||||
avatar_url: str
|
||||
html_url: str
|
||||
|
||||
class ContributorsResponseFields(BaseModel):
|
||||
"""Implements the fields for each repository in the /contributors endpoint
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
name: str
|
||||
contributors: list[ ContributorFields ]
|
||||
|
||||
class ChangelogsResponseFields(BaseModel):
|
||||
"""Implements the fields for the /changelogs endpoint.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
sha: str
|
||||
author: str
|
||||
message: str
|
||||
html_url: str
|
109
app/models/ResponseModels.py
Normal file
109
app/models/ResponseModels.py
Normal file
@ -0,0 +1,109 @@
|
||||
from pydantic import BaseModel
|
||||
import app.models.ResponseFields as ResponseFields
|
||||
|
||||
"""Implements pydantic models and model generator for the API's responses."""
|
||||
|
||||
class ToolsResponseModel(BaseModel):
|
||||
"""Implements the JSON response model for the /tools endpoint.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
tools: list[ ResponseFields.ToolsResponseFields ]
|
||||
|
||||
class PatchesResponseModel(BaseModel):
|
||||
"""Implements the JSON response model for the /patches endpoint.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
__root__: list[ ResponseFields.PatchesResponseFields ]
|
||||
|
||||
class ContributorsResponseModel(BaseModel):
|
||||
"""Implements the JSON response model for the /contributors endpoint.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
repositories: list[ ResponseFields.ContributorsResponseFields ]
|
||||
|
||||
class PingResponseModel(BaseModel):
|
||||
"""Implements the JSON response model for the /heartbeat endpoint.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
status: int
|
||||
detail: str
|
||||
|
||||
class ClientDeletedResponse(BaseModel):
|
||||
"""Implements the response fields for deleted clients.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
id: str
|
||||
deleted: bool
|
||||
|
||||
class ClientSecretUpdatedResponse(BaseModel):
|
||||
"""Implements the response fields for updated client secrets.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
id: str
|
||||
secret: str
|
||||
|
||||
class ClientAuthTokenResponse(BaseModel):
|
||||
"""Implements the response fields for client auth tokens.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
access_token: str
|
||||
|
||||
class ClientTokenRefreshResponse(BaseModel):
|
||||
"""Implements the response fields for client token refresh.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
access_token: str
|
||||
|
||||
class ClientStatusResponse(BaseModel):
|
||||
"""Implements the response fields for client status.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
id: str
|
||||
active: bool
|
||||
|
||||
class ChangelogsResponseModel(BaseModel):
|
||||
"""Implements the JSON response model for the /changelogs endpoint.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
repository: str
|
||||
path: str
|
||||
commits: list[ ResponseFields.ChangelogsResponseFields ]
|
||||
|
||||
class RevokedTokenResponse(BaseModel):
|
||||
"""Implements the response fields for token invalidation.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
revoked: bool
|
0
app/models/__init__.py
Normal file
0
app/models/__init__.py
Normal file
0
app/routers/__init__.py
Normal file
0
app/routers/__init__.py
Normal file
66
app/routers/auth.py
Normal file
66
app/routers/auth.py
Normal file
@ -0,0 +1,66 @@
|
||||
import os
|
||||
from fastapi_paseto_auth import AuthPASETO
|
||||
from fastapi import APIRouter, Request, Response, Depends, status, HTTPException, Header
|
||||
from app.dependencies import load_config
|
||||
from app.controllers.Clients import Clients
|
||||
import app.models.ClientModels as ClientModels
|
||||
import app.models.GeneralErrors as GeneralErrors
|
||||
import app.models.ResponseModels as ResponseModels
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/auth",
|
||||
tags=['Authentication']
|
||||
)
|
||||
clients = Clients()
|
||||
config: dict = load_config()
|
||||
|
||||
@router.post('/', response_model=ResponseModels.ClientAuthTokenResponse, status_code=status.HTTP_200_OK)
|
||||
async def auth(request: Request, response: Response, client: ClientModels.ClientAuthModel, Authorize: AuthPASETO = Depends()) -> dict:
|
||||
"""Authenticate a client and get an auth token.
|
||||
|
||||
Returns:
|
||||
access_token: auth token
|
||||
"""
|
||||
|
||||
if client.id == os.environ['CLIENT_ID'] and client.secret == os.environ['CLIENT_SECRET']:
|
||||
authenticated: bool = True
|
||||
|
||||
if not authenticated:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
user_claims: dict[str, str] = {}
|
||||
user_claims['discord_id_hash'] = client.discord_id_hash
|
||||
access_token = Authorize.create_access_token(subject=client.id,
|
||||
user_claims=user_claims,
|
||||
fresh=True)
|
||||
|
||||
return {"access_token": access_token}
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
|
||||
@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:
|
||||
"""Revoke a token.
|
||||
|
||||
Returns:
|
||||
revoked: bool
|
||||
|
||||
"""
|
||||
Authorize.paseto_required()
|
||||
|
||||
if await clients.ban_token(Authorize.get_jti()):
|
||||
return {"revoked": True}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail={
|
||||
"error": GeneralErrors.InternalServerError().error,
|
||||
"message": GeneralErrors.InternalServerError().message
|
||||
}
|
||||
)
|
0
app/routers/ballot.py
Normal file
0
app/routers/ballot.py
Normal file
14
app/routers/root.py
Normal file
14
app/routers/root.py
Normal file
@ -0,0 +1,14 @@
|
||||
from fastapi import APIRouter, Request, Response, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_class=RedirectResponse,
|
||||
status_code=status.HTTP_301_MOVED_PERMANENTLY, tags=['Root'])
|
||||
async def root(request: Request, response: Response) -> RedirectResponse:
|
||||
"""Brings up API documentation
|
||||
|
||||
Returns:
|
||||
None: Redirects to /docs
|
||||
"""
|
||||
return RedirectResponse(url="/docs")
|
30
app/utils/Generators.py
Normal file
30
app/utils/Generators.py
Normal file
@ -0,0 +1,30 @@
|
||||
import time
|
||||
import uuid
|
||||
import secrets
|
||||
|
||||
class Generators:
|
||||
"""Generates UUIDs and secrets"""
|
||||
|
||||
async def generate_secret(self) -> str:
|
||||
"""Generate a random secret
|
||||
|
||||
Returns:
|
||||
str: A random secret
|
||||
"""
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
async def generate_id(self) -> str:
|
||||
"""Generate a random UUID
|
||||
|
||||
Returns:
|
||||
str: A random UUID (str instead of UUID object)
|
||||
"""
|
||||
return str(uuid.uuid4())
|
||||
|
||||
async def generate_timestamp(self) -> int:
|
||||
"""Generate a timestamp
|
||||
|
||||
Returns:
|
||||
int: A timestamp
|
||||
"""
|
||||
return int(time.time())
|
78
app/utils/Logger.py
Normal file
78
app/utils/Logger.py
Normal file
@ -0,0 +1,78 @@
|
||||
from loguru import logger
|
||||
from redis import RedisError
|
||||
from argon2.exceptions import VerifyMismatchError
|
||||
|
||||
class HTTPXLogger():
|
||||
"""Logger adapter for HTTPX."""
|
||||
|
||||
async def log_request(self, request) -> None:
|
||||
"""Logs HTTPX requests
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
logger.info(f"[HTTPX] Request: {request.method} {request.url} - Waiting for response")
|
||||
|
||||
async def log_response(self, response) -> None:
|
||||
"""Logs HTTPX responses
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
request = response.request
|
||||
|
||||
logger.info(f"[HTTPX] Response: {request.method} {request.url} - Status: {response.status_code} {response.reason_phrase}")
|
||||
|
||||
class InternalCacheLogger:
|
||||
async def log(self, operation: str, result: RedisError | None = None, key: str = "",) -> None:
|
||||
"""Logs internal cache operations
|
||||
|
||||
Args:
|
||||
operation (str): Operation name
|
||||
key (str): Key used in the operation
|
||||
"""
|
||||
if type(result) is RedisError:
|
||||
logger.error(f"[InternalCache] REDIS {operation} - Failed with error: {result}")
|
||||
else:
|
||||
logger.info(f"[InternalCache] REDIS {operation} {key} - OK")
|
||||
|
||||
class UserLogger:
|
||||
async def log(self, operation: str, result: RedisError | VerifyMismatchError | None = None,
|
||||
key: str = "",) -> None:
|
||||
"""Logs internal cache operations
|
||||
|
||||
Args:
|
||||
operation (str): Operation name
|
||||
key (str): Key used in the operation
|
||||
"""
|
||||
if type(result) is RedisError:
|
||||
logger.error(f"[User] REDIS {operation} - Failed with error: {result}")
|
||||
else:
|
||||
logger.info(f"[User] REDIS {operation} {key} - OK")
|
||||
|
||||
class AnnouncementsLogger:
|
||||
async def log(self, operation: str, result: RedisError | None = None, key: str = "") -> None:
|
||||
"""Logs internal cache operations
|
||||
|
||||
Args:
|
||||
operation (str): Operation name
|
||||
key (str): Key used in the operation
|
||||
"""
|
||||
if type(result) is RedisError:
|
||||
logger.error(f"[ANNOUNCEMENT] REDIS {operation} - Failed with error: {result}")
|
||||
else:
|
||||
logger.info(f"[ANNOUNCEMENT] REDIS {operation} {key} - OK")
|
||||
|
||||
class MirrorsLogger:
|
||||
async def log(self, operation: str, result: RedisError | None = None, key: str = "") -> None:
|
||||
"""Logs internal cache operations
|
||||
|
||||
Args:
|
||||
operation (str): Operation name
|
||||
key (str): Key used in the operation
|
||||
"""
|
||||
if type(result) is RedisError:
|
||||
logger.error(f"[MIRRORS] REDIS {operation} - Failed with error: {result}")
|
||||
else:
|
||||
logger.info(f"[MIRRORS] REDIS {operation} {key} - OK")
|
23
app/utils/RedisConnector.py
Normal file
23
app/utils/RedisConnector.py
Normal file
@ -0,0 +1,23 @@
|
||||
import os
|
||||
import toml
|
||||
from redis import asyncio as aioredis
|
||||
|
||||
# Load config
|
||||
|
||||
config: dict = toml.load("config.toml")
|
||||
|
||||
# Redis connection parameters
|
||||
|
||||
redis_config: dict[ str, str | int ] = {
|
||||
"url": f"redis://{os.environ['REDIS_URL']}",
|
||||
"port": os.environ['REDIS_PORT'],
|
||||
}
|
||||
|
||||
class RedisConnector:
|
||||
"""Implements the RedisConnector class for the ReVanced API"""
|
||||
|
||||
@staticmethod
|
||||
def connect(database: str) -> aioredis.Redis:
|
||||
"""Connect to Redis"""
|
||||
redis_url = f"{redis_config['url']}:{redis_config['port']}/{database}"
|
||||
return aioredis.from_url(redis_url, encoding="utf-8", decode_responses=True)
|
0
app/utils/__init__.py
Normal file
0
app/utils/__init__.py
Normal file
46
config.toml
Normal file
46
config.toml
Normal file
@ -0,0 +1,46 @@
|
||||
[docs]
|
||||
|
||||
title = "ReVanced Polling API"
|
||||
description = """We do a little polling ඞ
|
||||
## The official JSON API for ReVanced polls 🚀
|
||||
|
||||
### Important Information
|
||||
|
||||
* Rate Limiting - 60 requests per minute
|
||||
* Cache - 5 minutes
|
||||
* Token duration - 5 minutes
|
||||
|
||||
### Additional Notes
|
||||
|
||||
1. Breaking changes are to be expected
|
||||
2. Client side caching is advised to avoid unnecessary requests
|
||||
3. Abuse of the API will result in IP blocks
|
||||
|
||||
"""
|
||||
version = "0.0.1"
|
||||
|
||||
[license]
|
||||
|
||||
name = "AGPL-3.0"
|
||||
url = "https://www.gnu.org/licenses/agpl-3.0.en.html"
|
||||
|
||||
[logging]
|
||||
level = "INFO"
|
||||
json_logs = false
|
||||
|
||||
[cache]
|
||||
expire = 300
|
||||
database = 0
|
||||
|
||||
[slowapi]
|
||||
limit = "60/minute"
|
||||
database = 1
|
||||
|
||||
[tokens]
|
||||
database = 2
|
||||
|
||||
[ballots]
|
||||
database = 3
|
||||
|
||||
[auth]
|
||||
access_token_expires = 300
|
78
mypy.ini
Normal file
78
mypy.ini
Normal file
@ -0,0 +1,78 @@
|
||||
[mypy]
|
||||
python_version = 3.10
|
||||
pretty = true
|
||||
follow_imports = normal
|
||||
namespace_packages = true
|
||||
show_column_numbers = true
|
||||
show_error_codes = true
|
||||
allow_redefinition = false
|
||||
check_untyped_defs = true
|
||||
implicit_reexport = false
|
||||
strict_optional = true
|
||||
strict_equality = true
|
||||
warn_no_return = true
|
||||
warn_redundant_casts = true
|
||||
warn_unused_configs = true
|
||||
warn_unused_ignores = true
|
||||
warn_unreachable = true
|
||||
plugins = pydantic.mypy
|
||||
|
||||
[mypy-toml.*]
|
||||
# Current stubs are not compatible with python 3.10
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-uvicorn.*]
|
||||
# No stubs available
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-aioredis.*]
|
||||
# No stubs available
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-fastapi.*]
|
||||
# No stubs available
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-slowapi.*]
|
||||
# No stubs available
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-fastapi_cache.*]
|
||||
# No stubs available
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-msgpack.*]
|
||||
# No stubs available
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-orjson.*]
|
||||
# No stubs available
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-httpx_cache.*]
|
||||
# No stubs available
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-redis.*]
|
||||
# No stubs available
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-toolz.*]
|
||||
# No stubs available
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-fastapi_paseto_auth.*]
|
||||
# No stubs available
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-aiofiles.*]
|
||||
# No stubs available
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-gunicorn.*]
|
||||
# No stubs available
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-asgiref.*]
|
||||
# No stubs available
|
||||
ignore_missing_imports = True
|
1686
poetry.lock
generated
Normal file
1686
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
pyproject.toml
Normal file
40
pyproject.toml
Normal file
@ -0,0 +1,40 @@
|
||||
[tool.poetry]
|
||||
name = "revanced-polling-api"
|
||||
version = "0.0.1"
|
||||
description = "We do a little polling ඞ"
|
||||
authors = ["Alexandre Teles <alexandre.teles@ufba.br>"]
|
||||
license = "AGPLv3"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
fastapi = ">=0.85.0"
|
||||
httpx = {version = ">=0.23.0", extras = ["http2"]}
|
||||
httpx-cache = ">=0.6.0"
|
||||
toml = ">=0.10.2"
|
||||
slowapi = ">=0.1.6"
|
||||
orjson = ">=3.8.0"
|
||||
fastapi-cache2 = ">=0.1.9"
|
||||
redis = ">=4.3.4"
|
||||
loguru = ">=0.6.0"
|
||||
sentry-sdk = ">=1.9.8"
|
||||
argon2-cffi = ">=21.3.0"
|
||||
hypercorn = {extras = ["uvloop"], version = ">=0.14.3"}
|
||||
cytoolz = ">=0.12.0"
|
||||
fastapi-paseto-auth = "^0.6.0"
|
||||
ujson = ">=5.5.0"
|
||||
hiredis = ">=2.0.0"
|
||||
aiofiles = ">=22.1.0"
|
||||
uvicorn = ">=0.18.3"
|
||||
gunicorn = ">=20.1.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
mypy = ">=0.971"
|
||||
types-toml = ">=0.10.8"
|
||||
types-redis = ">=4.3.21.1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[virtualenvs]
|
||||
create = true
|
64
requirements.txt
Normal file
64
requirements.txt
Normal file
@ -0,0 +1,64 @@
|
||||
aiofiles==22.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
aiorwlock==1.3.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
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==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"
|
||||
attrs==21.4.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
certifi==2022.9.24 ; 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"
|
||||
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"
|
||||
cytoolz==0.12.0 ; 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-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"
|
||||
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"
|
||||
h11==0.12.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"
|
||||
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"
|
||||
httpx-cache==0.6.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
httpx==0.23.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
httpx[http2]==0.23.0 ; 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"
|
||||
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"
|
||||
limits==1.6 ; 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"
|
||||
orjson==3.8.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
packaging==21.3 ; 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"
|
||||
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"
|
||||
pycryptodomex==3.15.0 ; 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"
|
||||
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"
|
||||
redis==4.3.4 ; 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.10.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
setuptools==65.5.1 ; 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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
urllib3==1.26.12 ; python_version >= "3.10" and python_version < "4"
|
||||
uvicorn==0.19.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"
|
||||
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"
|
||||
wsproto==1.2.0 ; python_version >= "3.10" and python_version < "4.0"
|
148
run.py
Normal file
148
run.py
Normal file
@ -0,0 +1,148 @@
|
||||
import os
|
||||
import sys
|
||||
import toml
|
||||
import logging
|
||||
import sentry_sdk
|
||||
from app.main import app
|
||||
from loguru import logger
|
||||
from fastapi import FastAPI
|
||||
from types import FrameType
|
||||
from typing import Any, Optional
|
||||
from multiprocessing import cpu_count
|
||||
from gunicorn.glogging import Logger
|
||||
from gunicorn.app.base import BaseApplication
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
from sentry_sdk.integrations.httpx import HttpxIntegration
|
||||
from sentry_sdk.integrations.gnu_backtrace import GnuBacktraceIntegration
|
||||
|
||||
config: dict = toml.load("config.toml")
|
||||
|
||||
# Enable sentry logging
|
||||
|
||||
sentry_sdk.init(os.environ['SENTRY_DSN'], traces_sample_rate=1.0, integrations=[
|
||||
RedisIntegration(),
|
||||
HttpxIntegration(),
|
||||
GnuBacktraceIntegration(),
|
||||
],)
|
||||
|
||||
LOG_LEVEL: Any = logging.getLevelName(config['logging']['level'])
|
||||
JSON_LOGS: bool = config['logging']['json_logs']
|
||||
WORKERS: int = int(cpu_count() + 1)
|
||||
BIND: str = f'{os.environ.get("HYPERCORN_HOST")}:{os.environ.get("HYPERCORN_PORT")}'
|
||||
|
||||
class InterceptHandler(logging.Handler):
|
||||
"""Intercept logs and forward them to Loguru.
|
||||
|
||||
Args:
|
||||
logging.Handler (Filterer): Handler to filter logs
|
||||
"""
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
"""Emit a log record."""
|
||||
|
||||
# Get corresponding Loguru level if it exists
|
||||
level: str | int
|
||||
frame: FrameType
|
||||
depth: int
|
||||
|
||||
try:
|
||||
level = logger.level(record.levelname).name
|
||||
except ValueError:
|
||||
level = record.levelno
|
||||
|
||||
# Find caller from where originated the logged message
|
||||
frame, depth = logging.currentframe(), 2
|
||||
while frame.f_code.co_filename == logging.__file__:
|
||||
frame = frame.f_back
|
||||
depth += 1
|
||||
|
||||
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
|
||||
|
||||
|
||||
class StubbedGunicornLogger(Logger):
|
||||
"""Defining a custom logger class to prevent gunicorn from logging to stdout
|
||||
|
||||
Args:
|
||||
Logger (object): Gunicon logger class
|
||||
"""
|
||||
def setup(self, cfg) -> None:
|
||||
"""Setup logger."""
|
||||
|
||||
handler: logging.NullHandler = logging.NullHandler()
|
||||
self.error_logger: Logger = logging.getLogger("gunicorn.error")
|
||||
|
||||
self.error_logger.addHandler(handler)
|
||||
|
||||
self.access_logger: Logger = logging.getLogger("gunicorn.access")
|
||||
|
||||
self.access_logger.addHandler(handler)
|
||||
self.error_logger.setLevel(LOG_LEVEL)
|
||||
self.access_logger.setLevel(LOG_LEVEL)
|
||||
|
||||
|
||||
class StandaloneApplication(BaseApplication):
|
||||
"""Defines a Guicorn application
|
||||
|
||||
Args:
|
||||
BaseApplication (object): Base class for Gunicorn applications
|
||||
"""
|
||||
|
||||
def __init__(self, app: FastAPI, options: dict | None = None):
|
||||
"""Initialize the application
|
||||
|
||||
Args:
|
||||
app (fastapi.FastAPI): FastAPI application
|
||||
options (dict, optional): Gunicorn options. Defaults to None.
|
||||
"""
|
||||
self.options: dict = options or {}
|
||||
self.application: FastAPI = app
|
||||
super().__init__()
|
||||
|
||||
def load_config(self) -> None:
|
||||
"""Load Gunicorn configuration."""
|
||||
config: dict = {
|
||||
key: value for key, value in self.options.items()
|
||||
if key in self.cfg.settings and value is not None
|
||||
}
|
||||
for key, value in config.items():
|
||||
self.cfg.set(key.lower(), value)
|
||||
|
||||
def load(self) -> FastAPI:
|
||||
"""Load the application
|
||||
|
||||
Returns:
|
||||
FastAPI: FastAPI application
|
||||
"""
|
||||
return self.application
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
intercept_handler = InterceptHandler()
|
||||
logging.root.setLevel(LOG_LEVEL)
|
||||
|
||||
seen: set = set()
|
||||
for name in [
|
||||
*logging.root.manager.loggerDict.keys(),
|
||||
"gunicorn",
|
||||
"gunicorn.access",
|
||||
"gunicorn.error",
|
||||
"uvicorn",
|
||||
"uvicorn.access",
|
||||
"uvicorn.error",
|
||||
]:
|
||||
if name not in seen:
|
||||
seen.add(name.split(".")[0])
|
||||
logging.getLogger(name).handlers = [intercept_handler]
|
||||
|
||||
logger.configure(handlers=[{"sink": sys.stdout, "serialize": JSON_LOGS}])
|
||||
|
||||
options: dict = {
|
||||
"bind": BIND,
|
||||
"workers": WORKERS,
|
||||
"accesslog": "-",
|
||||
"errorlog": "-",
|
||||
"worker_class": "uvicorn.workers.UvicornWorker",
|
||||
"logger_class": StubbedGunicornLogger,
|
||||
"preload": True,
|
||||
}
|
||||
|
||||
StandaloneApplication(app, options).run()
|
Loading…
x
Reference in New Issue
Block a user