feat(models): Add embed color model and methods

This commit is contained in:
grey-cat-1908 2022-04-15 11:51:36 +03:00
parent d998a0408d
commit 643a231e9b
14 changed files with 507 additions and 265 deletions

View file

@ -21,8 +21,8 @@ from ..message.message import Message
from ...exceptions import EmbedFieldError from ...exceptions import EmbedFieldError
from ...models.message.embed import Embed from ...models.message.embed import Embed
from ...utils import Snowflake, Timestamp from ...utils import Snowflake, Timestamp
from ...utils import APIModelBase from ...utils.api_model import APIModelBase
from ...utils.types import APINullable from ...utils.types import APINullable, UNDEFINED
if TYPE_CHECKING: if TYPE_CHECKING:
from .thread import ThreadMember, ThreadMetadata from .thread import ThreadMember, ThreadMetadata
@ -162,32 +162,32 @@ class Channel(APIModelBase):
only included when part of the `resolved` data received on a slash command interaction only included when part of the `resolved` data received on a slash command interaction
""" """
id: APINullable[Snowflake] = None id: APINullable[Snowflake] = UNDEFINED
type: APINullable[int] = None type: APINullable[int] = UNDEFINED
guild_id: APINullable[Snowflake] = None guild_id: APINullable[Snowflake] = UNDEFINED
position: APINullable[int] = None position: APINullable[int] = UNDEFINED
permission_overwrites: APINullable[List] = None permission_overwrites: APINullable[List] = UNDEFINED
name: APINullable[str] = None name: APINullable[str] = UNDEFINED
topic: APINullable[str] = None topic: APINullable[str] = UNDEFINED
nsfw: APINullable[bool] = None nsfw: APINullable[bool] = UNDEFINED
last_message_id: APINullable[Snowflake] = None last_message_id: APINullable[Snowflake] = UNDEFINED
bitrate: APINullable[int] = None bitrate: APINullable[int] = UNDEFINED
user_limit: APINullable[int] = None user_limit: APINullable[int] = UNDEFINED
rate_limit_per_user: APINullable[int] = None rate_limit_per_user: APINullable[int] = UNDEFINED
recipients: APINullable[List] = None recipients: APINullable[List] = UNDEFINED
icon: APINullable[str] = None icon: APINullable[str] = UNDEFINED
owner_id: APINullable[Snowflake] = None owner_id: APINullable[Snowflake] = UNDEFINED
application_id: APINullable[Snowflake] = None application_id: APINullable[Snowflake] = UNDEFINED
parent_id: APINullable[Snowflake] = None parent_id: APINullable[Snowflake] = UNDEFINED
last_pin_timestamp: APINullable[Timestamp] = None last_pin_timestamp: APINullable[Timestamp] = UNDEFINED
rtc_region: APINullable[str] = None rtc_region: APINullable[str] = UNDEFINED
video_quality_mode: APINullable[int] = None video_quality_mode: APINullable[int] = UNDEFINED
message_count: APINullable[int] = None message_count: APINullable[int] = UNDEFINED
member_count: APINullable[int] = None member_count: APINullable[int] = UNDEFINED
thread_metadata: APINullable[ThreadMetadata] = None thread_metadata: APINullable[ThreadMetadata] = UNDEFINED
member: APINullable[List] = None member: APINullable[List] = UNDEFINED
default_auto_archive_duration: APINullable[int] = None default_auto_archive_duration: APINullable[int] = UNDEFINED
permissions: APINullable[str] = None permissions: APINullable[str] = UNDEFINED
@property @property
def mention(self): def mention(self):
@ -846,7 +846,7 @@ class ThreadsList(APIModelBase):
threads: List[Thread] threads: List[Thread]
members: List[ThreadMember] members: List[ThreadMember]
has_more: APINullable[bool] = None has_more: APINullable[bool] = UNDEFINED
# noinspection PyTypeChecker # noinspection PyTypeChecker

View file

