Skip to content

Commit

Permalink
feat: added account/settings endpoints (get, post)
Browse files Browse the repository at this point in the history
- GET `account/settings` gives access to get all information about account settings
- POST `account/settings` gives access to set any of the settings options (you access all options by Pydantic model)
  • Loading branch information
KatantDev committed Dec 26, 2023
1 parent 5728578 commit c742d97
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 35 deletions.
4 changes: 2 additions & 2 deletions ymdantic/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .pydantic_factory import PydanticFactory
from .pydantic_factory import PydanticFactoryBody, PydanticFactoryArgs
from .to_camel import to_camel

__all__ = ("PydanticFactory", "to_camel")
__all__ = ("PydanticFactoryBody", "PydanticFactoryArgs", "to_camel")
13 changes: 11 additions & 2 deletions ymdantic/adapters/pydantic_factory.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from adaptix import Retort, TypeHint
from pydantic import TypeAdapter
from typing import Any, Dict
from typing import Any, Dict, Optional

from ymdantic.models.base import YMBaseModel


class PydanticFactory:
class PydanticFactoryBody:
@staticmethod
def load(data: Dict[str, Any], type_: Any) -> Any:
client = data.pop("__client")
Expand All @@ -17,3 +18,11 @@ def load(data: Dict[str, Any], type_: Any) -> Any:
@staticmethod
def dump(data: Dict[str, Any], type_: Any) -> Any:
return TypeAdapter(type_).dump_python(data)


class PydanticFactoryArgs(Retort):
def dump(self, data: Any, tp: Optional[TypeHint] = None, /) -> Any:
params: Optional[YMBaseModel] = data.pop("params", None)
if params is not None:
return params.model_dump(mode="json", by_alias=True, exclude_none=True)
return super().dump(data, tp)
8 changes: 8 additions & 0 deletions ymdantic/client/session/aiohttp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@

from aiohttp import ClientSession, FormData, ClientError, ClientTimeout
from dataclass_rest.base_client import BaseClient
from dataclass_rest.client_protocol import FactoryProtocol
from dataclass_rest.exceptions import ClientLibraryError
from dataclass_rest.http_request import HttpRequest

from ymdantic.adapters import PydanticFactoryBody, PydanticFactoryArgs
from ymdantic.client.session.aiohttp_method import YMHttpMethod


