Merge pull request #126 from ReVanced/dev

feat: manager endpoints and more
This commit is contained in:
Alexandre Teles (afterSt0rm) 2023-11-22 13:15:07 -03:00 committed by GitHub
commit 71f81f7f20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 980 additions and 964 deletions

View File

@ -12,7 +12,7 @@ repos:
- id: check-toml
- id: check-merge-conflict
- repo: https://github.com/psf/black
rev: 23.9.1
rev: 23.11.0
hooks:
- id: black
language_version: python3.11

View File

@ -1,3 +1,10 @@
{
"python.analysis.typeCheckingMode": "off"
"python.analysis.typeCheckingMode": "off",
"spellright.language": [
"pt"
],
"spellright.documentTypes": [
"markdown",
"latex"
]
}

View File

@ -1,7 +1,10 @@
FROM python:3.11-slim
ARG GITHUB_TOKEN
ARG SENTRY_DSN
ENV GITHUB_TOKEN $GITHUB_TOKEN
ENV SENTRY_DSN $SENTRY_DSN
WORKDIR /usr/src/app

View File

@ -1,25 +1,27 @@
# api/__init__.py
from sanic import Blueprint
import importlib
import pkgutil
from api.utils.versioning import get_version
from api.github import github
from api.ping import ping
from api.socials import socials
from api.info import info
from api.compat import github as compat
from api.donations import donations
from api.announcements import announcements
from api.login import login
from api.robots import robots
# Dynamically import all modules in the 'api' package, excluding subdirectories
versioned_blueprints = {}
for finder, module_name, ispkg in pkgutil.iter_modules(["api"]):
if not ispkg:
# Import the module
module = importlib.import_module(f"api.{module_name}")
api = Blueprint.group(
login,
ping,
github,
info,
socials,
donations,
announcements,
compat,
robots,
url_prefix="/",
)
# Add the module's blueprint to the versioned list, if it exists
if hasattr(module, module_name):
blueprint = getattr(module, module_name)
version = get_version(module_name)
versioned_blueprints.setdefault(version, []).append(blueprint)
# Create Blueprint groups for each version
api = []
for version, blueprints in versioned_blueprints.items():
if version == "old":
group = Blueprint.group(*blueprints, url_prefix="/")
else:
group = Blueprint.group(*blueprints, version=version, url_prefix="/")
api.append(group)

View File

@ -10,6 +10,7 @@ Routes:
- DELETE /announcements/<announcement_id:int>: Delete an announcement.
"""
import os
import datetime
from sanic import Blueprint, Request
from sanic.response import JSONResponse, json
@ -20,10 +21,9 @@ from data.models import AnnouncementDbModel, AttachmentDbModel
import sanic_beskar
from api.models.announcements import AnnouncementResponseModel
from config import api_version
from limiter import limiter
from api.utils.limiter import limiter
announcements: Blueprint = Blueprint("announcements", version=api_version)
announcements: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
@announcements.get("/announcements")

View File

@ -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)

View File

@ -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)

View File

@ -1,5 +1,4 @@
import asyncio
from json import loads
import os
from operator import eq
from typing import Optional
@ -7,9 +6,9 @@ from typing import Optional
import ujson
from aiohttp import ClientResponse
from sanic import SanicException
from toolz import filter, map, partial
from toolz.dicttoolz import get_in, keyfilter
from toolz.itertoolz import mapcat, pluck
from cytoolz import filter, map, partial, curry, pipe
from cytoolz.dicttoolz import get_in, keyfilter
from cytoolz.itertoolz import mapcat, pluck
from api.backends.backend import Backend, Repository
from api.backends.entities import *
@ -396,3 +395,46 @@ class Github(Backend):
)
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}

View File

@ -9,8 +9,7 @@ Routes:
- GET /patches/<tag:str>: Retrieve a list of patches for a given release tag.
"""
import os
from sanic import Blueprint, Request
from sanic.response import JSONResponse, json
from sanic_ext import openapi
@ -20,12 +19,12 @@ from api.models.github import *
from api.models.compat import ToolsResponseModel, ContributorsResponseModel
from config import compat_repositories, owner
github: Blueprint = Blueprint("old")
compat: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
github_backend: Github = Github()
@github.get("/tools")
@compat.get("/tools")
@openapi.definition(
summary="Get patching tools' latest version.", response=[ToolsResponseModel]
)
@ -62,7 +61,7 @@ async def tools(request: Request) -> JSONResponse:
return json(data, status=200)
@github.get("/contributors")
@compat.get("/contributors")
@openapi.definition(
summary="Get organization-wise contributors.", response=[ContributorsResponseModel]
)

View File

@ -5,14 +5,16 @@ Routes:
- GET /donations: Get ReVanced donation links and wallets.
"""
import os
from sanic import Blueprint, Request
from sanic.response import JSONResponse, json
from sanic_ext import openapi
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")

View File

@ -10,6 +10,7 @@ Routes:
"""
import os
from sanic import Blueprint, Request
from sanic.response import JSONResponse, json
from sanic_ext import openapi
@ -17,9 +18,9 @@ from sanic_ext import openapi
from api.backends.entities import Release, Contributor
from api.backends.github import Github, GithubRepository
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()

View File

