Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2 version of the package #7

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion nekosbest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""

__version__ = "1.1.10"
__version__ = "2.0.0a"
__author__ = "PredaaA"
__copyright__ = "Copyright 2021-present PredaaA"

Expand Down
62 changes: 47 additions & 15 deletions nekosbest/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,73 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""

from typing import List, Union
from typing import List, Optional

from .errors import InvalidAmount, UnknownCategory
from .http import HttpClient
from .models import CATEGORIES, Result
from .models import Categories, CategoryEndpoint, Result

# TODO: Add Ruff


class Client:
"""Client to make requests to nekos.best API."""

def __init__(self):
self.http = HttpClient()
def __init__(self) -> None:
self.http: HttpClient = HttpClient()

async def get_image(self, category: str, amount: int = 1) -> List[Result]:
"""
|coro|
async def close(self) -> None:
"""Closes the client."""
await self.http.session.close()

Returns an image URL of a specific category.
async def fetch(self, category: Optional[Categories] = None, amount: int = 1) -> List[Result]:
"""Returns one or multiple images URLs of a specific category along with their metadata.

Parameters
----------
category: str
category: Optional[Categories]
The category of image you want to get.
If not specified, it will return a random image.
Defaults to None which therefore will be a random image.
amount: int
The amount of images. Must be between 1 and 20.
Defaults to 1.

Returns
-------
List[Result]
"""
if not category in CATEGORIES:
raise ValueError(
f"This isn't a valid category. It must be one of the following: {', '.join(CATEGORIES)}."
if category is None:
category = Categories.random()

if not Categories.is_valid(category):
raise UnknownCategory(
f"This isn't a valid category. It must be one of the following: {', '.join(Categories.__members__)}."
)
if not 1 <= amount <= 20:
raise ValueError("Amount parameter must be between 1 and 20.")
raise InvalidAmount("Amount parameter must be between 1 and 20.")

endpoint = CategoryEndpoint(category, amount)
response = await self.http.get_results(endpoint)
return [Result(result) for result in response["results"]]

data = await self.http.get(category, amount)
return Result(data["results"][0]) if amount == 1 else [Result(r) for r in data["results"]]
async def fetch_file(
self, category: Optional[Categories] = None, amount: int = 1
) -> List[Result]:
"""Returns one or multiple images bytes of a specific category along with their metadata.

Parameters
----------
category: Optional[Categories]
The category of image you want to get.
If not specified, it will return a random image.
Defaults to None which therefore will be a random image.
amount: int
The amount of images. Must be between 1 and 20.
Defaults to 1.