@ -9,8 +9,8 @@ from typing import List, Any, Optional, overload
from .channel import Channel, ChannelType, channel_types_for_converting, ThreadsList from .channel import Channel, ChannelType, channel_types_for_converting, ThreadsList
from ...utils import Snowflake, Timestamp from ...utils import Snowflake, Timestamp
from ...utils import APIModelBase from ...utils.api_model import APIModelBase
from ...utils.types import APINullable from ...utils.types import APINullable, UNDEFINED
class DefaultMessageNotificationLevel(IntEnum): class DefaultMessageNotificationLevel(IntEnum):
@ -292,63 +292,63 @@ class Guild(APIModelBase):
The scheduled events in the guild The scheduled events in the guild
""" """
id: APINullable[Snowflake] = None id: APINullable[Snowflake] = UNDEFINED
name: APINullable[str] = None name: APINullable[str] = UNDEFINED
icon: APINullable[str] = None icon: APINullable[str] = UNDEFINED
icon_hash: APINullable[str] = None icon_hash: APINullable[str] = UNDEFINED
splash: APINullable[str] = None splash: APINullable[str] = UNDEFINED
discovery_splash: APINullable[str] = None discovery_splash: APINullable[str] = UNDEFINED
owner: APINullable[bool] = None owner: APINullable[bool] = UNDEFINED
owner_id: APINullable[Snowflake] = None owner_id: APINullable[Snowflake] = UNDEFINED
permissions: APINullable[str] = None permissions: APINullable[str] = UNDEFINED
region: APINullable[str] = None region: APINullable[str] = UNDEFINED
afk_channel_id: APINullable[Snowflake] = None afk_channel_id: APINullable[Snowflake] = UNDEFINED
afk_timeout: APINullable[int] = None afk_timeout: APINullable[int] = UNDEFINED
widget_enabled: APINullable[bool] = None widget_enabled: APINullable[bool] = UNDEFINED
widget_channel_id: APINullable[Snowflake] = None widget_channel_id: APINullable[Snowflake] = UNDEFINED
verification_level: APINullable[int] = None verification_level: APINullable[int] = UNDEFINED
default_message_notifications: APINullable[int] = None default_message_notifications: APINullable[int] = UNDEFINED
explicit_content_filter: APINullable[int] = None explicit_content_filter: APINullable[int] = UNDEFINED
features: APINullable[List[str]] = None features: APINullable[List[str]] = UNDEFINED
roles: APINullable[List] = None roles: APINullable[List] = UNDEFINED
emojis: APINullable[List] = None emojis: APINullable[List] = UNDEFINED
# TODO: Make a structures of emoji and role # TODO: Make a structures of emoji and role
mfa_level: APINullable[int] = None mfa_level: APINullable[int] = UNDEFINED
application_id: APINullable[Snowflake] = None application_id: APINullable[Snowflake] = UNDEFINED
system_channel_id: APINullable[Snowflake] = None system_channel_id: APINullable[Snowflake] = UNDEFINED
system_channel_flags: APINullable[int] = None system_channel_flags: APINullable[int] = UNDEFINED
rules_channel_id: APINullable[Snowflake] = None rules_channel_id: APINullable[Snowflake] = UNDEFINED
joined_at: APINullable[Timestamp] = None joined_at: APINullable[Timestamp] = UNDEFINED
# TODO: Deal with joined_at # TODO: Deal with joined_at
large: APINullable[bool] = None large: APINullable[bool] = UNDEFINED
unavailable: APINullable[bool] = None unavailable: APINullable[bool] = UNDEFINED
member_count: APINullable[int] = None member_count: APINullable[int] = UNDEFINED
voice_states: APINullable[List] = None voice_states: APINullable[List] = UNDEFINED
members: APINullable[List] = None members: APINullable[List] = UNDEFINED
threads: APINullable[List] = None threads: APINullable[List] = UNDEFINED
presences: APINullable[List] = None presences: APINullable[List] = UNDEFINED
# TODO: Make a structure for voice_states, members, channels, threads, presences(?) # TODO: Make a structure for voice_states, members, channels, threads, presences(?)
max_presences: APINullable[int] = None max_presences: APINullable[int] = UNDEFINED
max_members: APINullable[int] = None max_members: APINullable[int] = UNDEFINED
vanity_url_code: APINullable[str] = None vanity_url_code: APINullable[str] = UNDEFINED
description: APINullable[str] = None description: APINullable[str] = UNDEFINED
banner: APINullable[str] = None banner: APINullable[str] = UNDEFINED
premium_tier: APINullable[str] = None premium_tier: APINullable[str] = UNDEFINED
premium_subscription_count: APINullable[int] = None premium_subscription_count: APINullable[int] = UNDEFINED
preferred_locale: APINullable[str] = None preferred_locale: APINullable[str] = UNDEFINED
public_updates_channel_id: APINullable[Snowflake] = None public_updates_channel_id: APINullable[Snowflake] = UNDEFINED
max_video_channel_users: APINullable[int] = None max_video_channel_users: APINullable[int] = UNDEFINED
approximate_member_count: APINullable[int] = None approximate_member_count: APINullable[int] = UNDEFINED
approximate_presence_count: APINullable[int] = None approximate_presence_count: APINullable[int] = UNDEFINED
nsfw_level: APINullable[int] = None nsfw_level: APINullable[int] = UNDEFINED
premium_progress_bar_enabled: APINullable[bool] = None premium_progress_bar_enabled: APINullable[bool] = UNDEFINED
stage_instances: APINullable[List] = None stage_instances: APINullable[List] = UNDEFINED
stickers: APINullable[List] = None stickers: APINullable[List] = UNDEFINED
welcome_screen: APINullable = None welcome_screen: APINullable = UNDEFINED
guild_scheduled_events: APINullable[List] = None guild_scheduled_events: APINullable[List] = UNDEFINED
# TODO: Make a structure for welcome_screen, stage_instances, # TODO: Make a structure for welcome_screen, stage_instances,
# stickers and guild_scheduled_events # stickers and guild_scheduled_events

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from ...utils.api_model import APIModelBase from ...utils.api_model import APIModelBase
from ...utils.types import APINullable from ...utils.types import APINullable, UNDEFINED
from ...utils.snowflake import Snowflake from ...utils.snowflake import Snowflake
from ...utils.timestamp import Timestamp from ...utils.timestamp import Timestamp
@ -40,8 +40,8 @@ class ThreadMetadata(APIModelBase):
auto_archive_duration: int auto_archive_duration: int
archive_timestamp: Timestamp archive_timestamp: Timestamp
locked: bool locked: bool
invitable: APINullable[bool] = None invitable: APINullable[bool] = UNDEFINED
create_timestamp: APINullable[Timestamp] = None create_timestamp: APINullable[Timestamp] = UNDEFINED
@dataclass(repr=False) @dataclass(repr=False)
@ -62,5 +62,5 @@ class ThreadMember(APIModelBase):
join_timestamp: Timestamp join_timestamp: Timestamp
flags: int flags: int
id: APINullable[Snowflake] = None id: APINullable[Snowflake] = UNDEFINED
user_id: APINullable[Snowflake] = None user_id: APINullable[Snowflake] = UNDEFINED

View file

