Python library for using gdz.ru API
#                © Copyright 2023
#         Licensed under the MIT License
#       https://opensource.org/licenses/MIT
#    https://github.com/Den4ikSuperOstryyPer4ik

__version__ = "1.0.0"

src/gdzru/__about__.py
Den4ikSuperOstryyPer4ik/GDZ-py
#                © Copyright 2023
#         Licensed under the MIT License
#       https://opensource.org/licenses/MIT
#    https://github.com/Den4ikSuperOstryyPer4ik

from gdzru import api, exceptions, types
from gdzru.api import AsyncAPI, BaseAPIWrapper, SyncAPI
from gdzru.exceptions import APIError

__all__ = [
    "api",
    "types",
    "exceptions",
    "BaseAPIWrapper",
    "SyncAPI",
    "AsyncAPI",
    "APIError",
]

src/gdzru/__init__.py
Den4ikSuperOstryyPer4ik/GDZ-py
#                © Copyright 2023
#         Licensed under the MIT License
#       https://opensource.org/licenses/MIT
#    https://github.com/Den4ikSuperOstryyPer4ik

from typing import ClassVar, TypeVar

import requests
from aiohttp import ClientResponse, ClientSession
from pydantic import BaseModel

from gdzru.exceptions import APIError
from gdzru.types import BookInfo, FullBookList, TaskInfo
from gdzru.types.type import Type

T = TypeVar("T", Type, BaseModel)


class BaseAPIWrapper:
    """
    A base API requests wrapper.
    """

    BASE_URL = "https://gdz-ru.com"
    HEADERS: ClassVar[dict] = {
        "User-Agent": "okhttp/4.9.1",
    }


class AsyncAPI(BaseAPIWrapper):
    """
    An asynchronous wrapper for the gdz.ru API.
    """

    async def get(self, url: str, response_type: T = None, *, is_file: bool = False, **kwargs) -> T | ClientResponse:
        """
        Asynchronously sends a GET request to the specified URL.

        Args:
            url (str): The URL or path of the method to send the request to.
            response_type (Optional[T]): The expected type of the response. Defaults to None.
            **kwargs: Additional keyword arguments to pass to the request.

        Returns:
            T | Response: The response from the GET request.
                If a response_type is specified, the response will be validated and returned as an instance of that type
                Otherwise, the raw response object is returned.

        Raises:
            gdzru.exceptions.APIError: If the response status >= 400, an APIError is raised with the status code
        """
        url = self.BASE_URL + url if not url.startswith(self.BASE_URL) and not is_file else url

        async with ClientSession(headers=self.HEADERS) as session:
            response = await session.get(url, **kwargs)

        if response.status >= 400:
            raise APIError(response.url, response.status, await response.text())

        return response_type.model_validate_json(await response.text()) if response_type else response

    async def get_books_list(self, country_id: int = 1) -> FullBookList:
        """
        Retrieve the full book list for a specific country.

        Args:
            country_id (int): The ID of the country. Defaults to 1.

        Returns:
            FullBookList: The full book list for the specified country.
        """
        return SyncAPI().get_books_list(country_id=country_id)

    async def get_book(self, url: str) -> BookInfo:
        """
        Retrieves book information from the specified URL.

        Args:
            url (str): The URL of the book.

        Returns:
            gdzru.types.BookInfo: The information of the book.
        """
        return await self.get(url, response_type=BookInfo)

    async def get_attachment(self, url: str) -> bytes:
        """
        Retrieve the contents of an attachment/cover/image from a given URL.

        Args:
            url (str): The URL of the attachment/cover/image.

        Returns:
            bytes: The contents of the attachment.
        """
        return SyncAPI().get_attachment(url=url)

    get_cover = get_image = get_attachment

    async def get_task(self, method: str) -> TaskInfo:
        """
        Retrieves a task using the specified method.

        Args:
            method (str): The method to use for retrieving the task.

        Returns:
            TaskInfo: The retrieved task information.
        """
        return await self.get(method, response_type=TaskInfo)


