mirror of
https://github.com/ReVanced/revanced-api.git
synced 2026-01-18 08:53:57 +00:00
feat: API Fixes and Adjustments (#23)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
1273f9224b
commit
b18097e030
@@ -4,8 +4,7 @@ from sanic import Blueprint
|
||||
from api.github import github
|
||||
from api.ping import ping
|
||||
from api.socials import socials
|
||||
from api.apkdl import apkdl
|
||||
from api.compat import github as old
|
||||
from api.donations import donations
|
||||
|
||||
api = Blueprint.group(ping, github, socials, donations, apkdl, old, url_prefix="/")
|
||||
api = Blueprint.group(ping, github, socials, donations, old, url_prefix="/")
|
||||
|
||||
@@ -93,17 +93,40 @@ class Contributor(dict):
|
||||
avatar_url: str,
|
||||
html_url: str,
|
||||
contributions: Optional[int] = None,
|
||||
bio: Optional[str] = None,
|
||||
):
|
||||
if contributions:
|
||||
dict.__init__(
|
||||
self,
|
||||
login=login,
|
||||
avatar_url=avatar_url,
|
||||
html_url=html_url,
|
||||
contributions=contributions,
|
||||
)
|
||||
else:
|
||||
dict.__init__(self, login=login, avatar_url=avatar_url, html_url=html_url)
|
||||
match contributions, bio:
|
||||
case None, None:
|
||||
dict.__init__(
|
||||
self, login=login, avatar_url=avatar_url, html_url=html_url, bio=bio
|
||||
)
|
||||
case int(_), None:
|
||||
dict.__init__(
|
||||
self,
|
||||
login=login,
|
||||
avatar_url=avatar_url,
|
||||
html_url=html_url,
|
||||
contributions=contributions,
|
||||
)
|
||||
case None, str(_):
|
||||
dict.__init__(
|
||||
self,
|
||||
login=login,
|
||||
avatar_url=avatar_url,
|
||||
html_url=html_url,
|
||||
bio=bio,
|
||||
)
|
||||
case int(_), str(_):
|
||||
dict.__init__(
|
||||
self,
|
||||
login=login,
|
||||
avatar_url=avatar_url,
|
||||
html_url=html_url,
|
||||
contributions=contributions,
|
||||
bio=bio,
|
||||
)
|
||||
case _:
|
||||
raise ValueError("Invalid arguments")
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import asyncio
|
||||
from json import loads
|
||||
import os
|
||||
from operator import eq
|
||||
from typing import Any, Optional
|
||||
from typing import Optional
|
||||
|
||||
import ujson
|
||||
from aiohttp import ClientResponse
|
||||
from sanic import SanicException
|
||||
from toolz import filter, map
|
||||
from toolz import filter, map, partial
|
||||
from toolz.dicttoolz import get_in, keyfilter
|
||||
from toolz.itertoolz import mapcat
|
||||
from toolz.itertoolz import mapcat, pluck
|
||||
|
||||
from api.backends.backend import Backend, Repository
|
||||
from api.backends.entities import *
|
||||
@@ -95,19 +96,27 @@ class Github(Backend):
|
||||
async def __assemble_contributor(
|
||||
contributor: dict, team_view: bool = False
|
||||
) -> Contributor:
|
||||
if team_view:
|
||||
filter_contributor = keyfilter(
|
||||
lambda key: key in {"login", "avatar_url", "html_url"},
|
||||
contributor,
|
||||
)
|
||||
return Contributor(**filter_contributor)
|
||||
match team_view:
|
||||
case True:
|
||||
keys = {"login", "avatar_url", "html_url", "bio"}
|
||||
case _:
|
||||
keys = {"login", "avatar_url", "html_url", "contributions"}
|
||||
|
||||
filter_contributor = keyfilter(
|
||||
lambda key: key in {"login", "avatar_url", "html_url", "contributions"},
|
||||
lambda key: key in keys,
|
||||
contributor,
|
||||
)
|
||||
|
||||
return Contributor(**filter_contributor)
|
||||
|
||||
@staticmethod
|
||||
async def __validate_request(_response: ClientResponse) -> None:
|
||||
if _response.status != 200:
|
||||
raise SanicException(
|
||||
context=await _response.json(loads=ujson.loads),
|
||||
status_code=_response.status,
|
||||
)
|
||||
|
||||
async def list_releases(
|
||||
self, repository: GithubRepository, per_page: int = 30, page: int = 1
|
||||
) -> list[Release]:
|
||||
@@ -126,11 +135,7 @@ class Github(Backend):
|
||||
response: ClientResponse = await http_get(
|
||||
headers=self.headers, url=list_releases_endpoint
|
||||
)
|
||||
if response.status != 200:
|
||||
raise SanicException(
|
||||
context=await response.json(loads=ujson.loads),
|
||||
status_code=response.status,
|
||||
)
|
||||
await self.__validate_request(response)
|
||||
releases: list[Release] = await asyncio.gather(
|
||||
*map(
|
||||
lambda release: self.__assemble_release(release),
|
||||
@@ -156,11 +161,7 @@ class Github(Backend):
|
||||
response: ClientResponse = await http_get(
|
||||
headers=self.headers, url=release_by_tag_endpoint
|
||||
)
|
||||
if response.status != 200:
|
||||
raise SanicException(
|
||||
context=await response.json(loads=ujson.loads),
|
||||
status_code=response.status,
|
||||
)
|
||||
await self.__validate_request(response)
|
||||
return await self.__assemble_release(await response.json(loads=ujson.loads))
|
||||
|
||||
async def get_latest_release(
|
||||
@@ -179,11 +180,7 @@ class Github(Backend):
|
||||
response: ClientResponse = await http_get(
|
||||
headers=self.headers, url=latest_release_endpoint
|
||||
)
|
||||
if response.status != 200:
|
||||
raise SanicException(
|
||||
context=await response.json(loads=ujson.loads),
|
||||
status_code=response.status,
|
||||
)
|
||||
await self.__validate_request(response)
|
||||
return await self.__assemble_release(await response.json(loads=ujson.loads))
|
||||
|
||||
async def get_latest_pre_release(
|
||||
@@ -202,11 +199,7 @@ class Github(Backend):
|
||||
response: ClientResponse = await http_get(
|
||||
headers=self.headers, url=list_releases_endpoint
|
||||
)
|
||||
if response.status != 200:
|
||||
raise SanicException(
|
||||
context=await response.json(loads=ujson.loads),
|
||||
status_code=response.status,
|
||||
)
|
||||
await self.__validate_request(response)
|
||||
latest_pre_release = next(
|
||||
filter(
|
||||
lambda release: release["prerelease"],
|
||||
@@ -229,11 +222,7 @@ class Github(Backend):
|
||||
response: ClientResponse = await http_get(
|
||||
headers=self.headers, url=contributors_endpoint
|
||||
)
|
||||
if response.status != 200:
|
||||
raise SanicException(
|
||||
context=await response.json(loads=ujson.loads),
|
||||
status_code=response.status,
|
||||
)
|
||||
await self.__validate_request(response)
|
||||
contributors: list[Contributor] = await asyncio.gather(
|
||||
*map(self.__assemble_contributor, await response.json(loads=ujson.loads))
|
||||
)
|
||||
@@ -241,38 +230,43 @@ class Github(Backend):
|
||||
return contributors
|
||||
|
||||
async def get_patches(
|
||||
self, repository: GithubRepository, tag_name: str
|
||||
self, repository: GithubRepository, tag_name: str = "latest", dev: bool = False
|
||||
) -> list[dict]:
|
||||
"""Get a dictionary of patch URLs for a given repository.
|
||||
|
||||
Args:
|
||||
repository (GithubRepository): The repository for which to retrieve patches.
|
||||
tag_name: The name of the release tag.
|
||||
tag_name (str): The name of the release tag.
|
||||
dev (bool): If we should get the latest pre-release instead.
|
||||
|
||||
Returns:
|
||||
list[dict]: A JSON object containing the patches.
|
||||
"""
|
||||
|
||||
async def __fetch_download_url(release: Release) -> str:
|
||||
asset = get_in(["assets"], release)
|
||||
async def __fetch_download_url(_release: Release) -> str:
|
||||
asset = get_in(["assets"], _release)
|
||||
patch_asset = next(
|
||||
filter(lambda x: eq(get_in(["name"], x), "patches.json"), asset), None
|
||||
)
|
||||
return get_in(["browser_download_url"], patch_asset)
|
||||
|
||||
response: ClientResponse = await http_get(
|
||||
headers=self.headers,
|
||||
url=await __fetch_download_url(
|
||||
await self.get_release_by_tag_name(
|
||||
match tag_name:
|
||||
case "latest":
|
||||
match dev:
|
||||
case True:
|
||||
release = await self.get_latest_pre_release(repository)
|
||||
case _:
|
||||
release = await self.get_latest_release(repository)
|
||||
case _:
|
||||
release = await self.get_release_by_tag_name(
|
||||
repository=repository, tag_name=tag_name
|
||||
)
|
||||
),
|
||||
|
||||
response: ClientResponse = await http_get(
|
||||
headers=self.headers,
|
||||
url=await __fetch_download_url(_release=release),
|
||||
)
|
||||
if response.status != 200:
|
||||
raise SanicException(
|
||||
context=await response.json(loads=ujson.loads),
|
||||
status_code=response.status,
|
||||
)
|
||||
await self.__validate_request(response)
|
||||
return ujson.loads(await response.read())
|
||||
|
||||
async def get_team_members(self, repository: GithubRepository) -> list[Contributor]:
|
||||
@@ -285,18 +279,30 @@ class Github(Backend):
|
||||
list[Contributor]: A list of members in the owner organization.
|
||||
"""
|
||||
team_members_endpoint: str = f"{self.base_url}/orgs/{repository.owner}/members"
|
||||
user_info_endpoint: str = f"{self.base_url}/users/"
|
||||
response: ClientResponse = await http_get(
|
||||
headers=self.headers, url=team_members_endpoint
|
||||
)
|
||||
if response.status != 200:
|
||||
raise SanicException(
|
||||
context=await response.json(loads=ujson.loads),
|
||||
status_code=response.status,
|
||||
await self.__validate_request(response)
|
||||
logins: list[str] = list(pluck("login", await response.json()))
|
||||
_http_get = partial(http_get, headers=self.headers)
|
||||
user_data_response: list[dict] = await asyncio.gather(
|
||||
*map(
|
||||
lambda login: _http_get(url=f"{user_info_endpoint}{login}"),
|
||||
logins,
|
||||
)
|
||||
)
|
||||
user_data = await asyncio.gather(
|
||||
*map(
|
||||
lambda _response: _response.json(loads=ujson.loads),
|
||||
user_data_response,
|
||||
)
|
||||
)
|
||||
print(await response.json(loads=ujson.loads))
|
||||
team_members: list[Contributor] = await asyncio.gather(
|
||||
*map(
|
||||
lambda member: self.__assemble_contributor(member, team_view=True),
|
||||
await response.json(loads=ujson.loads),
|
||||
list(user_data),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -315,17 +321,18 @@ class Github(Backend):
|
||||
list[dict[str, str]]: A JSON object containing the releases.
|
||||
"""
|
||||
|
||||
def transform(data, repository):
|
||||
def transform(data: dict, repository: GithubRepository):
|
||||
"""Transforms a dictionary from the input list into a list of dictionaries with the desired structure.
|
||||
|
||||
Args:
|
||||
data(dict): A dictionary from the input list.
|
||||
repository(GithubRepository): The repository for which to retrieve releases.
|
||||
|
||||
Returns:
|
||||
_[list]: A list of dictionaries with the desired structure.
|
||||
"""
|
||||
|
||||
def process_asset(asset):
|
||||
def process_asset(asset: dict) -> dict:
|
||||
"""Transforms an asset dictionary into a new dictionary with the desired structure.
|
||||
|
||||
Args:
|
||||
@@ -353,3 +360,39 @@ class Github(Backend):
|
||||
)
|
||||
|
||||
return list(mapcat(lambda pair: transform(*pair), zip(results, repositories)))
|
||||
|
||||
async def compat_get_contributors(
|
||||
self, repositories: list[GithubRepository]
|
||||
) -> list:
|
||||
"""Get the contributors for a set of repositories (v1 compat).
|
||||
|
||||
Args:
|
||||
repositories (set[GithubRepository]): The repositories for which to retrieve contributors.
|
||||
|
||||
Returns:
|
||||
list[dict[str, str]]: A JSON object containing the contributors.
|
||||
"""
|
||||
|
||||
def transform(data: dict, repository: GithubRepository) -> list:
|
||||
"""Transforms a dictionary from the input list into a list of dictionaries with the desired structure.
|
||||
|
||||
Args:
|
||||
data(dict): A dictionary from the input list.
|
||||
repository(GithubRepository): The repository for which to retrieve contributors.
|
||||
|
||||
Returns:
|
||||
_[list]: A list of dictionaries with the desired structure.
|
||||
"""
|
||||
return {
|
||||
"name": f"{repository.owner}/{repository.name}",
|
||||
"contributors": data,
|
||||
}
|
||||
|
||||
results = await asyncio.gather(
|
||||
*map(
|
||||
lambda repository: self.get_contributors(repository),
|
||||
repositories,
|
||||
)
|
||||
)
|
||||
|
||||
return list(map(lambda pair: transform(*pair), zip(results, repositories)))
|
||||
|
||||
@@ -17,7 +17,7 @@ from sanic_ext import openapi
|
||||
|
||||
from api.backends.github import Github, GithubRepository
|
||||
from api.models.github import *
|
||||
from api.models.compat import ToolsResponseModel
|
||||
from api.models.compat import ToolsResponseModel, ContributorsResponseModel
|
||||
from config import compat_repositories, owner
|
||||
|
||||
github: Blueprint = Blueprint("old")
|
||||
@@ -59,3 +59,29 @@ async def tools(request: Request) -> JSONResponse:
|
||||
}
|
||||
|
||||
return json(data, status=200)
|
||||
|
||||
|
||||
@github.get("/contributors")
|
||||
@openapi.definition(
|
||||
summary="Get organization-wise contributors.", response=[ContributorsResponseModel]
|
||||
)
|
||||
async def contributors(request: Request) -> JSONResponse:
|
||||
"""
|
||||
Retrieve a list of releases for a Github repository.
|
||||
|
||||
**Returns:**
|
||||
- JSONResponse: A Sanic JSONResponse object containing the list of releases.
|
||||
|
||||
**Raises:**
|
||||
- HTTPException: If there is an error retrieving the releases.
|
||||
"""
|
||||
|
||||
data: dict[str, list] = {
|
||||
"repositories": await github_backend.compat_get_contributors(
|
||||
repositories=[
|
||||
GithubRepository(owner=owner, name=repo) for repo in compat_repositories
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
return json(data, status=200)
|
||||
|
||||
@@ -10,7 +10,7 @@ from sanic.response import JSONResponse, json
|
||||
from sanic_ext import openapi
|
||||
|
||||
from api.models.donations import DonationsResponseModel
|
||||
from config import donation_info, api_version
|
||||
from config import api_version, wallets, links
|
||||
|
||||
donations: Blueprint = Blueprint("donations", version=api_version)
|
||||
|
||||
@@ -27,5 +27,10 @@ async def root(request: Request) -> JSONResponse:
|
||||
**Returns:**
|
||||
- JSONResponse: A Sanic JSONResponse instance containing a dictionary with the donation links and wallets.
|
||||
"""
|
||||
data: dict[str, dict] = {"donations": donation_info}
|
||||
data: dict[str, dict] = {
|
||||
"donations": {
|
||||
"wallets": wallets,
|
||||
"links": links,
|
||||
}
|
||||
}
|
||||
return json(data, status=200)
|
||||
|
||||
@@ -82,15 +82,21 @@ async def latest_release(request: Request, repo: str) -> JSONResponse:
|
||||
- HTTPException: If there is an error retrieving the releases.
|
||||
"""
|
||||
|
||||
data: dict[str, Release] = {
|
||||
"release": await github_backend.get_latest_pre_release(
|
||||
repository=GithubRepository(owner=owner, name=repo)
|
||||
)
|
||||
if request.args.get("dev") == "true"
|
||||
else await github_backend.get_latest_release(
|
||||
repository=GithubRepository(owner=owner, name=repo)
|
||||
)
|
||||
}
|
||||
data: dict[str, Release]
|
||||
|
||||
match request.args.get("dev"):
|
||||
case "true":
|
||||
data = {
|
||||
"release": await github_backend.get_latest_pre_release(
|
||||
repository=GithubRepository(owner=owner, name=repo)
|
||||
)
|
||||
}
|
||||
case _:
|
||||
data = {
|
||||
"release": await github_backend.get_latest_release(
|
||||
repository=GithubRepository(owner=owner, name=repo)
|
||||
)
|
||||
}
|
||||
|
||||
return json(data, status=200)
|
||||
|
||||
@@ -165,6 +171,9 @@ async def get_patches(request: Request, tag: str) -> JSONResponse:
|
||||
**Args:**
|
||||
- tag (str): The tag for the patches to be retrieved.
|
||||
|
||||
**Query Parameters:**
|
||||
- dev (bool): Whether or not to retrieve the latest development release.
|
||||
|
||||
**Returns:**
|
||||
- JSONResponse: A Sanic JSONResponse object containing the list of patches.
|
||||
|
||||
@@ -174,9 +183,11 @@ async def get_patches(request: Request, tag: str) -> JSONResponse:
|
||||
|
||||
repo: str = "revanced-patches"
|
||||
|
||||
dev: bool = bool(request.args.get("dev"))
|
||||
|
||||
data: dict[str, list[dict]] = {
|
||||
"patches": await github_backend.get_patches(
|
||||
repository=GithubRepository(owner=owner, name=repo), tag_name=tag
|
||||
repository=GithubRepository(owner=owner, name=repo), tag_name=tag, dev=dev
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
from api.models.github import ContributorsFields
|
||||
|
||||
|
||||
class ToolsResponseFields(BaseModel):
|
||||
@@ -25,3 +26,24 @@ class ToolsResponseModel(BaseModel):
|
||||
"""
|
||||
|
||||
tools: list[ToolsResponseFields]
|
||||
|
||||
|
||||
class ContributorsResponseFields(BaseModel):
|
||||
"""Implements the fields for the /contributors endpoint.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
name: str
|
||||
contributors: list[ContributorsFields]
|
||||
|
||||
|
||||
class ContributorsResponseModel(BaseModel):
|
||||
"""Implements the JSON response model for the /contributors endpoint.
|
||||
|
||||
Args:
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
repositories: list[ContributorsResponseFields]
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class DonationFields(BaseModel):
|
||||
"""
|
||||
A Pydantic BaseModel that represents all the donation links and wallets.
|
||||
"""
|
||||
|
||||
wallets: dict[str, str]
|
||||
links: dict[str, str]
|
||||
|
||||
|
||||
class DonationsResponseModel(BaseModel):
|
||||
"""
|
||||
A Pydantic BaseModel that represents a dictionary of donation links.
|
||||
"""
|
||||
|
||||
donations: dict[str, str]
|
||||
"""
|
||||
A dictionary where the keys are the names of the donation destinations, and
|
||||
the values are the links to services or wallet addresses.
|
||||
"""
|
||||
donations: DonationFields
|
||||
|
||||
@@ -117,6 +117,7 @@ class TeamMemberFields(BaseModel):
|
||||
login: str
|
||||
avatar_url: str
|
||||
html_url: str
|
||||
bio: Optional[str]
|
||||
|
||||
|
||||
class TeamMembersModel(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user