diff --git a/nekosbest/__init__.py b/nekosbest/__init__.py index ca3f7ed..0e1dabd 100644 --- a/nekosbest/__init__.py +++ b/nekosbest/__init__.py @@ -16,7 +16,7 @@ along with this program. If not, see . """ -__version__ = "1.1.10" +__version__ = "2.0.0a" __author__ = "PredaaA" __copyright__ = "Copyright 2021-present PredaaA" diff --git a/nekosbest/client.py b/nekosbest/client.py index 74a3518..2ba9fe6 100644 --- a/nekosbest/client.py +++ b/nekosbest/client.py @@ -16,41 +16,73 @@ along with this program. If not, see . """ -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 diff --git a/nekosbest/errors.py b/nekosbest/errors.py index 7dad13d..0233d89 100644 --- a/nekosbest/errors.py +++ b/nekosbest/errors.py @@ -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.""" diff --git a/nekosbest/http.py b/nekosbest/http.py index fe41f44..bf84d22 100644 --- a/nekosbest/http.py +++ b/nekosbest/http.py @@ -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 diff --git a/nekosbest/models.py b/nekosbest/models.py index 6a18eea..1b1a2a1 100644 --- a/nekosbest/models.py +++ b/nekosbest/models.py @@ -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"" + + @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"" + + @property + def formatted(self) -> str: + return f"{BASE_URL}/search?query={self.query}&type={self.type}&category={self.category}&amount={self.amount}" class Result: @@ -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] diff --git a/nekosbest/types.py b/nekosbest/types.py index 22c3a7b..b8df3a9 100644 --- a/nekosbest/types.py +++ b/nekosbest/types.py @@ -19,12 +19,12 @@ 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 @@ -32,4 +32,4 @@ class NekoType(TypedDict): class ResultType(TypedDict): - results: List[Union[NekoType, RandomGifsType]] + results: List[Union[ImagesType, GifsType]] diff --git a/setup.cfg b/setup.cfg index c4bcf0a..ee79d72 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,7 +32,7 @@ include_package_data = False packages = find_namespace: python_requires = >=3.8 install_requires = - aiohttp>=3.6.2,<3.8.1 + aiohttp>=3.6.2,<3.9.0 [options.packages.find] include =