class SyncAPI(BaseAPIWrapper):
    """
    A synchronous wrapper for the gdz.ru API.
    """

    def get(self, url: str, response_type: T = None, *, is_file: bool = False, **kwargs) -> T | requests.Response:
        """
        Asynchronously sends a GET request to the specified URL.

        Args:
            url (str): The URL or path of the method to send the request to.
            response_type (Optional[T]): The expected type of the response. Defaults to None.
            **kwargs: Additional keyword arguments to pass to the request.

        Returns:
            T | Response: The response from the GET request.
                If a response_type is specified, the response will be validated and returned as an instance of that type
                Otherwise, the raw response object is returned.

        Raises:
            gdzru.exceptions.APIError: If the response status >= 400, an APIError is raised with the status code
        """
        url = self.BASE_URL + url if not url.startswith(self.BASE_URL) and not is_file else url

        response = requests.get(url, timeout=30, headers=self.HEADERS, verify=not is_file, **kwargs)

        if response.status_code >= 400:
            raise APIError(response.url, response.status_code, response.text)

        return response_type.model_validate_json(response.text) if response_type else response

    def get_books_list(self, country_id: int = 1) -> FullBookList:
        """
        Retrieve the full book list for a specific country.

        Args:
            country_id (int): The ID of the country. Defaults to 1.

        Returns:
            FullBookList: The full book list for the specified country.
        """
        return self.get(
            f"/full-book-list?country_id={country_id}",
            response_type=FullBookList
        )

    def get_book(self, url: str) -> BookInfo:
        """
        Retrieves book information from the specified URL.

        Args:
            url (str): The URL of the book.

        Returns:
            gdzru.types.BookInfo: The information of the book.
        """
        return self.get(url, response_type=BookInfo)

    def get_attachment(self, url: str) -> bytes:
        """
        Retrieve the contents of an attachment/cover/image from a given URL.

        Args:
            url (str): The URL of the attachment/cover/image.

        Returns:
            bytes: The contents of the attachment.
        """
        response = self.get(url, is_file=True)
        return response.content

    get_cover = get_image = get_attachment

    def get_task(self, method: str) -> TaskInfo:
        """
        Retrieves a task using the specified method.

        Args:
            method (str): The method to use for retrieving the task.

        Returns:
            TaskInfo: The retrieved task information.
        """
        return self.get(method, response_type=TaskInfo)

src/gdzru/api.py
Den4ikSuperOstryyPer4ik/GDZ-py
#                © Copyright 2023
#         Licensed under the MIT License
#       https://opensource.org/licenses/MIT
#    https://github.com/Den4ikSuperOstryyPer4ik

from typing import Any, Optional


class APIError(Exception):
    """Request API Error"""

    def __init__(self, url: str, status_code: int, details: Optional[Any] = None) -> None:
        error_text = f"RequestError [{status_code}]:\nURL: {url}"

        super().__init__(error_text)

        self.url = url
        self.status_code = status_code
        self.details = details

src/gdzru/exceptions.py
Den4ikSuperOstryyPer4ik/GDZ-py
#                © Copyright 2023
#         Licensed under the MIT License
#       https://opensource.org/licenses/MIT
#    https://github.com/Den4ikSuperOstryyPer4ik

from gdzru.types.book import BookInfo
from gdzru.types.full_book_list import FullBookList
from gdzru.types.task import TaskInfo

__all__ = [
    "BookInfo",
    "FullBookList",
    "TaskInfo",
]

src/gdzru/types/__init__.py
Den4ikSuperOstryyPer4ik/GDZ-py
#                © Copyright 2023
#         Licensed under the MIT License
#       https://opensource.org/licenses/MIT
#    https://github.com/Den4ikSuperOstryyPer4ik

from gdzru.types.type import Type


class Paths(Type):
    action: str | None = None
    controller: str | None = None


class Cover(Type):
    title: str | None = None
    url: str | None = None


class Price(Type):
    purchase: int | None = None
    download: int | None = None


class Book(Type):
    id: int | None = None
    parent_id: int | None = None
    country: str | None = None
    subject_id: int | None = None
    title: str | None = None
    header: str | None = None
    breadcrumb: str | None = None
    year: str | None = None
    classes: list[int] | None = None
    authors: list[str] | None = None
    authors_ru: list[str] | None = None
    authors_en: list[str] | None = None
    authors_by: list | None = None
    authors_ua: list | None = None
    description: str | None = None
    publisher: str | None = None
    category: str | None = None
    series: str | None = None
    subtype: str | None = None
    study_level: str | None = None
    parts: list[str] | None = None
    cover: Cover | None = None
    cover_url: str | None = None
    search_keywords: str | None = None
    price: Price | None = None
    tasks_view: str | None = None
    is_paid: bool | None = None
    url: str | None = None
    priority: int | None = None
    updated_at: int | None = None


class Task(Type):
    title: str | None = None
    title_short: str | None = None
    description: str | None = None
    youtube_video_id: str | None = None
    url: str | None = None


class Topic(Type):
    title: str | None = None
    title_short: str | None = None
    description: str | None = None
    tasks: list[Task] | None = None
    topics: list["Topic"] | None = None


class Structure(Type):
    title: str | None = None
    title_short: str | None = None
    description: str | None = None
    tasks: list[Task] | None = None
    topics: list[Topic] | None = None


class PaidContent(Type):
    watched: int | None = None
    limit: int | None = None
    tasks: list | None = None


class BookInfo(Type):
    success: bool | None = None
    message: str | None = None
    paths: Paths | None = None
    book: Book | None = None
    structure: list[Structure] | None = None
    paid_content: PaidContent | None = None

src/gdzru/types/book.py
Den4ikSuperOstryyPer4ik/GDZ-py
#                © Copyright 2023
#         Licensed under the MIT License
#       https://opensource.org/licenses/MIT
#    https://github.com/Den4ikSuperOstryyPer4ik

from gdzru.types.type import Type


class Class(Type):
    id: int | None = None
    title: str | None = None