Returns
-------
List[Result]
"""
# TODO
8 changes: 8 additions & 0 deletions nekosbest/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ class NekosBestBaseError(Exception):
"""Base error of nekosbest client."""


class UnknownCategory(NekosBestBaseError):
"""Raised when an unknown category is passed."""


class InvalidAmount(NekosBestBaseError):
"""Raised when an invalid amount is passed."""


class NotFound(NekosBestBaseError):
"""Raised when API returns a 404."""

Expand Down
48 changes: 31 additions & 17 deletions nekosbest/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,43 @@
from nekosbest import __version__

from .errors import APIError, ClientError, NotFound
from .models import CategoryEndpoint, SearchEndpoint

if TYPE_CHECKING:
from .types import ResultType


class HttpClient:
BASE_URL = "https://nekos.best/api/v2"
DEFAULT_HEADERS = {
"User-Agent": f"nekosbest.py v{__version__} (Python/{(platform.python_version())[:3]} aiohttp/{aiohttp.__version__})"
}
def __init__(self) -> None:
self.session: aiohttp.ClientSession = aiohttp.ClientSession(
headers={
"User-Agent": f"nekosbest.py v{__version__} (Python/{(platform.python_version())[:3]} aiohttp/{aiohttp.__version__})"
}
)

async def get(self, endpoint: str, amount: int, **kwargs) -> ResultType:
async def get_results(self, endpoint: CategoryEndpoint) -> ResultType:
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{self.BASE_URL}/{endpoint}",
params={"amount": amount} if amount > 1 else {},
headers=self.DEFAULT_HEADERS,
) as resp:
if resp.status == 404:
raise NotFound()
if resp.status != 200:
raise APIError(resp.status)
return await resp.json(content_type=None)
async with self.session.get(endpoint.formatted) as resp:
if resp.status == 404:
raise NotFound
if resp.status != 200:
raise APIError(resp.status)

return await resp.json()
except aiohttp.ClientConnectionError:
raise ClientError

async def get_search_results(self, endpoint: SearchEndpoint) -> ResultType:
...
# TODO

async def get_file(self, image_url: str) -> bytes:
# Add a idiot proof check here
try:
async with self.session.get(image_url) as resp:
if resp.status != 200:
raise APIError(resp.status)

return await resp.read()
except aiohttp.ClientConnectionError:
raise ClientError()
raise ClientError
182 changes: 139 additions & 43 deletions nekosbest/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,54 +18,148 @@

from __future__ import annotations

import random
from enum import Enum, IntEnum
from typing import TYPE_CHECKING, Optional


if TYPE_CHECKING:
from .types import ResultType

CATEGORIES = (
"baka",
"bite",
"blush",
"bored",
"cry",
"cuddle",
"dance",
"facepalm",
"feed",
"happy",
"highfive",
"hug",
"kiss",
"laugh",
"neko",
"pat",
"poke",
"pout",
"shrug",
"slap",
"sleep",
"smile",
"smug",
"stare",
"think",
"thumbsup",
"tickle",
"wave",
"wink",
"kitsune",
"waifu",
"handhold",
"kick",
"punch",
"shoot",
"husbando",
"yeet",
"nod",
"nom",
"nope"
)

BASE_URL = "https://nekos.best/api/v2"


class Categories(Enum):
"""Represents the categories of images you can get from the API."""

# Static images
neko = "neko"
kitsune = "kitsune"
waifu = "waifu"
husbando = "husbando"

# Gifs
baka = "baka"
bite = "bite"
blush = "blush"
bored = "bored"
cry = "cry"
cuddle = "cuddle"
dance = "dance"
facepalm = "facepalm"
feed = "feed"
happy = "happy"
highfive = "highfive"
hug = "hug"
kiss = "kiss"
laugh = "laugh"
pat = "pat"
poke = "poke"
pout = "pout"
shrug = "shrug"
slap = "slap"
sleep = "sleep"
smile = "smile"
smug = "smug"
stare = "stare"
think = "think"
thumbsup = "thumbsup"
tickle = "tickle"
wave = "wave"
wink = "wink"
handhold = "handhold"
kick = "kick"
punch = "punch"
shoot = "shoot"
yeet = "yeet"
nod = "nod"
nom = "nom"
nope = "nope"

@classmethod
def is_valid(cls, category: str) -> bool:
"""Checks if a category is valid.

Parameters
----------
category: str
The category to check.

Returns
-------
bool
Whether the category is valid.
"""

return category in cls.__members__

@classmethod
def random(cls) -> Categories:
"""Gets a random category."""
return random.choice(list(cls))


class SearchTypes(IntEnum):
"""Represents the types of search you can do with the API."""

image = 1
gif = 2


class CategoryEndpoint:
"""Represents an category endpoint from the API.

Attributes
----------
category: str
The category of the endpoint.
amount: int
The amount of images to get from the endpoint.
"""

__slots__ = ("category", "amount")

def __init__(self, category: str, amount: int = 1):
self.category: str = category
self.amount: int = amount

def __repr__(self) -> str:
return f"<CategoryEndpoint category={self.category} amount={self.amount}>"

@property
def formatted(self) -> str:
return f"{BASE_URL}/{self.category}?amount={self.amount}"


class SearchEndpoint:
"""Represents an search endpoint from the API.

Attributes
----------
query: str
The query to search for.
type: SearchTypes
The type of images to return.
category: str
The category of the images to return.
amount: int
The amount of images to get from the endpoint.
"""

__slots__ = ("query", "type", "category", "amount")

def __init__(self, query: str, type: SearchTypes, category: str, amount: int = 1):
self.query: str = query
self.type: SearchTypes = type
self.category: str = category
self.amount: int = amount

def __repr__(self) -> str:
return f"<SearchEndpoint query={self.query} type={self.type} category={self.category} amount={self.amount}>"

@property
def formatted(self) -> str:
return f"{BASE_URL}/search?query={self.query}&type={self.type}&category={self.category}&amount={self.amount}"


class Result:
Expand All @@ -75,6 +169,8 @@ class Result:
----------
url: Optional[str]
The image / gif URL.
data: Optional[bytes]
The image / gif bytes.
artist_href: Optional[str]
The artist's page URL.
artist_name: Optional[str]
Expand Down
6 changes: 3 additions & 3 deletions nekosbest/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@
from typing import List, TypedDict, Union


class RandomGifsType(TypedDict):
class GifsType(TypedDict):
anime_name: str
url: str


class NekoType(TypedDict):
class ImagesType(TypedDict):
artist_href: str
artist_name: str
source_url: str
url: str


class ResultType(TypedDict):
results: List[Union[NekoType, RandomGifsType]]
results: List[Union[ImagesType, GifsType]]
Loading