Release v2

This commit is contained in:
2026-07-16 02:12:29 -03:00
parent 6f98163632
commit 76cbd799ce
47 changed files with 2409 additions and 2810 deletions
+158
View File
@@ -0,0 +1,158 @@
from enum import StrEnum
from typing import Annotated, Self
from httpx import HTTPStatusError
from fastapi import APIRouter, Query, Response
from pydantic import BaseModel, Field, model_validator
from nyekiapi.lib import settings
from nyekiapi.api.v2.errors import NotFoundError, BadGatewayError, InternalServerError
from nyekiapi.api.v2.schemas import ErrorSchema
from nyekiapi.api.v2.profiles.schemas import ProfileSchema
from nyekiapi.lib.integrations import discord, github, Profile
router = APIRouter(tags=["Profiles"])
sources = []
if settings.DISCORD_TOKEN:
sources.append(("DISCORD", "discord"))
if settings.GITHUB_TOKEN:
sources.append(("GITHUB", "github"))
ProfileSource = StrEnum("ProfileSource", sources)
class ProfileQuerySchema(BaseModel):
source: ProfileSource = Field(..., description="The source of the profile.")
source_id: str | None = Field(
None,
description='The ID of the profile in the source.\nSupported by: `"discord"` `"github"`',
)
source_name: str | None = Field(
None,
description='The name of the profile in the source.\nSupported by: `"github"`',
)
@model_validator(mode="after")
def validate_source(self) -> Self:
if self.source_id and self.source_name:
raise ValueError(
"Both source_id and source_name cannot be provided at the same time."
)
if self.source.value == "discord" and not self.source_id:
raise ValueError("source_id is required for Discord profiles.")
if self.source.value == "github" and not (self.source_name or self.source_id):
raise ValueError(
"Either source_name or source_id is required for GitHub profiles."
)
return self
ProfileQuery = Annotated[ProfileQuerySchema, Query()]
async def _get_profile(profile: ProfileQuerySchema) -> Profile:
try:
if profile.source.value == "discord":
return await discord.get_profile_by_id(profile.source_id)
elif profile.source.value == "github":
if profile.source_id:
return await github.get_profile_by_id(profile.source_id)
else:
return await github.get_profile_by_name(profile.source_name)
except HTTPStatusError as e:
if e.response.status_code == 404:
raise NotFoundError("profile") from e
elif e.response.status_code >= 500:
raise BadGatewayError() from e
else:
raise InternalServerError() from e
@router.get(
"/profile",
responses={
404: {"model": ErrorSchema},
422: {"model": ErrorSchema},
500: {"model": ErrorSchema},
502: {"model": ErrorSchema},
},
)
async def get_profile(profile: ProfileQuery) -> ProfileSchema:
"""Returns basic information about a profile from a given source."""
profile = await _get_profile(profile)
return ProfileSchema.from_integration(profile)
@router.get(
"/profile/avatar",
responses={
404: {"model": ErrorSchema},
422: {"model": ErrorSchema},
500: {"model": ErrorSchema},
502: {"model": ErrorSchema},
},
status_code=302,
)
async def get_profile_avatar(response: Response, profile: ProfileQuery) -> None:
"""Redirects to the avatar URL of a profile from a given source."""
profile = await _get_profile(profile)
response.headers["Location"] = profile.avatar_url
@router.get(
"/profile/banner",
responses={
404: {"model": ErrorSchema},
422: {"model": ErrorSchema},
500: {"model": ErrorSchema},
502: {"model": ErrorSchema},
},
status_code=302,
)
async def get_profile_banner(response: Response, profile: ProfileQuery) -> None:
"""Redirects to the banner URL of a profile from a given source."""
profile = await _get_profile(profile)
if not profile.banner_url:
raise NotFoundError("profile banner")
response.headers["Location"] = profile.banner_url
@router.get(
"/profile/url",
responses={
404: {"model": ErrorSchema},
422: {"model": ErrorSchema},
500: {"model": ErrorSchema},
502: {"model": ErrorSchema},
},
status_code=302,
)
async def get_profile_url(response: Response, profile: ProfileQuery) -> None:
"""Redirects to the profile URL of a profile from a given source."""
profile = await _get_profile(profile)
response.headers["Location"] = profile.url
+29
View File
@@ -0,0 +1,29 @@
from pydantic import BaseModel, Field
from nyekiapi.lib.integrations import Profile
class ProfileSchema(BaseModel):
id: str = Field(
..., description="The unique identifier of the profile at the source."
)
url: str = Field(..., description="The URL of the profile at the source.")
name: str = Field(..., description="The display name of the profile at the source.")
username: str = Field(..., description="The username of the profile at the source.")
avatar_url: str = Field(
..., description="The avatar URL of the profile at the source."
)
banner_url: str | None = Field(
None, description="The banner URL of the profile at the source."
)
@classmethod
def from_integration(cls, profile: Profile) -> "ProfileSchema":
return cls(
id=profile.id,
url=profile.url,
name=profile.name,
username=profile.username,
avatar_url=profile.avatar_url,
banner_url=profile.banner_url,
)