Expand All @@ -27,6 +29,12 @@ def __init__(
self.timeout: ClientTimeout = timeout or ClientTimeout(total=0)
self._session: Optional[ClientSession] = None

def _init_request_body_factory(self) -> FactoryProtocol:
return PydanticFactoryBody()

def _init_request_args_factory(self) -> FactoryProtocol:
return PydanticFactoryArgs()

@asynccontextmanager
async def context(self, auto_close: bool = True) -> AsyncIterator["AiohttpClient"]:
"""
Expand Down
31 changes: 25 additions & 6 deletions ymdantic/client/yandex_music.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
# mypy: disable-error-code="empty-body"
from typing import List, Union, Optional

from dataclass_rest import get
from dataclass_rest.client_protocol import FactoryProtocol
from dataclass_rest import get, post
from pydantic import HttpUrl

from ymdantic.adapters.pydantic_factory import PydanticFactory
from ymdantic.client.session import AiohttpClient
from ymdantic.exceptions import UndefinedUser
from ymdantic.models import (
Expand All @@ -21,6 +19,8 @@
NewReleasesBlock,
S3FileUrl,
DownloadInfoDirect,
AccountSettings,
SetAccountSettingsBody,
)


Expand All @@ -41,9 +41,6 @@ def __init__(
},
)

def _init_request_body_factory(self) -> FactoryProtocol:
return PydanticFactory()

async def get_track(self, track_id: Union[int, str]) -> TrackType:
response = await self.get_track_request(track_id=track_id)
return response.result[0]
Expand Down Expand Up @@ -202,3 +199,25 @@ async def get_recommended_new_releases(self) -> List[NewRelease]:
@get("landing/block/new-releases")
async def get_recommended_new_releases_request(self) -> NewReleasesResponse:
...

async def get_account_settings(self) -> AccountSettings:
response = await self.get_account_settings_request()
return response.result

@get("account/settings")
async def get_account_settings_request(self) -> Response[AccountSettings]:
...

async def set_account_settings(
self,
settings: SetAccountSettingsBody,
) -> AccountSettings:
response = await self.set_account_settings_request(params=settings)
return response.result

@post("account/settings")
async def set_account_settings_request(
self,
params: SetAccountSettingsBody,
) -> Response[AccountSettings]:
...
36 changes: 20 additions & 16 deletions ymdantic/mixins.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
from typing import Dict, Any, TYPE_CHECKING
from typing_extensions import Self

from pydantic import model_validator, ValidationInfo
from pydantic import model_validator, PrivateAttr, BaseModel

if TYPE_CHECKING:
from ymdantic import YMClient


class ClientMixin:
class ClientMixin(BaseModel):
"""Миксин, добавляющий в Pydantic модель клиент для отправки запросов."""

_client: "YMClient"
_client: "YMClient" = PrivateAttr()

@model_validator(mode="before")
def inject_ym_client(
cls,
obj: Dict[str, Any],
info: ValidationInfo,
) -> Dict[str, Any]:
def model_post_init(self, __context: Any) -> None:
"""
Валидатор, добавляющий в модель клиент для отправки запросов.
После инициализации модели миксин добавляет в данные модели инстанс клиента.
:param obj: Словарь с данными модели.
:param info: Информация о валидации.
:return: Словарь с данными модели.
:param __context: Контекст при валидации модели.
"""
if info.context is not None:
cls._client = info.context.get("client")
return obj
self._client = __context.get("bot") if __context else None

def as_(self, client: "YMClient") -> Self:
"""
Добавляет инстанс бота в уже созданный объект.
Требуется, если мы не добавили его при валидации модели.
:param client: Инстанс клиента
:return: self
"""
self._client = client
return self


class DeprecatedMixin:
Expand Down
3 changes: 3 additions & 0 deletions ymdantic/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)
from .artists import Artist
from .s3 import S3FileUrl
from .account import AccountSettings, SetAccountSettingsBody

# Ребилд моделей с учётом новых изменений. (TrackType)
ShortAlbum.model_rebuild()
Expand All @@ -37,4 +38,6 @@
"NewReleasesResponse",
"NewRelease",
"S3FileUrl",
"AccountSettings",
"SetAccountSettingsBody",
)
3 changes: 3 additions & 0 deletions ymdantic/models/account/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .settings import AccountSettings, SetAccountSettingsBody

__all__ = ("AccountSettings", "SetAccountSettingsBody")
80 changes: 80 additions & 0 deletions ymdantic/models/account/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from datetime import datetime
from typing import Literal, Optional

from ymdantic.models.base import YMBaseModel, YMPostBaseModel


class SetAccountSettingsBody(YMPostBaseModel):
last_fm_scrobbling_enabled: Optional[bool] = None
# Включено ли скробблирование в Last.fm.
facebook_scrobbling_enabled: Optional[bool] = None
# Включено ли скробблирование в Facebook.
shuffle_enabled: Optional[bool] = None
# Включено ли перемешивание.
add_new_track_on_playlist_top: Optional[bool] = None
# Добавлять ли новые треки в начало плейлиста.
volume_percents: Optional[int] = None
# Громкость в процентах.
user_music_visibility: Optional[Literal["PUBLIC", "PRIVATE"]] = None
# Видимость музыки пользователя.
user_social_visibility: Optional[Literal["PUBLIC", "PRIVATE"]] = None
# Видимость социальной активности пользователя.
ads_disabled: Optional[bool] = None
# Отключена ли реклама.
rbt_disabled: Optional[bool] = None
# Отключены ли РБТ.
theme: Optional[Literal["black", "white"]] = None
# Тема.
promos_disabled: Optional[bool] = None
# Отключены ли промо-акции.
auto_play_radio: Optional[bool] = None
# Автовоспроизведение радио.
sync_queue_enabled: Optional[bool] = None
# Включена ли синхронизация очереди.
explicit_forbidden: Optional[bool] = None
# Запрещен ли контент с ненормативной лексикой.
child_mod_enabled: Optional[bool] = None
# Включен ли режим для детей.
wizard_is_passed: Optional[bool] = None
# Прошел ли пользователь визард.


class AccountSettings(YMBaseModel):
"""Pydantic модель настроек аккаунта."""

uid: int
# Уникальный идентификатор пользователя.
last_fm_scrobbling_enabled: bool
# Включено ли скробблирование в Last.fm.
facebook_scrobbling_enabled: bool
# Включено ли скробблирование в Facebook.
shuffle_enabled: bool
# Включено ли перемешивание.
add_new_track_on_playlist_top: bool
# Добавлять ли новые треки в начало плейлиста.
volume_percents: int
# Громкость в процентах.
user_music_visibility: Literal["PUBLIC", "PRIVATE"]
# Видимость музыки пользователя.
user_social_visibility: Literal["PUBLIC", "PRIVATE"]
# Видимость социальной активности пользователя.
ads_disabled: bool
# Отключена ли реклама.
modified: datetime
# Дата последнего изменения настроек.
rbt_disabled: bool
# Отключены ли РБТ.
theme: Literal["black", "white"]
# Тема.
promos_disabled: bool
# Отключены ли промо-акции.
auto_play_radio: bool
# Автовоспроизведение радио.
sync_queue_enabled: bool
# Включена ли синхронизация очереди.
explicit_forbidden: bool
# Запрещен ли контент с ненормативной лексикой.
child_mod_enabled: bool
# Включен ли режим для детей.
wizard_is_passed: bool
# Прошел ли пользователь визард.
16 changes: 14 additions & 2 deletions ymdantic/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,23 @@
from ymdantic.mixins import ClientMixin


class YMBaseModel(BaseModel, ClientMixin):
"""Базовая Pydantic модель для всех будущих моделей."""
class YMBaseModel(ClientMixin, BaseModel):
"""Базовая Pydantic модель для всех будущих моделей (GET)."""

model_config = ConfigDict(
alias_generator=to_camel,
populate_by_name=True,
extra="forbid",
)


class YMPostBaseModel(ClientMixin, BaseModel):
"""Базовая Pydantic модель для всех будущих моделей (POST)."""

model_config = ConfigDict(
alias_generator=to_camel,
populate_by_name=True,
json_encoders={
bool: lambda value: str(value).lower(),
},
)
10 changes: 3 additions & 7 deletions ymdantic/models/response.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
from typing import TypeVar, Generic, TYPE_CHECKING
from typing import TypeVar, Generic

from pydantic import BaseModel, Field

from ymdantic.mixins import ClientMixin

if TYPE_CHECKING:
pass


ResponseVar = TypeVar("ResponseVar")
# Переменная, которая будет использоваться в качестве типа для поля result в
# модели Response. Это позволит нам в дальнейшем указывать тип возвращаемого
Expand All @@ -16,7 +12,7 @@
# будет подставляться другой тип.


class InvocationInfo(BaseModel):
class InvocationInfo(ClientMixin, BaseModel):
"""Pydantic модель, представляющая информацию о вызове."""

hostname: str
Expand All @@ -27,7 +23,7 @@ class InvocationInfo(BaseModel):
# Продолжительность выполнения запроса в миллисекундах.


class Response(BaseModel, Generic[ResponseVar], ClientMixin):
class Response(ClientMixin, BaseModel, Generic[ResponseVar]):
"""Pydantic модель, представляющая ответ."""

invocation_info: InvocationInfo = Field(alias="invocationInfo")
Expand Down

0 comments on commit c742d97

Please sign in to comment.