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 ...models.message.embed import Embed
from ...utils import Snowflake, Timestamp
from ...utils import APIModelBase
from ...utils.types import APINullable
from ...utils.api_model import APIModelBase
from ...utils.types import APINullable, UNDEFINED
if TYPE_CHECKING:
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
"""
id: APINullable[Snowflake] = None
type: APINullable[int] = None
guild_id: APINullable[Snowflake] = None
position: APINullable[int] = None
permission_overwrites: APINullable[List] = None
name: APINullable[str] = None
topic: APINullable[str] = None
nsfw: APINullable[bool] = None
last_message_id: APINullable[Snowflake] = None
bitrate: APINullable[int] = None
user_limit: APINullable[int] = None
rate_limit_per_user: APINullable[int] = None
recipients: APINullable[List] = None
icon: APINullable[str] = None
owner_id: APINullable[Snowflake] = None
application_id: APINullable[Snowflake] = None
parent_id: APINullable[Snowflake] = None
last_pin_timestamp: APINullable[Timestamp] = None
rtc_region: APINullable[str] = None
video_quality_mode: APINullable[int] = None
message_count: APINullable[int] = None
member_count: APINullable[int] = None
thread_metadata: APINullable[ThreadMetadata] = None
member: APINullable[List] = None
default_auto_archive_duration: APINullable[int] = None
permissions: APINullable[str] = None
id: APINullable[Snowflake] = UNDEFINED
type: APINullable[int] = UNDEFINED
guild_id: APINullable[Snowflake] = UNDEFINED
position: APINullable[int] = UNDEFINED
permission_overwrites: APINullable[List] = UNDEFINED
name: APINullable[str] = UNDEFINED
topic: APINullable[str] = UNDEFINED
nsfw: APINullable[bool] = UNDEFINED
last_message_id: APINullable[Snowflake] = UNDEFINED
bitrate: APINullable[int] = UNDEFINED
user_limit: APINullable[int] = UNDEFINED
rate_limit_per_user: APINullable[int] = UNDEFINED
recipients: APINullable[List] = UNDEFINED
icon: APINullable[str] = UNDEFINED
owner_id: APINullable[Snowflake] = UNDEFINED
application_id: APINullable[Snowflake] = UNDEFINED
parent_id: APINullable[Snowflake] = UNDEFINED
last_pin_timestamp: APINullable[Timestamp] = UNDEFINED
rtc_region: APINullable[str] = UNDEFINED
video_quality_mode: APINullable[int] = UNDEFINED
message_count: APINullable[int] = UNDEFINED
member_count: APINullable[int] = UNDEFINED
thread_metadata: APINullable[ThreadMetadata] = UNDEFINED
member: APINullable[List] = UNDEFINED
default_auto_archive_duration: APINullable[int] = UNDEFINED
permissions: APINullable[str] = UNDEFINED
@property
def mention(self):
@ -846,7 +846,7 @@ class ThreadsList(APIModelBase):
threads: List[Thread]
members: List[ThreadMember]
has_more: APINullable[bool] = None
has_more: APINullable[bool] = UNDEFINED
# 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 ...utils import Snowflake, Timestamp
from ...utils import APIModelBase
from ...utils.types import APINullable
from ...utils.api_model import APIModelBase
from ...utils.types import APINullable, UNDEFINED
class DefaultMessageNotificationLevel(IntEnum):
@ -292,63 +292,63 @@ class Guild(APIModelBase):
The scheduled events in the guild
"""
id: APINullable[Snowflake] = None
name: APINullable[str] = None
icon: APINullable[str] = None
icon_hash: APINullable[str] = None
splash: APINullable[str] = None
discovery_splash: APINullable[str] = None
owner: APINullable[bool] = None
owner_id: APINullable[Snowflake] = None
permissions: APINullable[str] = None
region: APINullable[str] = None
afk_channel_id: APINullable[Snowflake] = None
afk_timeout: APINullable[int] = None
widget_enabled: APINullable[bool] = None
widget_channel_id: APINullable[Snowflake] = None
verification_level: APINullable[int] = None
default_message_notifications: APINullable[int] = None
explicit_content_filter: APINullable[int] = None
features: APINullable[List[str]] = None
roles: APINullable[List] = None
emojis: APINullable[List] = None
id: APINullable[Snowflake] = UNDEFINED
name: APINullable[str] = UNDEFINED
icon: APINullable[str] = UNDEFINED
icon_hash: APINullable[str] = UNDEFINED
splash: APINullable[str] = UNDEFINED
discovery_splash: APINullable[str] = UNDEFINED
owner: APINullable[bool] = UNDEFINED
owner_id: APINullable[Snowflake] = UNDEFINED
permissions: APINullable[str] = UNDEFINED
region: APINullable[str] = UNDEFINED
afk_channel_id: APINullable[Snowflake] = UNDEFINED
afk_timeout: APINullable[int] = UNDEFINED
widget_enabled: APINullable[bool] = UNDEFINED
widget_channel_id: APINullable[Snowflake] = UNDEFINED
verification_level: APINullable[int] = UNDEFINED
default_message_notifications: APINullable[int] = UNDEFINED
explicit_content_filter: APINullable[int] = UNDEFINED
features: APINullable[List[str]] = UNDEFINED
roles: APINullable[List] = UNDEFINED
emojis: APINullable[List] = UNDEFINED
# TODO: Make a structures of emoji and role
mfa_level: APINullable[int] = None
application_id: APINullable[Snowflake] = None
system_channel_id: APINullable[Snowflake] = None
system_channel_flags: APINullable[int] = None
rules_channel_id: APINullable[Snowflake] = None
joined_at: APINullable[Timestamp] = None
mfa_level: APINullable[int] = UNDEFINED
application_id: APINullable[Snowflake] = UNDEFINED
system_channel_id: APINullable[Snowflake] = UNDEFINED
system_channel_flags: APINullable[int] = UNDEFINED
rules_channel_id: APINullable[Snowflake] = UNDEFINED
joined_at: APINullable[Timestamp] = UNDEFINED
# TODO: Deal with joined_at
large: APINullable[bool] = None
unavailable: APINullable[bool] = None
member_count: APINullable[int] = None
voice_states: APINullable[List] = None
members: APINullable[List] = None
threads: APINullable[List] = None
presences: APINullable[List] = None
large: APINullable[bool] = UNDEFINED
unavailable: APINullable[bool] = UNDEFINED
member_count: APINullable[int] = UNDEFINED
voice_states: APINullable[List] = UNDEFINED
members: APINullable[List] = UNDEFINED
threads: APINullable[List] = UNDEFINED
presences: APINullable[List] = UNDEFINED
# TODO: Make a structure for voice_states, members, channels, threads, presences(?)
max_presences: APINullable[int] = None
max_members: APINullable[int] = None
vanity_url_code: APINullable[str] = None
description: APINullable[str] = None
banner: APINullable[str] = None
premium_tier: APINullable[str] = None
premium_subscription_count: APINullable[int] = None
preferred_locale: APINullable[str] = None
public_updates_channel_id: APINullable[Snowflake] = None
max_video_channel_users: APINullable[int] = None
approximate_member_count: APINullable[int] = None
approximate_presence_count: APINullable[int] = None
nsfw_level: APINullable[int] = None
premium_progress_bar_enabled: APINullable[bool] = None
stage_instances: APINullable[List] = None
stickers: APINullable[List] = None
welcome_screen: APINullable = None
guild_scheduled_events: APINullable[List] = None
max_presences: APINullable[int] = UNDEFINED
max_members: APINullable[int] = UNDEFINED
vanity_url_code: APINullable[str] = UNDEFINED
description: APINullable[str] = UNDEFINED
banner: APINullable[str] = UNDEFINED
premium_tier: APINullable[str] = UNDEFINED
premium_subscription_count: APINullable[int] = UNDEFINED
preferred_locale: APINullable[str] = UNDEFINED
public_updates_channel_id: APINullable[Snowflake] = UNDEFINED
max_video_channel_users: APINullable[int] = UNDEFINED
approximate_member_count: APINullable[int] = UNDEFINED
approximate_presence_count: APINullable[int] = UNDEFINED
nsfw_level: APINullable[int] = UNDEFINED
premium_progress_bar_enabled: APINullable[bool] = UNDEFINED
stage_instances: APINullable[List] = UNDEFINED
stickers: APINullable[List] = UNDEFINED
welcome_screen: APINullable = UNDEFINED
guild_scheduled_events: APINullable[List] = UNDEFINED
# TODO: Make a structure for welcome_screen, stage_instances,
# stickers and guild_scheduled_events