class Subject(Type):
    id: int | None = None
    title: str | None = None


class Paths(Type):
    action: str | None = None
    controller: str | None = None


class Cover(Type):
    title: str | None = None
    url: str | None = None


class Price(Type):
    purchase: int | None = None
    download: int | None = None


class Book(Type):
    id: int | None = None
    parent_id: int | None = None
    country: str | None = None
    subject_id: int | None = None
    title: str | None = None
    header: str | None = None
    breadcrumb: str | None = None
    year: str | None = None
    classes: list[int] | None = None
    authors: list[str] | None = None
    authors_ru: list[str] | None = None
    authors_en: list[str] | None = None
    authors_by: list | None = None
    authors_ua: list | None = None
    description: str | None = None
    publisher: str | None = None
    category: str | None = None
    series: str | None = None
    subtype: str | None = None
    study_level: str | None = None
    parts: list[str] | None = None
    cover: Cover | None = None
    cover_url: str | None = None
    search_keywords: str | None = None
    price: Price | None = None
    tasks_view: str | None = None
    is_paid: bool | None = None
    url: str | None = None
    priority: int | None = None
    updated_at: int | None = None


class PaidContent(Type):
    watched: int | None = None
    limit: int | None = None
    tasks: list | None = None


class FullBookList(Type):
    success: bool | None = None
    message: str | None = None
    classes: list[Class] | None = None
    subjects: list[Subject] | None = None
    paths: Paths | None = None
    books: list[Book] | None = None
    description: str | None = None
    paid_content: PaidContent | None = None

src/gdzru/types/full_book_list.py
Den4ikSuperOstryyPer4ik/GDZ-py
#                © Copyright 2023
#         Licensed under the MIT License
#       https://opensource.org/licenses/MIT
#    https://github.com/Den4ikSuperOstryyPer4ik

from gdzru.types.type import Type


class Paths(Type):
    action: str | None = None
    controller: str | None = None


class Cover(Type):
    title: str | None = None
    url: str | None = None


class Price(Type):
    purchase: int | None = None
    download: int | None = None


class Book(Type):
    id: int | None = None
    parent_id: int | None = None
    country: str | None = None
    subject_id: int | None = None
    title: str | None = None
    header: str | None = None
    breadcrumb: str | None = None
    year: str | None = None
    classes: list[int] | None = None
    authors: list[str] | None = None
    authors_ru: list | None = None
    authors_en: list | None = None
    authors_by: list | None = None
    authors_ua: list | None = None
    description: str | None = None
    publisher: str | None = None
    category: str | None = None
    series: str | None = None
    subtype: str | None = None
    study_level: str | None = None
    parts: list[str] | None = None
    cover: Cover | None = None
    cover_url: str | None = None
    search_keywords: str | None = None
    price: Price | None = None
    tasks_view: str | None = None
    is_paid: bool | None = None
    url: str | None = None
    priority: int | None = None
    updated_at: int | None = None


class Task(Type):
    title: str | None = None
    title_short: str | None = None
    description: str | None = None
    youtube_video_id: str | None = None
    url: str | None = None


class Image(Type):
    title: str | None = None
    url: str | None = None


class Edition(Type):
    title: str | None = None
    description: str | None = None
    type: int | None = None
    publisher_id: int | None = None
    texts: list | None = None
    images: list[Image] | None = None
    youtube_videos: list | None = None


class PaidContent(Type):
    watched: int | None = None
    limit: int | None = None
    tasks: list | None = None


class TaskInfo(Type):
    success: bool | None = None
    message: str | None = None
    paths: Paths | None = None
    book: Book | None = None
    task: Task | None = None
    editions: list[Edition] | None = None
    paid_content: PaidContent | None = None

src/gdzru/types/task.py
Den4ikSuperOstryyPer4ik/GDZ-py
#                © Copyright 2023
#         Licensed under the MIT License
#       https://opensource.org/licenses/MIT
#    https://github.com/Den4ikSuperOstryyPer4ik

import json
import typing
from datetime import date, datetime, timedelta
from enum import Enum

from pydantic import BaseModel


class Type(BaseModel):
    @staticmethod
    def __default__(obj: typing.Any):
        if isinstance(obj, (bytes, typing.Match)):
            return repr(obj)
        elif isinstance(obj, (Enum, datetime, date, timedelta)):
            return str(obj)

        return {
            "_": obj.__class__.__name__,
            **{
                attr: getattr(obj, attr)
                for attr in filter(lambda x: not x.startswith("_"), obj.__dict__)
                if getattr(obj, attr) is not None
            },
        }

    def __str__(self) -> str:
        return json.dumps(self, indent=4, default=Type.__default__, ensure_ascii=False)

src/gdzru/types/type.py
Den4ikSuperOstryyPer4ik/GDZ-py
#                © Copyright 2023
#         Licensed under the MIT License
#       https://opensource.org/licenses/MIT
#    https://github.com/Den4ikSuperOstryyPer4ik


tests/__init__.py
Den4ikSuperOstryyPer4ik/GDZ-py