mirror of
https://github.com/revanced/revanced-api.git
synced 2025-04-30 06:34:36 +02:00
Merge pull request #126 from ReVanced/dev
feat: manager endpoints and more
This commit is contained in:
commit
71f81f7f20
@ -12,7 +12,7 @@ repos:
|
|||||||
- id: check-toml
|
- id: check-toml
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.9.1
|
rev: 23.11.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python3.11
|
language_version: python3.11
|
||||||
|
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@ -1,3 +1,10 @@
|
|||||||
{
|
{
|
||||||
"python.analysis.typeCheckingMode": "off"
|
"python.analysis.typeCheckingMode": "off",
|
||||||
|
"spellright.language": [
|
||||||
|
"pt"
|
||||||
|
],
|
||||||
|
"spellright.documentTypes": [
|
||||||
|
"markdown",
|
||||||
|
"latex"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
ARG GITHUB_TOKEN
|
ARG GITHUB_TOKEN
|
||||||
|
ARG SENTRY_DSN
|
||||||
|
|
||||||
ENV GITHUB_TOKEN $GITHUB_TOKEN
|
ENV GITHUB_TOKEN $GITHUB_TOKEN
|
||||||
|
ENV SENTRY_DSN $SENTRY_DSN
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
@ -1,25 +1,27 @@
|
|||||||
# api/__init__.py
|
# api/__init__.py
|
||||||
from sanic import Blueprint
|
from sanic import Blueprint
|
||||||
|
import importlib
|
||||||
|
import pkgutil
|
||||||
|
from api.utils.versioning import get_version
|
||||||
|
|
||||||
from api.github import github
|
# Dynamically import all modules in the 'api' package, excluding subdirectories
|
||||||
from api.ping import ping
|
versioned_blueprints = {}
|
||||||
from api.socials import socials
|
for finder, module_name, ispkg in pkgutil.iter_modules(["api"]):
|
||||||
from api.info import info
|
if not ispkg:
|
||||||
from api.compat import github as compat
|
# Import the module
|
||||||
from api.donations import donations
|
module = importlib.import_module(f"api.{module_name}")
|
||||||
from api.announcements import announcements
|
|
||||||
from api.login import login
|
|
||||||
from api.robots import robots
|
|
||||||
|
|
||||||
api = Blueprint.group(
|
# Add the module's blueprint to the versioned list, if it exists
|
||||||
login,
|
if hasattr(module, module_name):
|
||||||
ping,
|
blueprint = getattr(module, module_name)
|
||||||
github,
|
version = get_version(module_name)
|
||||||
info,
|
versioned_blueprints.setdefault(version, []).append(blueprint)
|
||||||
socials,
|
|
||||||
donations,
|
# Create Blueprint groups for each version
|
||||||
announcements,
|
api = []
|
||||||
compat,
|
for version, blueprints in versioned_blueprints.items():
|
||||||
robots,
|
if version == "old":
|
||||||
url_prefix="/",
|
group = Blueprint.group(*blueprints, url_prefix="/")
|
||||||
)
|
else:
|
||||||
|
group = Blueprint.group(*blueprints, version=version, url_prefix="/")
|
||||||
|
api.append(group)
|
||||||
|
@ -10,6 +10,7 @@ Routes:
|
|||||||
- DELETE /announcements/<announcement_id:int>: Delete an announcement.
|
- DELETE /announcements/<announcement_id:int>: Delete an announcement.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import datetime
|
import datetime
|
||||||
from sanic import Blueprint, Request
|
from sanic import Blueprint, Request
|
||||||
from sanic.response import JSONResponse, json
|
from sanic.response import JSONResponse, json
|
||||||
@ -20,10 +21,9 @@ from data.models import AnnouncementDbModel, AttachmentDbModel
|
|||||||
import sanic_beskar
|
import sanic_beskar
|
||||||
|
|
||||||
from api.models.announcements import AnnouncementResponseModel
|
from api.models.announcements import AnnouncementResponseModel
|
||||||
from config import api_version
|
from api.utils.limiter import limiter
|
||||||
from limiter import limiter
|
|
||||||
|
|
||||||
announcements: Blueprint = Blueprint("announcements", version=api_version)
|
announcements: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
||||||
|
|
||||||
|
|
||||||
@announcements.get("/announcements")
|
@announcements.get("/announcements")
|
||||||
|
32
api/apkdl.py
32
api/apkdl.py
@ -1,32 +0,0 @@
|
|||||||
"""
|
|
||||||
This module provides a blueprint for the app endpoint.
|
|
||||||
|
|
||||||
Routes:
|
|
||||||
- GET /app/info: Get app info.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from sanic import Blueprint, Request
|
|
||||||
from sanic.response import JSONResponse, json
|
|
||||||
from sanic_ext import openapi
|
|
||||||
|
|
||||||
from api.backends.apkdl import ApkDl
|
|
||||||
from api.backends.entities import AppInfo
|
|
||||||
from api.models.appinfo import AppInfoModel
|
|
||||||
|
|
||||||
from config import api_version
|
|
||||||
|
|
||||||
apkdl: Blueprint = Blueprint("app", version=api_version)
|
|
||||||
|
|
||||||
apkdl_backend: ApkDl = ApkDl()
|
|
||||||
|
|
||||||
|
|
||||||
@apkdl.get("/app/info/<app_id:str>")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Get information about an app",
|
|
||||||
response=[AppInfoModel],
|
|
||||||
)
|
|
||||||
async def root(request: Request, app_id: str) -> JSONResponse:
|
|
||||||
data: dict[str, AppInfo] = {
|
|
||||||
"app_info": await apkdl_backend.get_app_info(package_name=app_id)
|
|
||||||
}
|
|
||||||
return json(data, status=200)
|
|
@ -1,64 +0,0 @@
|
|||||||
from base64 import b64encode
|
|
||||||
|
|
||||||
from aiohttp import ClientResponse
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from sanic import SanicException
|
|
||||||
from toolz.functoolz import compose
|
|
||||||
|
|
||||||
from api.backends.backend import AppInfoProvider
|
|
||||||
from api.backends.entities import AppInfo
|
|
||||||
from api.utils.http_utils import http_get
|
|
||||||
|
|
||||||
name: str = "apkdl"
|
|
||||||
base_url: str = "https://apk-dl.com"
|
|
||||||
|
|
||||||
|
|
||||||
class ApkDl(AppInfoProvider):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(name, base_url)
|
|
||||||
|
|
||||||
async def get_app_info(self, package_name: str) -> AppInfo:
|
|
||||||
"""Fetches information about an Android app from the ApkDl website.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
package_name (str): The package name of the app to fetch.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
AppInfo: An AppInfo object containing the name, category, and logo of the app.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
SanicException: If the HTTP request fails or the app data is incomplete or not found.
|
|
||||||
"""
|
|
||||||
app_url: str = f"{base_url}/{package_name}"
|
|
||||||
response: ClientResponse = await http_get(headers={}, url=app_url)
|
|
||||||
if response.status != 200:
|
|
||||||
raise SanicException(
|
|
||||||
f"ApkDl: {response.status}", status_code=response.status
|
|
||||||
)
|
|
||||||
page = BeautifulSoup(await response.read(), "lxml")
|
|
||||||
find_div_text = compose(
|
|
||||||
lambda d: d.find_next_sibling("div"),
|
|
||||||
lambda d: page.find("div", text=d),
|
|
||||||
)
|
|
||||||
fetch_logo_url = compose(
|
|
||||||
lambda div: div.img["src"],
|
|
||||||
lambda _: page.find("div", {"class": "logo"}),
|
|
||||||
)
|
|
||||||
logo_response: ClientResponse = await http_get(
|
|
||||||
headers={}, url=fetch_logo_url(None)
|
|
||||||
)
|
|
||||||
logo: str = (
|
|
||||||
f"data:image/png;base64,{b64encode(await logo_response.content.read()).decode('utf-8')}"
|
|
||||||
if logo_response.status == 200
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
app_data = dict(
|
|
||||||
name=find_div_text("App Name").text,
|
|
||||||
category=find_div_text("Category").text,
|
|
||||||
logo=logo,
|
|
||||||
)
|
|
||||||
if not all(app_data.values()):
|
|
||||||
raise SanicException(
|
|
||||||
"ApkDl: App data incomplete or not found", status_code=500
|
|
||||||
)
|
|
||||||
return AppInfo(**app_data)
|
|
@ -1,5 +1,4 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from json import loads
|
|
||||||
import os
|
import os
|
||||||
from operator import eq
|
from operator import eq
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@ -7,9 +6,9 @@ from typing import Optional
|
|||||||
import ujson
|
import ujson
|
||||||
from aiohttp import ClientResponse
|
from aiohttp import ClientResponse
|
||||||
from sanic import SanicException
|
from sanic import SanicException
|
||||||
from toolz import filter, map, partial
|
from cytoolz import filter, map, partial, curry, pipe
|
||||||
from toolz.dicttoolz import get_in, keyfilter
|
from cytoolz.dicttoolz import get_in, keyfilter
|
||||||
from toolz.itertoolz import mapcat, pluck
|
from cytoolz.itertoolz import mapcat, pluck
|
||||||
|
|
||||||
from api.backends.backend import Backend, Repository
|
from api.backends.backend import Backend, Repository
|
||||||
from api.backends.entities import *
|
from api.backends.entities import *
|
||||||
@ -396,3 +395,46 @@ class Github(Backend):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return list(map(lambda pair: transform(*pair), zip(results, repositories)))
|
return list(map(lambda pair: transform(*pair), zip(results, repositories)))
|
||||||
|
|
||||||
|
async def generate_custom_sources(
|
||||||
|
self, repositories: list[GithubRepository], dev: bool
|
||||||
|
) -> dict[str, dict[str, str]]:
|
||||||
|
"""Generate a custom sources dictionary for a set of repositories.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repositories (list[GithubRepository]): The repositories for which to generate the sources.
|
||||||
|
dev (bool): If we should get the latest pre-release instead.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, dict[str, str]]: A dictionary containing the custom sources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
filter_by_name = curry(lambda name, item: name in item["name"])
|
||||||
|
filter_patches_jar = curry(
|
||||||
|
lambda item: "patches" in item["name"] and item["name"].endswith(".jar")
|
||||||
|
)
|
||||||
|
get_fields = curry(
|
||||||
|
lambda fields, item: {field: item[field] for field in fields}
|
||||||
|
)
|
||||||
|
rename_key = curry(
|
||||||
|
lambda old, new, d: {new if k == old else k: v for k, v in d.items()}
|
||||||
|
)
|
||||||
|
|
||||||
|
sources = await self.compat_get_tools(repositories, dev)
|
||||||
|
|
||||||
|
patches = pipe(
|
||||||
|
sources,
|
||||||
|
lambda items: next(filter(filter_patches_jar, items), None),
|
||||||
|
get_fields(["version", "browser_download_url"]),
|
||||||
|
rename_key("browser_download_url", "url"),
|
||||||
|
)
|
||||||
|
|
||||||
|
integrations = pipe(
|
||||||
|
sources,
|
||||||
|
lambda items: next(filter(filter_by_name("integrations"), items), None),
|
||||||
|
get_fields(["version", "browser_download_url"]),
|
||||||
|
rename_key("browser_download_url", "url"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"patches": patches, "integrations": integrations}
|
||||||
|
@ -9,8 +9,7 @@ Routes:
|
|||||||
- GET /patches/<tag:str>: Retrieve a list of patches for a given release tag.
|
- GET /patches/<tag:str>: Retrieve a list of patches for a given release tag.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
from sanic import Blueprint, Request
|
from sanic import Blueprint, Request
|
||||||
from sanic.response import JSONResponse, json
|
from sanic.response import JSONResponse, json
|
||||||
from sanic_ext import openapi
|
from sanic_ext import openapi
|
||||||
@ -20,12 +19,12 @@ from api.models.github import *
|
|||||||
from api.models.compat import ToolsResponseModel, ContributorsResponseModel
|
from api.models.compat import ToolsResponseModel, ContributorsResponseModel
|
||||||
from config import compat_repositories, owner
|
from config import compat_repositories, owner
|
||||||
|
|
||||||
github: Blueprint = Blueprint("old")
|
compat: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
||||||
|
|
||||||
github_backend: Github = Github()
|
github_backend: Github = Github()
|
||||||
|
|
||||||
|
|
||||||
@github.get("/tools")
|
@compat.get("/tools")
|
||||||
@openapi.definition(
|
@openapi.definition(
|
||||||
summary="Get patching tools' latest version.", response=[ToolsResponseModel]
|
summary="Get patching tools' latest version.", response=[ToolsResponseModel]
|
||||||
)
|
)
|
||||||
@ -62,7 +61,7 @@ async def tools(request: Request) -> JSONResponse:
|
|||||||
return json(data, status=200)
|
return json(data, status=200)
|
||||||
|
|
||||||
|
|
||||||
@github.get("/contributors")
|
@compat.get("/contributors")
|
||||||
@openapi.definition(
|
@openapi.definition(
|
||||||
summary="Get organization-wise contributors.", response=[ContributorsResponseModel]
|
summary="Get organization-wise contributors.", response=[ContributorsResponseModel]
|
||||||
)
|
)
|
||||||
|
@ -5,14 +5,16 @@ Routes:
|
|||||||
- GET /donations: Get ReVanced donation links and wallets.
|
- GET /donations: Get ReVanced donation links and wallets.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from sanic import Blueprint, Request
|
from sanic import Blueprint, Request
|
||||||
from sanic.response import JSONResponse, json
|
from sanic.response import JSONResponse, json
|
||||||
from sanic_ext import openapi
|
from sanic_ext import openapi
|
||||||
|
|
||||||
from api.models.donations import DonationsResponseModel
|
from api.models.donations import DonationsResponseModel
|
||||||
from config import api_version, wallets, links
|
from config import wallets, links
|
||||||
|
|
||||||
donations: Blueprint = Blueprint("donations", version=api_version)
|
donations: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
||||||
|
|
||||||
|
|
||||||
@donations.get("/donations")
|
@donations.get("/donations")
|
||||||
|
@ -10,6 +10,7 @@ Routes:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
from sanic import Blueprint, Request
|
from sanic import Blueprint, Request
|
||||||
from sanic.response import JSONResponse, json
|
from sanic.response import JSONResponse, json
|
||||||
from sanic_ext import openapi
|
from sanic_ext import openapi
|
||||||
@ -17,9 +18,9 @@ from sanic_ext import openapi
|
|||||||
from api.backends.entities import Release, Contributor
|
from api.backends.entities import Release, Contributor
|
||||||
from api.backends.github import Github, GithubRepository
|
from api.backends.github import Github, GithubRepository
|
||||||
from api.models.github import *
|
from api.models.github import *
|
||||||
from config import owner, default_repository, api_version
|
from config import owner, default_repository
|
||||||
|
|
||||||
github: Blueprint = Blueprint("github", version=api_version)
|
github: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
||||||
|
|
||||||
github_backend: Github = Github()
|
github_backend: Github = Github()
|
||||||
|
|
||||||
|
@ -5,14 +5,15 @@ Routes:
|
|||||||
- GET /info: Get info about the owner of the API.
|
- GET /info: Get info about the owner of the API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
from sanic import Blueprint, Request
|
from sanic import Blueprint, Request
|
||||||
from sanic.response import JSONResponse, json
|
from sanic.response import JSONResponse, json
|
||||||
from sanic_ext import openapi
|
from sanic_ext import openapi
|
||||||
|
|
||||||
from api.models.info import InfoResponseModel
|
from api.models.info import InfoResponseModel
|
||||||
from config import api_version, default_info
|
from config import default_info
|
||||||
|
|
||||||
info: Blueprint = Blueprint("info", version=api_version)
|
info: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
||||||
|
|
||||||
|
|
||||||
@info.get("/info")
|
@info.get("/info")
|
||||||
|
10
api/login.py
10
api/login.py
@ -5,18 +5,16 @@ Routes:
|
|||||||
- POST /login: Login to the API
|
- POST /login: Login to the API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
from sanic import Blueprint, Request
|
from sanic import Blueprint, Request
|
||||||
from sanic.response import JSONResponse, json
|
from sanic.response import JSONResponse, json
|
||||||
from sanic_ext import openapi
|
from sanic_ext import openapi
|
||||||
from sanic_beskar.exceptions import AuthenticationError
|
from sanic_beskar.exceptions import AuthenticationError
|
||||||
|
|
||||||
from auth import beskar
|
from api.utils.auth import beskar
|
||||||
from limiter import limiter
|
from api.utils.limiter import limiter
|
||||||
|
|
||||||
from config import api_version
|
login: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
||||||
|
|
||||||
|
|
||||||
login: Blueprint = Blueprint("login", version=api_version)
|
|
||||||
|
|
||||||
|
|
||||||
@login.post("/login")
|
@login.post("/login")
|
||||||
|
61
api/manager.py
Normal file
61
api/manager.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
This module provides ReVanced Manager specific endpoints.
|
||||||
|
|
||||||
|
Routes:
|
||||||
|
- GET /manager/bootstrap: Get a list of the main ReVanced tools.
|
||||||
|
- GET /manager/sources: Get a list of ReVanced sources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from sanic import Blueprint, Request
|
||||||
|
from sanic.response import JSONResponse, json
|
||||||
|
from sanic_ext import openapi
|
||||||
|
|
||||||
|
from api.backends.github import GithubRepository, Github
|
||||||
|
|
||||||
|
from api.models.manager import BootsrapResponseModel, CustomSourcesResponseModel
|
||||||
|
from config import compat_repositories, owner
|
||||||
|
|
||||||
|
manager: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
||||||
|
|
||||||
|
github_backend: Github = Github()
|
||||||
|
|
||||||
|
|
||||||
|
@manager.get("/manager/bootstrap")
|
||||||
|
@openapi.definition(
|
||||||
|
summary="Get a list of the main ReVanced tools",
|
||||||
|
response=[BootsrapResponseModel],
|
||||||
|
)
|
||||||
|
async def bootstrap(request: Request) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
Returns a JSONResponse with a list of the main ReVanced tools.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- JSONResponse: A Sanic JSONResponse instance containing a list with the tool names.
|
||||||
|
"""
|
||||||
|
data: dict[str, dict] = {"tools": compat_repositories}
|
||||||
|
return json(data, status=200)
|
||||||
|
|
||||||
|
|
||||||
|
@manager.get("/manager/custom-source")
|
||||||
|
@openapi.definition(
|
||||||
|
summary="Get a list of ReVanced sources",
|
||||||
|
response=[CustomSourcesResponseModel],
|
||||||
|
)
|
||||||
|
async def custom_sources(request: Request) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
Returns a JSONResponse with a list of the main ReVanced sources.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- JSONResponse: A Sanic JSONResponse instance containing a list with the source names.
|
||||||
|
"""
|
||||||
|
data = await github_backend.generate_custom_sources(
|
||||||
|
repositories=[
|
||||||
|
GithubRepository(owner=owner, name=repo)
|
||||||
|
for repo in compat_repositories
|
||||||
|
if "patches" in repo or "integrations" in repo
|
||||||
|
],
|
||||||
|
dev=True if request.args.get("dev") else False,
|
||||||
|
)
|
||||||
|
|
||||||
|
return json(data, status=200)
|
@ -1,19 +0,0 @@
|
|||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class AppInfoFields(BaseModel):
|
|
||||||
"""
|
|
||||||
Fields for the AppInfo endpoint.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
category: str
|
|
||||||
logo: str
|
|
||||||
|
|
||||||
|
|
||||||
class AppInfoModel(BaseModel):
|
|
||||||
"""
|
|
||||||
Response model app info.
|
|
||||||
"""
|
|
||||||
|
|
||||||
app_info: AppInfoFields
|
|
32
api/models/manager.py
Normal file
32
api/models/manager.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class BootsrapResponseModel(BaseModel):
|
||||||
|
"""
|
||||||
|
A Pydantic BaseModel that represents a list of available tools.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tools: list[str]
|
||||||
|
"""
|
||||||
|
A list of available tools.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class CustomSourcesFields(BaseModel):
|
||||||
|
"""
|
||||||
|
Implements the fields for a source.
|
||||||
|
"""
|
||||||
|
|
||||||
|
url: str
|
||||||
|
preferred: bool
|
||||||
|
|
||||||
|
|
||||||
|
class CustomSourcesResponseModel(BaseModel):
|
||||||
|
"""
|
||||||
|
A Pydantic BaseModel that represents a list of available sources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_: dict[str, CustomSourcesFields]
|
||||||
|
"""
|
||||||
|
A list of available sources.
|
||||||
|
"""
|
@ -5,11 +5,11 @@ Routes:
|
|||||||
- HEAD /ping: Ping the API.
|
- HEAD /ping: Ping the API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
from sanic import Blueprint, HTTPResponse, Request, response
|
from sanic import Blueprint, HTTPResponse, Request, response
|
||||||
from sanic_ext import openapi
|
from sanic_ext import openapi
|
||||||
from config import api_version
|
|
||||||
|
|
||||||
ping: Blueprint = Blueprint("ping", version=api_version)
|
ping: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
||||||
|
|
||||||
|
|
||||||
@ping.head("/ping")
|
@ping.head("/ping")
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
import os
|
||||||
from sanic import Blueprint
|
from sanic import Blueprint
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
robots: Blueprint = Blueprint("robots")
|
robots: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
||||||
|
|
||||||
|
|
||||||
@robots.get("/robots.txt")
|
@robots.get("/robots.txt")
|
||||||
|
@ -5,14 +5,15 @@ Routes:
|
|||||||
- GET /socials: Get ReVanced socials.
|
- GET /socials: Get ReVanced socials.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
from sanic import Blueprint, Request
|
from sanic import Blueprint, Request
|
||||||
from sanic.response import JSONResponse, json
|
from sanic.response import JSONResponse, json
|
||||||
from sanic_ext import openapi
|
from sanic_ext import openapi
|
||||||
|
|
||||||
from api.models.socials import SocialsResponseModel
|
from api.models.socials import SocialsResponseModel
|
||||||
from config import social_links, api_version
|
from config import social_links
|
||||||
|
|
||||||
socials: Blueprint = Blueprint("socials", version=api_version)
|
socials: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
||||||
|
|
||||||
|
|
||||||
@socials.get("/socials")
|
@socials.get("/socials")
|
||||||
|
8
api/utils/versioning.py
Normal file
8
api/utils/versioning.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from cytoolz import keyfilter
|
||||||
|
from config import api_versions
|
||||||
|
|
||||||
|
|
||||||
|
def get_version(value: str) -> str:
|
||||||
|
result = keyfilter(lambda key: value in api_versions[key], api_versions)
|
||||||
|
|
||||||
|
return list(result.keys())[0] if result else "v0"
|
48
app.py
48
app.py
@ -1,19 +1,34 @@
|
|||||||
# app.py
|
# app.py
|
||||||
from sanic import Sanic
|
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from sanic import HTTPResponse, Sanic
|
||||||
import sanic.response
|
import sanic.response
|
||||||
from sanic_ext import Config
|
from sanic_ext import Config
|
||||||
|
|
||||||
from api import api
|
from api import api
|
||||||
from config import *
|
from config import openapi_title, openapi_version, openapi_description, hostnames
|
||||||
|
|
||||||
from limiter import configure_limiter
|
from api.utils.limiter import configure_limiter
|
||||||
from auth import configure_auth
|
from api.utils.auth import configure_auth
|
||||||
|
|
||||||
|
import sentry_sdk
|
||||||
|
|
||||||
|
if os.environ.get("SENTRY_DSN"):
|
||||||
|
sentry_sdk.init(
|
||||||
|
dsn=os.environ["SENTRY_DSN"],
|
||||||
|
enable_tracing=True,
|
||||||
|
traces_sample_rate=1.0,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("WARNING: Sentry DSN not set, not enabling Sentry")
|
||||||
|
|
||||||
REDIRECTS = {
|
REDIRECTS = {
|
||||||
"/": "/docs/swagger",
|
"/": "/docs/swagger",
|
||||||
}
|
}
|
||||||
|
|
||||||
app = Sanic("ReVanced-API")
|
app = Sanic("revanced-api")
|
||||||
app.extend(config=Config(oas_ignore_head=False))
|
app.extend(config=Config(oas_ignore_head=False))
|
||||||
app.ext.openapi.describe(
|
app.ext.openapi.describe(
|
||||||
title=openapi_title,
|
title=openapi_title,
|
||||||
@ -39,7 +54,7 @@ app.blueprint(api)
|
|||||||
# https://sanic.dev/en/guide/how-to/static-redirects.html
|
# https://sanic.dev/en/guide/how-to/static-redirects.html
|
||||||
|
|
||||||
|
|
||||||
def get_static_function(value):
|
def get_static_function(value) -> Any:
|
||||||
return lambda *_, **__: value
|
return lambda *_, **__: value
|
||||||
|
|
||||||
|
|
||||||
@ -47,13 +62,26 @@ for src, dest in REDIRECTS.items():
|
|||||||
app.route(src)(get_static_function(sanic.response.redirect(dest)))
|
app.route(src)(get_static_function(sanic.response.redirect(dest)))
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("response")
|
@app.on_request
|
||||||
async def add_cache_control(request, response):
|
async def domain_check(request) -> HTTPResponse:
|
||||||
|
if request.host not in hostnames:
|
||||||
|
return sanic.response.redirect(f"https://api.revanced.app/{request.path}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_response
|
||||||
|
async def add_cache_control(_, response):
|
||||||
response.headers["Cache-Control"] = "public, max-age=300"
|
response.headers["Cache-Control"] = "public, max-age=300"
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("response")
|
@app.on_response
|
||||||
async def add_csp(request, response):
|
async def add_csp(_, response):
|
||||||
response.headers[
|
response.headers[
|
||||||
"Content-Security-Policy"
|
"Content-Security-Policy"
|
||||||
] = "default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;"
|
] = "default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;"
|
||||||
|
|
||||||
|
|
||||||
|
app.static(
|
||||||
|
"/favicon.ico",
|
||||||
|
"static/img/favicon.ico",
|
||||||
|
name="favicon",
|
||||||
|
)
|
||||||
|
20
config.py
20
config.py
@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
backend: str = "github"
|
backend: str = "github"
|
||||||
redis: dict[str, str | int] = {"host": "localhost", "port": 6379}
|
redis: dict[str, str | int] = {"host": "localhost", "port": 6379}
|
||||||
|
hostnames: list[str] = [
|
||||||
|
"api.revanced.app",
|
||||||
|
"deimos.revanced.app",
|
||||||
|
"localhost:8000",
|
||||||
|
"127.0.0.1:8000",
|
||||||
|
]
|
||||||
|
|
||||||
# GitHub Backend Configuration
|
# GitHub Backend Configuration
|
||||||
|
|
||||||
@ -10,6 +16,20 @@ default_repository: str = ".github"
|
|||||||
|
|
||||||
# API Versioning
|
# API Versioning
|
||||||
|
|
||||||
|
api_versions: dict[str, list[str]] = {
|
||||||
|
"old": ["compat"],
|
||||||
|
"v2": [
|
||||||
|
"announcements",
|
||||||
|
"donations",
|
||||||
|
"github",
|
||||||
|
"info",
|
||||||
|
"login",
|
||||||
|
"ping",
|
||||||
|
"socials",
|
||||||
|
"manager",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
api_version: str = "v2"
|
api_version: str = "v2"
|
||||||
openapi_version: str = "2.0.0"
|
openapi_version: str = "2.0.0"
|
||||||
openapi_title: str = "ReVanced API"
|
openapi_title: str = "ReVanced API"
|
||||||
|
1437
poetry.lock
generated
1437
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -15,25 +15,26 @@ asyncstdlib = "^3.10.9"
|
|||||||
pydantic = "^1.10.13"
|
pydantic = "^1.10.13"
|
||||||
cytoolz = "^0.12.2"
|
cytoolz = "^0.12.2"
|
||||||
beautifulsoup4 = "^4.12.2"
|
beautifulsoup4 = "^4.12.2"
|
||||||
setuptools = "^68.1.2"
|
setuptools = "^69.0.2"
|
||||||
lxml = "^4.9.3"
|
lxml = "^4.9.3"
|
||||||
mypy = "^1.6.1"
|
mypy = "^1.7.0"
|
||||||
types-ujson = "^5.8.0.1"
|
types-ujson = "^5.8.0.1"
|
||||||
types-aiofiles = "^23.2.0.0"
|
types-aiofiles = "^23.2.0.0"
|
||||||
sanic-testing = "^23.6.0"
|
sanic-testing = "^23.6.0"
|
||||||
pytest-asyncio = "^0.21.1"
|
pytest-asyncio = "^0.21.1"
|
||||||
types-beautifulsoup4 = "^4.12.0.6"
|
types-beautifulsoup4 = "^4.12.0.7"
|
||||||
pytest-md = "^0.2.0"
|
pytest-md = "^0.2.0"
|
||||||
pytest-emoji = "^0.2.0"
|
pytest-emoji = "^0.2.0"
|
||||||
coverage = "^7.3.2"
|
coverage = "^7.3.2"
|
||||||
pytest-cov = "^4.1.0"
|
pytest-cov = "^4.1.0"
|
||||||
pytest = "^7.4.0"
|
pytest = "^7.4.3"
|
||||||
sqlalchemy = "^2.0.22"
|
sqlalchemy = "^2.0.23"
|
||||||
sanic-beskar = "^2.2.12"
|
sanic-beskar = "^2.2.12"
|
||||||
bson = "^0.5.10"
|
bson = "^0.5.10"
|
||||||
fastpbkdf2 = "^0.2"
|
fastpbkdf2 = "^0.2"
|
||||||
cryptography = "^41.0.4"
|
cryptography = "^41.0.5"
|
||||||
sanic-limiter = { git = "https://github.com/Omegastick/sanic-limiter" }
|
sanic-limiter = { git = "https://github.com/Omegastick/sanic-limiter" }
|
||||||
|
sentry-sdk = { extras = ["sanic"], version = "^1.35.0" }
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
asyncio_mode = "auto"
|
asyncio_mode = "auto"
|
||||||
|
@ -1,35 +1,34 @@
|
|||||||
aiodns==3.1.1 ; python_version >= "3.11" and python_version < "4.0"
|
aiodns==3.1.1 ; (sys_platform == "linux" or sys_platform == "darwin") and python_version >= "3.11" and python_version < "4.0"
|
||||||
aiofiles==23.2.1 ; python_version >= "3.11" and python_version < "4.0"
|
aiofiles==23.2.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
aiohttp[speedups]==3.8.6 ; python_version >= "3.11" and python_version < "4.0"
|
aiohttp[speedups]==3.9.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
aiosignal==1.3.1 ; python_version >= "3.11" and python_version < "4.0"
|
aiosignal==1.3.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
anyio==4.0.0 ; python_version >= "3.11" and python_version < "4.0"
|
anyio==4.0.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
argon2-cffi==23.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
argon2-cffi==23.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
argon2-cffi-bindings==21.2.0 ; python_version >= "3.11" and python_version < "4.0"
|
argon2-cffi-bindings==21.2.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
async-timeout==4.0.3 ; python_version >= "3.11" and python_version < "4.0"
|
|
||||||
asyncstdlib==3.10.9 ; python_version >= "3.11" and python_version < "4.0"
|
asyncstdlib==3.10.9 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
attrs==23.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
attrs==23.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
beautifulsoup4==4.12.2 ; python_version >= "3.11" and python_version < "4.0"
|
beautifulsoup4==4.12.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
brotli==1.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
brotli==1.1.0 ; platform_python_implementation == "CPython" and python_version >= "3.11" and python_version < "4.0"
|
||||||
|
brotlicffi==1.1.0.0 ; platform_python_implementation != "CPython" and python_version >= "3.11" and python_version < "4.0"
|
||||||
bson==0.5.10 ; python_version >= "3.11" and python_version < "4.0"
|
bson==0.5.10 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
certifi==2023.7.22 ; python_version >= "3.11" and python_version < "4.0"
|
certifi==2023.11.17 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
cffi==1.16.0 ; python_version >= "3.11" and python_version < "4.0"
|
cffi==1.16.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
charset-normalizer==3.3.0 ; python_version >= "3.11" and python_version < "4.0"
|
|
||||||
colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "win32"
|
colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "win32"
|
||||||
coverage==7.3.2 ; python_version >= "3.11" and python_version < "4.0"
|
coverage==7.3.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
coverage[toml]==7.3.2 ; python_version >= "3.11" and python_version < "4.0"
|
coverage[toml]==7.3.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
cryptography==41.0.4 ; python_version >= "3.11" and python_version < "4.0"
|
cryptography==41.0.5 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
cytoolz==0.12.2 ; python_version >= "3.11" and python_version < "4.0"
|
cytoolz==0.12.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
deprecated==1.2.14 ; python_version >= "3.11" and python_version < "4.0"
|
deprecated==1.2.14 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
fastpbkdf2==0.2 ; python_version >= "3.11" and python_version < "4.0"
|
fastpbkdf2==0.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
frozenlist==1.4.0 ; python_version >= "3.11" and python_version < "4.0"
|
frozenlist==1.4.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
greenlet==3.0.0 ; python_version >= "3.11" and python_version < "4.0" and platform_machine == "aarch64" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "ppc64le" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "x86_64" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "amd64" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "AMD64" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "win32" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "WIN32"
|
greenlet==3.0.1 ; python_version >= "3.11" and python_version < "4.0" and platform_machine == "aarch64" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "ppc64le" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "x86_64" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "amd64" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "AMD64" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "win32" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "WIN32"
|
||||||
h11==0.14.0 ; python_version >= "3.11" and python_version < "4.0"
|
h11==0.14.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
html5tagger==1.3.0 ; python_version >= "3.11" and python_version < "4.0"
|
html5tagger==1.3.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
httpcore==0.18.0 ; python_version >= "3.11" and python_version < "4.0"
|
httpcore==1.0.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
httptools==0.6.1 ; python_version >= "3.11" and python_version < "4.0"
|
httptools==0.6.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
httpx==0.25.0 ; python_version >= "3.11" and python_version < "4.0"
|
httpx==0.25.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
idna==3.4 ; python_version >= "3.11" and python_version < "4.0"
|
idna==3.4 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
importlib-resources==6.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
importlib-resources==6.1.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
iniconfig==2.0.0 ; python_version >= "3.11" and python_version < "4.0"
|
iniconfig==2.0.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
iso8601==2.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
iso8601==2.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
jinja2==3.1.2 ; python_version >= "3.11" and python_version < "4.0"
|
jinja2==3.1.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
@ -37,20 +36,20 @@ limits==3.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
|||||||
lxml==4.9.3 ; python_version >= "3.11" and python_version < "4.0"
|
lxml==4.9.3 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
markupsafe==2.1.3 ; python_version >= "3.11" and python_version < "4.0"
|
markupsafe==2.1.3 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
multidict==6.0.4 ; python_version >= "3.11" and python_version < "4.0"
|
multidict==6.0.4 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
mypy==1.6.1 ; python_version >= "3.11" and python_version < "4.0"
|
mypy==1.7.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
mypy-extensions==1.0.0 ; python_version >= "3.11" and python_version < "4.0"
|
mypy-extensions==1.0.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
packaging==23.2 ; python_version >= "3.11" and python_version < "4.0"
|
packaging==23.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
passlib==1.7.4 ; python_version >= "3.11" and python_version < "4.0"
|
passlib==1.7.4 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
pendulum==2.1.2 ; python_version >= "3.11" and python_version < "4.0"
|
pendulum==2.1.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
pluggy==1.3.0 ; python_version >= "3.11" and python_version < "4.0"
|
pluggy==1.3.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
py-buzz==4.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
py-buzz==4.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
pycares==4.4.0 ; python_version >= "3.11" and python_version < "4.0"
|
pycares==4.4.0 ; (sys_platform == "linux" or sys_platform == "darwin") and python_version >= "3.11" and python_version < "4.0"
|
||||||
pycparser==2.21 ; python_version >= "3.11" and python_version < "4.0"
|
pycparser==2.21 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
pycryptodomex==3.19.0 ; python_version >= "3.11" and python_version < "4.0"
|
pycryptodomex==3.19.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
pydantic==1.10.13 ; python_version >= "3.11" and python_version < "4.0"
|
pydantic==1.10.13 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
pyjwt==2.8.0 ; python_version >= "3.11" and python_version < "4.0"
|
pyjwt==2.8.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
pyseto==1.7.5 ; python_version >= "3.11" and python_version < "4.0"
|
pyseto==1.7.6 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
pytest==7.4.2 ; python_version >= "3.11" and python_version < "4.0"
|
pytest==7.4.3 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
pytest-asyncio==0.21.1 ; python_version >= "3.11" and python_version < "4.0"
|
pytest-asyncio==0.21.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
pytest-cov==4.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
pytest-cov==4.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
pytest-emoji==0.2.0 ; python_version >= "3.11" and python_version < "4.0"
|
pytest-emoji==0.2.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
@ -65,20 +64,22 @@ sanic-limiter @ git+https://github.com/Omegastick/sanic-limiter ; python_version
|
|||||||
sanic-routing==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
sanic-routing==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
sanic-testing==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
sanic-testing==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
sanic[ext]==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
sanic[ext]==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
|
sentry-sdk[sanic]==1.36.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
setuptools==68.2.2 ; python_version >= "3.11" and python_version < "4.0"
|
setuptools==68.2.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
six==1.16.0 ; python_version >= "3.11" and python_version < "4.0"
|
six==1.16.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
sniffio==1.3.0 ; python_version >= "3.11" and python_version < "4.0"
|
sniffio==1.3.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
soupsieve==2.5 ; python_version >= "3.11" and python_version < "4.0"
|
soupsieve==2.5 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
sqlalchemy==2.0.22 ; python_version >= "3.11" and python_version < "4.0"
|
sqlalchemy==2.0.23 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
toolz==0.12.0 ; python_version >= "3.11" and python_version < "4.0"
|
toolz==0.12.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
tracerite==1.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
tracerite==1.1.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
types-aiofiles==23.2.0.0 ; python_version >= "3.11" and python_version < "4.0"
|
types-aiofiles==23.2.0.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
types-beautifulsoup4==4.12.0.6 ; python_version >= "3.11" and python_version < "4.0"
|
types-beautifulsoup4==4.12.0.7 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
types-html5lib==1.1.11.15 ; python_version >= "3.11" and python_version < "4.0"
|
types-html5lib==1.1.11.15 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
types-ujson==5.8.0.1 ; python_version >= "3.11" and python_version < "4.0"
|
types-ujson==5.8.0.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
typing-extensions==4.8.0 ; python_version >= "3.11" and python_version < "4.0"
|
typing-extensions==4.8.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
ujson==5.8.0 ; python_version >= "3.11" and python_version < "4.0"
|
ujson==5.8.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
uvloop==0.18.0 ; sys_platform != "win32" and implementation_name == "cpython" and python_version >= "3.11" and python_version < "4.0"
|
urllib3==2.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
websockets==11.0.3 ; python_version >= "3.11" and python_version < "4.0"
|
uvloop==0.19.0 ; sys_platform != "win32" and implementation_name == "cpython" and python_version >= "3.11" and python_version < "4.0"
|
||||||
wrapt==1.15.0 ; python_version >= "3.11" and python_version < "4.0"
|
websockets==12.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
yarl==1.9.2 ; python_version >= "3.11" and python_version < "4.0"
|
wrapt==1.16.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
|
yarl==1.9.3 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
|
BIN
static/img/favicon.ico
Normal file
BIN
static/img/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
Loading…
x
Reference in New Issue
Block a user