@ -8,8 +8,8 @@ from enum import IntEnum
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from ...utils import Snowflake from ...utils import Snowflake
from ...utils import APIModelBase from ...utils.api_model import APIModelBase
from ...utils.types import APINullable from ...utils.types import APINullable, UNDEFINED
if TYPE_CHECKING: if TYPE_CHECKING:
from ..user.user import User from ..user.user import User
@ -73,18 +73,18 @@ class Webhook(APIModelBase):
The url used for executing the webhook (returned by the webhooks OAuth2 flow) The url used for executing the webhook (returned by the webhooks OAuth2 flow)
""" """
id: APINullable[Snowflake] = None id: APINullable[Snowflake] = UNDEFINED
type: APINullable[int] = None type: APINullable[int] = UNDEFINED
guild_id: APINullable[Snowflake] = None guild_id: APINullable[Snowflake] = UNDEFINED
channel_id: APINullable[Snowflake] = None channel_id: APINullable[Snowflake] = UNDEFINED
user: APINullable[User] = None user: APINullable[User] = UNDEFINED
name: APINullable[str] = None name: APINullable[str] = UNDEFINED
avatar: APINullable[str] = None avatar: APINullable[str] = UNDEFINED
token: APINullable[str] = None token: APINullable[str] = UNDEFINED
application_id: APINullable[Snowflake] = None application_id: APINullable[Snowflake] = UNDEFINED
source_guild: APINullable[Guild] = None source_guild: APINullable[Guild] = UNDEFINED
source_channel: APINullable[Channel] = None source_channel: APINullable[Channel] = UNDEFINED
url: APINullable[str] = None url: APINullable[str] = UNDEFINED
async def delete( async def delete(
self, *, webhook_id: Optional[Snowflake] = None, reason: Optional[str] = None self, *, webhook_id: Optional[Snowflake] = None, reason: Optional[str] = None

View file

@ -3,3 +3,4 @@
from .message import * from .message import *
from .embed import * from .embed import *
from .colors import *

View file

@ -0,0 +1,156 @@
# Copyright MelisaDev 2022 - Present
# Full MIT License can be found in `LICENSE.txt` at the project root.
from __future__ import annotations
import random
import string
import typing
CT = typing.TypeVar("CT", bound="Color")
class Color:
"""Represents a Discord colour. This class is similar
to a (red, green, blue) :class:`tuple`.
.. container:: operations
.. describe:: x == y
Checks if two colours are equal.
.. describe:: x != y
Checks if two colours are not equal.
.. describe:: hash(x)
Return the colour's hash.
.. describe:: str(x)
Returns the hex format for the colour.
.. describe:: int(x)
Returns the raw colour value.
Attributes
----------
value: :class:`int`
The raw integer colour value.
"""
__slots__ = ("value",)
def __init__(self, value: int):
if not isinstance(value, int):
raise TypeError(
f"Expected int parameter, received {value.__class__.__name__} instead."
)
self.value: int = value
def _get_byte(self, byte: int) -> int:
return (self.value >> (8 * byte)) & 0xFF
def __eq__(self, other: typing.Any) -> bool:
return isinstance(other, Color) and self.value == other.value
def __ne__(self, other: typing.Any) -> bool:
return not self.__eq__(other)
def __str__(self) -> str:
return f"#{self.value:0>6x}"
def __int__(self) -> int:
return self.value
def __repr__(self) -> str:
return f"<Colour value={self.value}>"
def __hash__(self) -> int:
return hash(self.value)
@property
def r(self) -> int:
""":class:`int`: Returns the red component of the colour."""
return self._get_byte(2)
@property
def g(self) -> int:
""":class:`int`: Returns the green component of the colour."""
return self._get_byte(1)
@property
def b(self) -> int:
""":class:`int`: Returns the blue component of the colour."""
return self._get_byte(0)
def to_rgb(self) -> typing.Tuple[int, int, int]:
"""
Tuple[:class:`int`, :class:`int`, :class:`int`]:
Returns an (r, g, b) tuple representing the colour."""
return (self.r, self.g, self.b)
@classmethod
def from_rgb(cls: typing.Type[CT], r: int, g: int, b: int) -> CT:
"""Constructs a :class:`Colour` from an RGB tuple."""
return cls((r << 16) + (g << 8) + b)
@classmethod
def from_hex_code(cls, hex_code: str, /) -> Color:
"""Convert the given hexadecimal color code to a `Color`.
The inputs may be of the following format (case insensitive):
`1a2`, `#1a2`, `0x1a2` (for web-safe colors), or
`1a2b3c`, `#1a2b3c`, `0x1a2b3c` (for regular 3-byte color-codes).
Parameters
----------
hex_code: :class:`str`
A hexadecimal color code to parse. This may optionally start with
a case insensitive `0x` or `#`.
Returns
-------
Color
A corresponding Color object.
Raises
------
:class:`ValueError`
If ``hex_code`` is not a hexadecimal or is a invalid length.
"""
if hex_code.startswith("#"):
hex_code = hex_code[1:]
elif hex_code.startswith(("0x", "0X")):
hex_code = hex_code[2:]
if not all(c in string.hexdigits for c in hex_code):
raise ValueError("Color code must be hexadecimal")
if len(hex_code) == 3:
r, g, b = (c << 4 | c for c in (int(c, 16) for c in hex_code))
return cls.from_rgb(r, g, b)
if len(hex_code) == 6:
return cls.from_rgb(
int(hex_code[:2], 16), int(hex_code[2:4], 16), int(hex_code[4:6], 16)
)
raise ValueError("Color code is invalid length. Must be 3 or 6 digits")
@classmethod
def default(cls: typing.Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0``."""
return cls(0)
@classmethod
def random(
cls: typing.Type[CT],
*,
seed: typing.Optional[typing.Union[int, str, float, bytes, bytearray]] = None,
) -> CT:
"""A factory method that returns a :class:`Colour` with a random hue.
Parameters
----------
seed: Optional[Union[:class:`int`, :class:`str`,
:class:`float`, :class:`bytes`, :class:`bytearray`]]
The seed to initialize the RNG with. If ``None`` is passed the default RNG is used.
"""
rand = random if seed is None else random.Random(seed)
return cls.from_hsv(rand.random(), 1, 1)

View file

@ -9,9 +9,10 @@ from enum import Enum
from typing import List, Union, Optional from typing import List, Union, Optional
from .colors import Color
from melisa.exceptions import EmbedFieldError from melisa.exceptions import EmbedFieldError
from melisa.utils.types import UNDEFINED, UndefinedOr from ...utils.api_model import APIModelBase
from melisa.utils.api_model import APIModelBase, APINullable from ...utils.types import APINullable, UNDEFINED
from melisa.utils.timestamp import Timestamp from melisa.utils.timestamp import Timestamp
@ -47,7 +48,7 @@ class EmbedType(Enum):
@dataclass(repr=False) @dataclass(repr=False)
class EmbedThumbnail(APIModelBase): class EmbedThumbnail:
"""Representation of the Embed Thumbnail """Representation of the Embed Thumbnail
Attributes Attributes
@ -63,13 +64,13 @@ class EmbedThumbnail(APIModelBase):
""" """
url: str url: str
proxy_url: APINullable[str] = None proxy_url: APINullable[str] = UNDEFINED
height: APINullable[int] = None height: APINullable[int] = UNDEFINED
width: APINullable[int] = None width: APINullable[int] = UNDEFINED
@dataclass(repr=False) @dataclass(repr=False)
class EmbedVideo(APIModelBase): class EmbedVideo:
"""Representation of the Embed Video """Representation of the Embed Video
Attributes Attributes
@ -85,13 +86,13 @@ class EmbedVideo(APIModelBase):
""" """
url: str url: str
proxy_url: APINullable[str] = None proxy_url: APINullable[str] = UNDEFINED
height: APINullable[int] = None height: APINullable[int] = UNDEFINED
width: APINullable[int] = None width: APINullable[int] = UNDEFINED
@dataclass(repr=False) @dataclass(repr=False)
class EmbedImage(APIModelBase): class EmbedImage:
"""Representation of the Embed Image """Representation of the Embed Image
Attributes Attributes
@ -107,13 +108,13 @@ class EmbedImage(APIModelBase):
""" """
url: str url: str
proxy_url: APINullable[str] = None proxy_url: APINullable[str] = UNDEFINED
height: APINullable[int] = None height: APINullable[int] = UNDEFINED
width: APINullable[int] = None width: APINullable[int] = UNDEFINED
@dataclass(repr=False) @dataclass(repr=False)
class EmbedProvider(APIModelBase): class EmbedProvider:
"""Representation of the Embed Provider """Representation of the Embed Provider
Attributes Attributes
@ -124,12 +125,12 @@ class EmbedProvider(APIModelBase):
Url of provider Url of provider
""" """
name: APINullable[str] = None name: APINullable[str] = UNDEFINED
url: APINullable[str] = None url: APINullable[str] = UNDEFINED
@dataclass(repr=False) @dataclass(repr=False)
class EmbedAuthor(APIModelBase): class EmbedAuthor:
"""Representation of the Embed Author """Representation of the Embed Author
Attributes Attributes
@ -145,13 +146,13 @@ class EmbedAuthor(APIModelBase):
""" """
name: str name: str
url: APINullable[str] = None url: APINullable[str] = UNDEFINED
icon_url: APINullable[str] = None icon_url: APINullable[str] = UNDEFINED
proxy_icon_url: APINullable[str] = None proxy_icon_url: APINullable[str] = UNDEFINED
@dataclass(repr=False) @dataclass(repr=False)
class EmbedFooter(APIModelBase): class EmbedFooter:
"""Representation of the Embed Footer """Representation of the Embed Footer
Attributes Attributes
@ -165,12 +166,12 @@ class EmbedFooter(APIModelBase):
""" """
text: str text: str
icon_url: APINullable[str] = None icon_url: APINullable[str] = UNDEFINED
proxy_icon_url: APINullable[str] = None proxy_icon_url: APINullable[str] = UNDEFINED
@dataclass(repr=False) @dataclass(repr=False)
class EmbedField(APIModelBase): class EmbedField:
"""Representation of the Embed Field """Representation of the Embed Field
Attributes Attributes
@ -185,7 +186,7 @@ class EmbedField(APIModelBase):
name: str name: str
value: str value: str
inline: APINullable[bool] = False inline: Optional[bool] = False
@dataclass(repr=False) @dataclass(repr=False)
@ -202,7 +203,12 @@ class Embed(APIModelBase):
description: Optional[:class:`str`] description: Optional[:class:`str`]
Description of embed Description of embed
color: Optional[:class:`int`] color: Optional[:class:`int`]
Color code of the embed Color code of the embed.
If you really want to do something with a color,
feel free to convert it to the ``Color``: ::
color = Color(embed.color)
fields: Optional[List[:class:`~melisa.models.message.embed.EmbedField`]] fields: Optional[List[:class:`~melisa.models.message.embed.EmbedField`]]
Fields information. Fields information.
footer: Optional[:class:`~melisa.models.message.embed.EmbedFooter`] footer: Optional[:class:`~melisa.models.message.embed.EmbedFooter`]
@ -221,19 +227,19 @@ class Embed(APIModelBase):
Video information. Video information.
""" """
title: APINullable[str] = None title: APINullable[str] = UNDEFINED
type: APINullable[EmbedType] = None type: APINullable[EmbedType] = UNDEFINED
description: APINullable[str] = None description: APINullable[str] = UNDEFINED
url: APINullable[str] = None url: APINullable[str] = UNDEFINED
timestamp: APINullable[Timestamp] = None timestamp: APINullable[Timestamp] = UNDEFINED
color: APINullable[int] = None color: APINullable[Color] = UNDEFINED
footer: APINullable[EmbedFooter] = None footer: APINullable[EmbedFooter] = UNDEFINED
image: APINullable[EmbedImage] = None image: APINullable[EmbedImage] = UNDEFINED
thumbnail: APINullable[EmbedThumbnail] = None thumbnail: APINullable[EmbedThumbnail] = UNDEFINED
video: APINullable[EmbedVideo] = None video: APINullable[EmbedVideo] = UNDEFINED
provider: APINullable[EmbedProvider] = None provider: APINullable[EmbedProvider] = UNDEFINED
author: APINullable[EmbedAuthor] = None author: APINullable[EmbedAuthor] = UNDEFINED
fields: APINullable[List[EmbedField]] = None fields: APINullable[List[EmbedField]] = UNDEFINED
def __post_init__(self): def __post_init__(self):
if self.title and len(self.title) > 256: if self.title and len(self.title) > 256:
@ -251,6 +257,26 @@ class Embed(APIModelBase):
if self.fields and len(self.fields) > 25: if self.fields and len(self.fields) > 25:
raise EmbedFieldError("""You can't set more than 25 embed fields!""") raise EmbedFieldError("""You can't set more than 25 embed fields!""")
def set_color(self, color: Union[int, Color]) -> Embed:
"""Sets color in the supported by discord format.
Parameters
----------
color: Union[:class:`~melisa.models.message.color.Color`, :class:`int`]
The datetime to set the timestamp to.
Returns
-------
:class:`~melisa.models.message.embed.Embed`
The new embed object.
"""
if isinstance(color, Color):
self.color = color.value
elif isinstance(color, int):
self.color = Color(value=color).value
return self
def set_timestamp(self, time: Union[Timestamp, datetime]) -> Embed: def set_timestamp(self, time: Union[Timestamp, datetime]) -> Embed:
"""Sets timestamp in the supported by discord format. """Sets timestamp in the supported by discord format.
@ -261,7 +287,7 @@ class Embed(APIModelBase):
Returns Returns
------- -------
:class:`~,e;osa.models.message.embed.Embed` :class:`~melisa.models.message.embed.Embed`
The new embed object. The new embed object.
""" """
self.timestamp = time.isoformat() self.timestamp = time.isoformat()
@ -272,9 +298,9 @@ class Embed(APIModelBase):
self, self,
name: str, name: str,
*, *,
url: Optional[str] = None, url: Optional[str] = UNDEFINED,
icon_url: Optional[str] = None, icon_url: Optional[str] = UNDEFINED,
proxy_icon_url: Optional[str] = None, proxy_icon_url: Optional[str] = UNDEFINED,
) -> Embed: ) -> Embed:
"""Set the author for the embed. """Set the author for the embed.
@ -303,7 +329,7 @@ class Embed(APIModelBase):
return self return self
def set_image(self, url: str, *, proxy_url: Optional[str] = None) -> Embed: def set_image(self, url: str, *, proxy_url: APINullable[str] = UNDEFINED) -> Embed:
"""Set the image for the embed. """Set the image for the embed.
Parameters Parameters
@ -322,7 +348,7 @@ class Embed(APIModelBase):
return self return self
def set_thumbnail(self, url: str, *, proxy_url: Optional[str] = None) -> Embed: def set_thumbnail(self, url: str, *, proxy_url: APINullable[str] = UNDEFINED) -> Embed:
"""Set the thumbnail for the embed. """Set the thumbnail for the embed.
Parameters Parameters
@ -345,8 +371,8 @@ class Embed(APIModelBase):
self, self,
text: str, text: str,
*, *,
icon_url: Optional[str] = None, icon_url: APINullable[str] = UNDEFINED,
proxy_icon_url: Optional[str] = None, proxy_icon_url: APINullable[str] = UNDEFINED,
) -> Embed: ) -> Embed:
""" """
Sets the embed footer. Sets the embed footer.
@ -392,7 +418,7 @@ class Embed(APIModelBase):
This embed. This embed.
""" """
if self.fields is None: if self.fields is UNDEFINED:
self.fields = [] self.fields = []
self.fields.append(EmbedField(name=name, value=value, inline=inline)) self.fields.append(EmbedField(name=name, value=value, inline=inline))
@ -403,9 +429,9 @@ class Embed(APIModelBase):
self, self,
index: int, index: int,
*, *,
name: UndefinedOr[str] = UNDEFINED, name: APINullable[str] = UNDEFINED,
value: UndefinedOr[str] = UNDEFINED, value: APINullable[str] = UNDEFINED,
inline: UndefinedOr[bool] = UNDEFINED, inline: APINullable[bool] = UNDEFINED,
) -> Embed: ) -> Embed:
"""Edit an existing field on this embed. """Edit an existing field on this embed.
@ -413,11 +439,11 @@ class Embed(APIModelBase):
---------- ----------
index: :class:`int` index: :class:`int`
The index of the field to edit. The index of the field to edit.
name: UndefinedOr[:class:`str`] name: Optional[:class:`str`]
The name of the field. The name of the field.
value: UndefinedOr[:class:`str`] value: Optional[:class:`str`]
The value of the field. The value of the field.
inline: UndefinedOr[:class:`bool`] inline: Optional[:class:`bool`]
Whether the field should be displayed inline. Whether the field should be displayed inline.
Returns Returns
@ -468,7 +494,7 @@ class Embed(APIModelBase):
del self.fields[index] del self.fields[index]
if not self.fields: if not self.fields:
self.fields = None self.fields = UNDEFINED
return self return self

View file

@ -9,7 +9,7 @@ from typing import List, TYPE_CHECKING, Optional, Dict
from ...utils import Snowflake, Timestamp from ...utils import Snowflake, Timestamp
from ...utils import APIModelBase from ...utils import APIModelBase
from ...utils.types import APINullable from ...utils.types import APINullable, UNDEFINED
if TYPE_CHECKING: if TYPE_CHECKING:
from ..guild.channel import Thread from ..guild.channel import Thread
@ -170,36 +170,36 @@ class Message(APIModelBase):
Deprecated the stickers sent with the message Deprecated the stickers sent with the message
""" """
id: APINullable[Snowflake] = None id: APINullable[Snowflake] = UNDEFINED
channel_id: APINullable[Snowflake] = None channel_id: APINullable[Snowflake] = UNDEFINED
guild_id: APINullable[Snowflake] = None guild_id: APINullable[Snowflake] = UNDEFINED
author: APINullable[Dict] = None author: APINullable[Dict] = UNDEFINED
member: APINullable[Dict] = None member: APINullable[Dict] = UNDEFINED
content: APINullable[str] = None content: APINullable[str] = UNDEFINED
timestamp: APINullable[Timestamp] = None timestamp: APINullable[Timestamp] = UNDEFINED
edited_timestamp: APINullable[Timestamp] = None edited_timestamp: APINullable[Timestamp] = UNDEFINED
tts: APINullable[bool] = None tts: APINullable[bool] = UNDEFINED
mention_everyone: APINullable[bool] = None mention_everyone: APINullable[bool] = UNDEFINED
mentions: APINullable[List] = None mentions: APINullable[List] = UNDEFINED
mention_roles: APINullable[List] = None mention_roles: APINullable[List] = UNDEFINED
mention_channels: APINullable[List] = None mention_channels: APINullable[List] = UNDEFINED
attachments: APINullable[List] = None attachments: APINullable[List] = UNDEFINED
embeds: APINullable[List] = None embeds: APINullable[List] = UNDEFINED
reactions: APINullable[List] = None reactions: APINullable[List] = UNDEFINED
nonce: APINullable[int] or APINullable[str] = None nonce: APINullable[int] or APINullable[str] = UNDEFINED
pinned: APINullable[bool] = None pinned: APINullable[bool] = UNDEFINED
webhook_id: APINullable[Snowflake] = None webhook_id: APINullable[Snowflake] = UNDEFINED
type: APINullable[int] = None type: APINullable[int] = UNDEFINED
activity: APINullable[Dict] = None activity: APINullable[Dict] = UNDEFINED
application: APINullable[Dict] = None application: APINullable[Dict] = UNDEFINED
application_id: APINullable[Snowflake] = None application_id: APINullable[Snowflake] = UNDEFINED
message_reference: APINullable[Dict] = None message_reference: APINullable[Dict] = UNDEFINED
flags: APINullable[int] = None flags: APINullable[int] = UNDEFINED
interaction: APINullable[Dict] = None interaction: APINullable[Dict] = UNDEFINED
thread: APINullable[Thread] = None thread: APINullable[Thread] = UNDEFINED
components: APINullable[List] = None components: APINullable[List] = UNDEFINED
sticker_items: APINullable[List] = None sticker_items: APINullable[List] = UNDEFINED
stickers: APINullable[List] = None stickers: APINullable[List] = UNDEFINED
async def pin(self, *, reason: Optional[str] = None): async def pin(self, *, reason: Optional[str] = None):
"""|coro| """|coro|

View file

@ -7,7 +7,7 @@ from typing import Optional, Tuple, List, Literal
from ...utils import Snowflake from ...utils import Snowflake
from ...utils import APIModelBase from ...utils import APIModelBase
from ...utils.types import APINullable from ...utils.types import APINullable, UNDEFINED
class BasePresence: class BasePresence:
@ -223,19 +223,19 @@ class Activity(BasePresence, APIModelBase):
name: str name: str
type: ActivityType type: ActivityType
created_at: APINullable[int] = None created_at: APINullable[int] = UNDEFINED
url: APINullable[str] = None url: APINullable[str] = UNDEFINED
timestamps: APINullable[ActivityTimestamp] = None timestamps: APINullable[ActivityTimestamp] = UNDEFINED
application_id: APINullable[Snowflake] = None application_id: APINullable[Snowflake] = UNDEFINED
details: APINullable[str] = None details: APINullable[str] = UNDEFINED
state: APINullable[str] = None state: APINullable[str] = UNDEFINED
emoji: APINullable[ActivityEmoji] = None emoji: APINullable[ActivityEmoji] = UNDEFINED
party: APINullable[ActivityParty] = None party: APINullable[ActivityParty] = UNDEFINED
assets: APINullable[ActivityAssets] = None assets: APINullable[ActivityAssets] = UNDEFINED
secrets: APINullable[ActivitySecrets] = None secrets: APINullable[ActivitySecrets] = UNDEFINED
instance: APINullable[bool] = None instance: APINullable[bool] = UNDEFINED
flags: APINullable[ActivityFlags] = None flags: APINullable[ActivityFlags] = UNDEFINED
buttons: APINullable[List[ActivityButton]] = None buttons: APINullable[List[ActivityButton]] = UNDEFINED
class StatusType(Enum): class StatusType(Enum):

View file

@ -8,7 +8,7 @@ from dataclasses import dataclass
from typing import Optional from typing import Optional
from ...utils.api_model import APIModelBase from ...utils.api_model import APIModelBase
from ...utils.types import APINullable from ...utils.types import APINullable, UNDEFINED
from ...utils.snowflake import Snowflake from ...utils.snowflake import Snowflake
@ -149,20 +149,20 @@ class User(APIModelBase):
The user their premium type in a usable enum. The user their premium type in a usable enum.
""" """
id: APINullable[Snowflake] = None id: APINullable[Snowflake] = UNDEFINED
username: APINullable[str] = None username: APINullable[str] = UNDEFINED
discriminator: APINullable[str] = None discriminator: APINullable[str] = UNDEFINED
avatar: APINullable[str] = None avatar: APINullable[str] = UNDEFINED
bot: APINullable[bool] = None bot: APINullable[bool] = UNDEFINED
system: APINullable[bool] = None system: APINullable[bool] = UNDEFINED
mfa_enabled: APINullable[bool] = None mfa_enabled: APINullable[bool] = UNDEFINED
banner: APINullable[str] = None banner: APINullable[str] = UNDEFINED
accent_color: APINullable[int] = None accent_color: APINullable[int] = UNDEFINED
local: APINullable[str] = None local: APINullable[str] = UNDEFINED
verified: APINullable[bool] = None verified: APINullable[bool] = UNDEFINED
email: APINullable[str] = None email: APINullable[str] = UNDEFINED
premium_type: APINullable[int] = None premium_type: APINullable[int] = UNDEFINED
public_flags: APINullable[int] = None public_flags: APINullable[int] = UNDEFINED
@property @property
def premium(self) -> Optional[PremiumTypes]: def premium(self) -> Optional[PremiumTypes]:

View file

@ -6,7 +6,6 @@
from __future__ import annotations from __future__ import annotations
import copy import copy
import datetime
from dataclasses import _is_dataclass_instance, fields from dataclasses import _is_dataclass_instance, fields
from enum import Enum, EnumMeta from enum import Enum, EnumMeta
from inspect import getfullargspec from inspect import getfullargspec
@ -19,45 +18,58 @@ from typing import (
Any, Any,
get_origin, get_origin,
Tuple, Tuple,
get_args, get_args, Optional,
) )
from typing_extensions import get_type_hints from typing_extensions import get_type_hints
from melisa.utils.types import APINullable, TypeCache from melisa.utils.types import UndefinedType, TypeCache, UNDEFINED
T = TypeVar("T") T = TypeVar("T")
def to_dict_without_none(model): def _asdict_ignore_none(obj: Generic[T]) -> Union[Tuple, Dict, T]:
""" """
Converts discord model or other object to dict. Returns a dict from a dataclass that ignores
all values that are None
Modification of _asdict_inner from dataclasses
Parameters
----------
obj: Generic[T]
The object to convert
Returns
-------
A dict without None values
""" """
if _is_dataclass_instance(model):
result = []
for field in fields(model): if _is_dataclass_instance(obj):
value = to_dict_without_none(getattr(model, field.name)) result = []
for f in fields(obj):
value = _asdict_ignore_none(getattr(obj, f.name))
if isinstance(value, Enum): if isinstance(value, Enum):
result.append((field.name, value.value)) result.append((f.name, value.value))
elif value is not None and not field.name.startswith("_"): # This if statement was added to the function
result.append((field.name, value)) elif not isinstance(value, UndefinedType) and not f.name.startswith(
"_"
):
result.append((f.name, value))
return dict(result) return dict(result)
if isinstance(model, tuple) and hasattr(model, "_fields"): elif isinstance(obj, tuple) and hasattr(obj, "_fields"):
return type(model)(*[to_dict_without_none(v) for v in model]) return type(obj)(*[_asdict_ignore_none(v) for v in obj])
if isinstance(model, (list, tuple)): elif isinstance(obj, (list, tuple)):
return type(model)(to_dict_without_none(v) for v in model) return type(obj)(_asdict_ignore_none(v) for v in obj)
if isinstance(model, dict): elif isinstance(obj, dict):
return type(model)( return type(obj)(
(to_dict_without_none(k), to_dict_without_none(v)) for k, v in model.items() (_asdict_ignore_none(k), _asdict_ignore_none(v))
for k, v in obj.items()
) )
else:
return copy.deepcopy(model) return copy.deepcopy(obj)
class APIModelBase: class APIModelBase:
@ -65,12 +77,12 @@ class APIModelBase:
Represents an object which has been fetched from the Discord API. Represents an object which has been fetched from the Discord API.
""" """
_client = None _client: Optional[Any] = None
@property @property
def _http(self): def _http(self):
if not self._client: if not self._client:
return None raise AttributeError("Object is not yet linked to a client")
return self._client.http return self._client.http
@ -78,17 +90,21 @@ class APIModelBase:
def set_client(cls, client): def set_client(cls, client):
cls._client = client cls._client = client
def __get_types(self, arg_type: type) -> Tuple[type]: def __get_types(self, attr: str, arg_type: type) -> Tuple[type]:
origin = get_origin(arg_type) origin = get_origin(arg_type)
if origin is Union: if origin is Union:
# Ahh yes, typing module has no type annotations for this...
# noinspection PyTypeChecker # noinspection PyTypeChecker
args: Tuple[type] = get_args(arg_type) args: Tuple[type] = get_args(arg_type)
if 2 <= len(args) < 4: if 2 <= len(args) < 4:
return args return args
raise TypeError raise ValueError(
f"Attribute `{attr}` in `{type(self).__name__}` has too many "
f"or not enough arguments! (got {len(args)} expected 2-3)"
)
return (arg_type,) return (arg_type,)
@ -99,8 +115,8 @@ class APIModelBase:
if getattr(attr_type, "__factory__", None): if getattr(attr_type, "__factory__", None):
factory = attr_type.__factory__ factory = attr_type.__factory__
if attr_value is None: if attr_value is UNDEFINED:
return None return UNDEFINED
if attr_type is not None and isinstance(attr_value, attr_type): if attr_type is not None and isinstance(attr_value, attr_type):
return attr_value return attr_value
@ -108,9 +124,6 @@ class APIModelBase:
if isinstance(attr_value, dict): if isinstance(attr_value, dict):
return factory(attr_value) return factory(attr_value)
if isinstance(attr_value, datetime.datetime):
return attr_value
return factory(attr_value) return factory(attr_value)
def __post_init__(self): def __post_init__(self):
@ -126,14 +139,19 @@ class APIModelBase:
if attr.startswith("_"): if attr.startswith("_"):
continue continue
types = self.__get_types(attr_type) types = self.__get_types(attr, attr_type)
types = tuple( types = tuple(
filter(lambda tpe: tpe is not None and tpe is not None, types) filter(
lambda tpe: tpe is not None and tpe is not UNDEFINED, types
)
) )
if not types: if not types:
raise TypeError raise ValueError(
f"Attribute `{attr}` in `{type(self).__name__}` only "
"consisted of missing/optional type!"
)
specific_tp = types[0] specific_tp = types[0]
@ -143,7 +161,7 @@ class APIModelBase:
specific_tp = tp specific_tp = tp
if isinstance(specific_tp, EnumMeta) and not attr_gotten: if isinstance(specific_tp, EnumMeta) and not attr_gotten:
attr_value = None attr_value = UNDEFINED
elif tp == list and attr_gotten and (classes := get_args(types[0])): elif tp == list and attr_gotten and (classes := get_args(types[0])):
attr_value = [ attr_value = [
self.__attr_convert(attr_item, classes[0]) self.__attr_convert(attr_item, classes[0])
@ -179,23 +197,30 @@ class APIModelBase:
return super().__str__() return super().__str__()
@classmethod @classmethod
def from_dict(cls: Generic[T], data: Dict[str, Union[str, bool, int, Any]]) -> T: def from_dict(
cls: Generic[T], data: Dict[str, Union[str, bool, int, Any]]
) -> T:
""" """
Parse an API object from a dictionary. Parse an API object from a dictionary.
""" """
if isinstance(data, cls): if isinstance(data, cls):
return data return data
# Disable inspection for IDE because this is valid code for the
# inherited classes:
# noinspection PyArgumentList # noinspection PyArgumentList
return cls( return cls(
**dict( **dict(
map( map(
lambda key: ( lambda key: (
key, key,
data[key].value if isinstance(data[key], Enum) else data[key], data[key].value
if isinstance(data[key], Enum)
else data[key],
), ),
filter( filter(
lambda object_argument: data.get(object_argument) is not None, lambda object_argument: data.get(object_argument)
is not None,
getfullargspec(cls.__init__).args, getfullargspec(cls.__init__).args,
), ),
) )
@ -203,4 +228,8 @@ class APIModelBase:
) )
def to_dict(self) -> Dict: def to_dict(self) -> Dict:
return to_dict_without_none(self) """
Transform the current object to a dictionary representation. Parameters that
start with an underscore are not serialized.
"""
return _asdict_ignore_none(self)

View file

@ -35,9 +35,7 @@ T = TypeVar("T")
Coro = TypeVar("Coro", bound=Callable[..., Coroutine[Any, Any, Any]]) Coro = TypeVar("Coro", bound=Callable[..., Coroutine[Any, Any, Any]])
APINullable = Union[T, None] APINullable = Union[T, UndefinedType]
UndefinedOr = Union[T, UndefinedType]
class Singleton(type): class Singleton(type):

15
tests/test_colors.py Normal file
View file

@ -0,0 +1,15 @@
from melisa import Color
rgb_right_example = (3, 217, 147)
class TestColor:
def test_from_rgb_converting(self):
assert Color.from_rgb(3, 217, 147).to_rgb() == rgb_right_example
def test_from_hex_code_converting(self):
assert Color.from_hex_code("#03d993").to_rgb() == rgb_right_example
def test_from_decimal_converting(self):
assert Color(252307).to_rgb() == rgb_right_example

View file

@ -1,10 +1,11 @@
import datetime import datetime
from melisa import Embed, Timestamp from melisa import Embed, Timestamp, Color
dict_embed = { dict_embed = {
'title': 'my title', 'title': 'my title',
'description': 'simple description', 'description': 'simple description',
'color': 252307,
'timestamp': datetime.datetime.utcfromtimestamp(1649748784).isoformat(), 'timestamp': datetime.datetime.utcfromtimestamp(1649748784).isoformat(),
'footer': { 'footer': {
'text': 'cool footer text' 'text': 'cool footer text'
@ -14,6 +15,16 @@ dict_embed = {
}, },
} }
EMBED = Embed(title="my title", description="simple description")
EMBED.set_author(name="best author")
EMBED.set_footer(text="cool footer text")
EMBED.set_color(Color.from_hex_code("#03d993"))
EMBED.set_timestamp(Timestamp.parse(1649748784))
def has_key_vals(actual, required):
return all(actual.get(key) == val for key, val in required.items())
class TestEmbed: class TestEmbed:
def test_total_length_when_embed_is_empty(self): def test_total_length_when_embed_is_empty(self):
@ -54,9 +65,15 @@ class TestEmbed:
embed.set_footer(text="cool footer text") embed.set_footer(text="cool footer text")
assert embed.total_length() == 53 assert embed.total_length() == 53
def test_comparing_embeds(self): def test_embed_to_dict(self):
embed = Embed(title="my title", description="simple description") """
embed.set_author(name="best author") Tests whether or not the dispatch class its string conversion
embed.set_footer(text="cool footer text") is correct.
embed.set_timestamp(Timestamp.parse(1649748784)) """
assert embed.to_dict() == dict_embed assert has_key_vals(EMBED.to_dict(), dict_embed)
def test_embed_from_dict(self):
assert has_key_vals(
Embed.from_dict(dict_embed).to_dict(),
dict_embed
)