View file

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

View file

@ -8,8 +8,8 @@ from enum import IntEnum
from typing import TYPE_CHECKING, Optional
from ...utils import Snowflake
from ...utils import APIModelBase
from ...utils.types import APINullable
from ...utils.api_model import APIModelBase
from ...utils.types import APINullable, UNDEFINED
if TYPE_CHECKING:
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)
"""
id: APINullable[Snowflake] = None
type: APINullable[int] = None
guild_id: APINullable[Snowflake] = None
channel_id: APINullable[Snowflake] = None
user: APINullable[User] = None
name: APINullable[str] = None
avatar: APINullable[str] = None
token: APINullable[str] = None
application_id: APINullable[Snowflake] = None
source_guild: APINullable[Guild] = None
source_channel: APINullable[Channel] = None
url: APINullable[str] = None
id: APINullable[Snowflake] = UNDEFINED
type: APINullable[int] = UNDEFINED
guild_id: APINullable[Snowflake] = UNDEFINED
channel_id: APINullable[Snowflake] = UNDEFINED
user: APINullable[User] = UNDEFINED
name: APINullable[str] = UNDEFINED
avatar: APINullable[str] = UNDEFINED
token: APINullable[str] = UNDEFINED
application_id: APINullable[Snowflake] = UNDEFINED
source_guild: APINullable[Guild] = UNDEFINED
source_channel: APINullable[Channel] = UNDEFINED
url: APINullable[str] = UNDEFINED
async def delete(
self, *, webhook_id: Optional[Snowflake] = None, reason: Optional[str] = None

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,6 @@
from __future__ import annotations
import copy
import datetime
from dataclasses import _is_dataclass_instance, fields
from enum import Enum, EnumMeta
from inspect import getfullargspec
@ -19,45 +18,58 @@ from typing import (
Any,
get_origin,
Tuple,
get_args,
get_args, Optional,
)
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")
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):
value = to_dict_without_none(getattr(model, field.name))
if _is_dataclass_instance(obj):
result = []
for f in fields(obj):
value = _asdict_ignore_none(getattr(obj, f.name))
if isinstance(value, Enum):
result.append((field.name, value.value))
elif value is not None and not field.name.startswith("_"):
result.append((field.name, value))
result.append((f.name, value.value))
# This if statement was added to the function
elif not isinstance(value, UndefinedType) and not f.name.startswith(
"_"
):
result.append((f.name, value))
return dict(result)
if isinstance(model, tuple) and hasattr(model, "_fields"):
return type(model)(*[to_dict_without_none(v) for v in model])
elif isinstance(obj, tuple) and hasattr(obj, "_fields"):
return type(obj)(*[_asdict_ignore_none(v) for v in obj])
if isinstance(model, (list, tuple)):
return type(model)(to_dict_without_none(v) for v in model)
elif isinstance(obj, (list, tuple)):
return type(obj)(_asdict_ignore_none(v) for v in obj)
if isinstance(model, dict):
return type(model)(
(to_dict_without_none(k), to_dict_without_none(v)) for k, v in model.items()
elif isinstance(obj, dict):
return type(obj)(
(_asdict_ignore_none(k), _asdict_ignore_none(v))
for k, v in obj.items()
)
return copy.deepcopy(model)
else:
return copy.deepcopy(obj)
class APIModelBase:
@ -65,12 +77,12 @@ class APIModelBase:
Represents an object which has been fetched from the Discord API.
"""
_client = None
_client: Optional[Any] = None
@property
def _http(self):
if not self._client:
return None
raise AttributeError("Object is not yet linked to a client")
return self._client.http
@ -78,17 +90,21 @@ class APIModelBase:
def set_client(cls, 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)
if origin is Union:
# Ahh yes, typing module has no type annotations for this...
# noinspection PyTypeChecker
args: Tuple[type] = get_args(arg_type)
if 2 <= len(args) < 4:
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,)
@ -99,8 +115,8 @@ class APIModelBase:
if getattr(attr_type, "__factory__", None):
factory = attr_type.__factory__
if attr_value is None:
return None
if attr_value is UNDEFINED:
return UNDEFINED
if attr_type is not None and isinstance(attr_value, attr_type):
return attr_value
@ -108,9 +124,6 @@ class APIModelBase:
if isinstance(attr_value, dict):
return factory(attr_value)
if isinstance(attr_value, datetime.datetime):
return attr_value
return factory(attr_value)
def __post_init__(self):
@ -126,14 +139,19 @@ class APIModelBase:
if attr.startswith("_"):
continue
types = self.__get_types(attr_type)
types = self.__get_types(attr, attr_type)
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:
raise TypeError
raise ValueError(
f"Attribute `{attr}` in `{type(self).__name__}` only "
"consisted of missing/optional type!"
)
specific_tp = types[0]
@ -143,7 +161,7 @@ class APIModelBase:
specific_tp = tp
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])):
attr_value = [
self.__attr_convert(attr_item, classes[0])
@ -179,23 +197,30 @@ class APIModelBase:
return super().__str__()
@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.
"""
if isinstance(data, cls):
return data
# Disable inspection for IDE because this is valid code for the
# inherited classes:
# noinspection PyArgumentList
return cls(
**dict(
map(
lambda 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(
lambda object_argument: data.get(object_argument) is not None,
lambda object_argument: data.get(object_argument)
is not None,
getfullargspec(cls.__init__).args,
),
)
@ -203,4 +228,8 @@ class APIModelBase:
)
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]])
APINullable = Union[T, None]
UndefinedOr = Union[T, UndefinedType]
APINullable = Union[T, UndefinedType]
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
from melisa import Embed, Timestamp
from melisa import Embed, Timestamp, Color
dict_embed = {
'title': 'my title',
'description': 'simple description',
'color': 252307,
'timestamp': datetime.datetime.utcfromtimestamp(1649748784).isoformat(),
'footer': {
'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:
def test_total_length_when_embed_is_empty(self):
@ -54,9 +65,15 @@ class TestEmbed:
embed.set_footer(text="cool footer text")
assert embed.total_length() == 53
def test_comparing_embeds(self):
embed = Embed(title="my title", description="simple description")
embed.set_author(name="best author")
embed.set_footer(text="cool footer text")
embed.set_timestamp(Timestamp.parse(1649748784))
assert embed.to_dict() == dict_embed
def test_embed_to_dict(self):
"""
Tests whether or not the dispatch class its string conversion
is correct.
"""
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
)