@ -5,14 +5,15 @@ Routes:
- GET /info: Get info about the owner of the API.
"""
import os
from sanic import Blueprint, Request
from sanic.response import JSONResponse, json
from sanic_ext import openapi
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")

View File

@ -5,18 +5,16 @@ Routes:
- POST /login: Login to the API
"""
import os
from sanic import Blueprint, Request
from sanic.response import JSONResponse, json
from sanic_ext import openapi
from sanic_beskar.exceptions import AuthenticationError
from auth import beskar
from limiter import limiter
from api.utils.auth import beskar
from api.utils.limiter import limiter
from config import api_version
login: Blueprint = Blueprint("login", version=api_version)
login: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
@login.post("/login")

61
api/manager.py Normal file
View 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)

View File

@ -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
View 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.
"""

View File

@ -5,11 +5,11 @@ Routes:
- HEAD /ping: Ping the API.
"""
import os
from sanic import Blueprint, HTTPResponse, Request, response
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")

View File

@ -1,8 +1,9 @@
import os
from sanic import Blueprint
from sanic.response import text
robots: Blueprint = Blueprint("robots")
robots: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
@robots.get("/robots.txt")

View File

@ -5,14 +5,15 @@ Routes:
- GET /socials: Get ReVanced socials.
"""
import os
from sanic import Blueprint, Request
from sanic.response import JSONResponse, json
from sanic_ext import openapi
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")

8
api/utils/versioning.py Normal file
View 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
View File

@ -1,19 +1,34 @@
# app.py
from sanic import Sanic
import os
from typing import Any
from sanic import HTTPResponse, Sanic
import sanic.response
from sanic_ext import Config
from api import api
from config import *
from config import openapi_title, openapi_version, openapi_description, hostnames
from limiter import configure_limiter
from auth import configure_auth
from api.utils.limiter import configure_limiter
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 = {
"/": "/docs/swagger",
}
app = Sanic("ReVanced-API")
app = Sanic("revanced-api")
app.extend(config=Config(oas_ignore_head=False))
app.ext.openapi.describe(
title=openapi_title,
@ -39,7 +54,7 @@ app.blueprint(api)
# https://sanic.dev/en/guide/how-to/static-redirects.html
def get_static_function(value):
def get_static_function(value) -> Any:
return lambda *_, **__: value
@ -47,13 +62,26 @@ for src, dest in REDIRECTS.items():
app.route(src)(get_static_function(sanic.response.redirect(dest)))
@app.middleware("response")
async def add_cache_control(request, response):
@app.on_request
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"
@app.middleware("response")
async def add_csp(request, response):
@app.on_response
async def add_csp(_, response):
response.headers[
"Content-Security-Policy"
] = "default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;"
app.static(
"/favicon.ico",
"static/img/favicon.ico",
name="favicon",
)

View File

@ -2,6 +2,12 @@
backend: str = "github"
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
@ -10,6 +16,20 @@ default_repository: str = ".github"
# API Versioning
api_versions: dict[str, list[str]] = {
"old": ["compat"],
"v2": [
"announcements",
"donations",
"github",
"info",
"login",
"ping",
"socials",
"manager",
],
}
api_version: str = "v2"
openapi_version: str = "2.0.0"
openapi_title: str = "ReVanced API"

1437
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,32 +8,33 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
aiohttp = {version = "^3.8.6", extras = ["speedups"]}
sanic = {version = "^23.6.0", extras = ["ext"]}
aiohttp = { version = "^3.8.6", extras = ["speedups"] }
sanic = { version = "^23.6.0", extras = ["ext"] }
ujson = "^5.8.0"
asyncstdlib = "^3.10.9"
pydantic = "^1.10.13"
cytoolz = "^0.12.2"
beautifulsoup4 = "^4.12.2"
setuptools = "^68.1.2"
setuptools = "^69.0.2"
lxml = "^4.9.3"
mypy = "^1.6.1"
mypy = "^1.7.0"
types-ujson = "^5.8.0.1"
types-aiofiles = "^23.2.0.0"
sanic-testing = "^23.6.0"
pytest-asyncio = "^0.21.1"
types-beautifulsoup4 = "^4.12.0.6"
types-beautifulsoup4 = "^4.12.0.7"
pytest-md = "^0.2.0"
pytest-emoji = "^0.2.0"
coverage = "^7.3.2"
pytest-cov = "^4.1.0"
pytest = "^7.4.0"
sqlalchemy = "^2.0.22"
pytest = "^7.4.3"
sqlalchemy = "^2.0.23"
sanic-beskar = "^2.2.12"
bson = "^0.5.10"
fastpbkdf2 = "^0.2"
cryptography = "^41.0.4"
cryptography = "^41.0.5"
sanic-limiter = { git = "https://github.com/Omegastick/sanic-limiter" }
sentry-sdk = { extras = ["sanic"], version = "^1.35.0" }
[tool.pytest.ini_options]
asyncio_mode = "auto"

View File

@ -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"
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"
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-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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
@ -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"
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"
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"
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"
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"
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"
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"
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"
pytest==7.4.2 ; 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.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-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"
@ -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-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"
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"
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"
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"
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-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-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"
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"
websockets==11.0.3 ; python_version >= "3.11" and python_version < "4.0"
wrapt==1.15.0 ; python_version >= "3.11" and python_version < "4.0"
yarl==1.9.2 ; python_version >= "3.11" and python_version < "4.0"
urllib3==2.1.0 ; 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"
websockets==12.0 ; 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB