Add Asset model and avatar helpers (#119)
This commit is contained in:
parent
aa55aa1d4c
commit
4b3b6aeb45
12
README.md
12
README.md
@ -14,6 +14,7 @@ A Python library for interacting with the Discord API, with a focus on bot devel
|
|||||||
- `Message.jump_url` property for quick links to messages
|
- `Message.jump_url` property for quick links to messages
|
||||||
- Built-in caching layer
|
- Built-in caching layer
|
||||||
- `Guild.me` property to access the bot's member object
|
- `Guild.me` property to access the bot's member object
|
||||||
|
- Easy CDN asset handling via the `Asset` model
|
||||||
- Experimental voice support
|
- Experimental voice support
|
||||||
- Helpful error handling utilities
|
- Helpful error handling utilities
|
||||||
|
|
||||||
@ -126,6 +127,17 @@ client = disagreement.Client(
|
|||||||
This dictionary is used whenever ``send_message`` or helpers like ``Message.reply``
|
This dictionary is used whenever ``send_message`` or helpers like ``Message.reply``
|
||||||
are called without an explicit ``allowed_mentions`` argument.
|
are called without an explicit ``allowed_mentions`` argument.
|
||||||
|
|
||||||
|
### Working With Assets
|
||||||
|
|
||||||
|
Properties like ``User.avatar`` and ``Guild.icon`` return :class:`disagreement.Asset` objects.
|
||||||
|
Use ``read`` to get the bytes or ``save`` to write them to disk.
|
||||||
|
|
||||||
|
```python
|
||||||
|
user = await client.fetch_user(123)
|
||||||
|
data = await user.avatar.read()
|
||||||
|
await user.avatar.save("avatar.png")
|
||||||
|
```
|
||||||
|
|
||||||
### Defining Subcommands with `AppCommandGroup`
|
### Defining Subcommands with `AppCommandGroup`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
@ -15,6 +15,7 @@ __copyright__ = "Copyright 2025 Slipstream"
|
|||||||
__version__ = "0.8.1"
|
__version__ = "0.8.1"
|
||||||
|
|
||||||
from .client import Client, AutoShardedClient
|
from .client import Client, AutoShardedClient
|
||||||
|
from .asset import Asset
|
||||||
from .models import (
|
from .models import (
|
||||||
Message,
|
Message,
|
||||||
User,
|
User,
|
||||||
@ -125,6 +126,7 @@ import logging
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"Client",
|
"Client",
|
||||||
"AutoShardedClient",
|
"AutoShardedClient",
|
||||||
|
"Asset",
|
||||||
"Message",
|
"Message",
|
||||||
"User",
|
"User",
|
||||||
"Reaction",
|
"Reaction",
|
||||||
|
51
disagreement/asset.py
Normal file
51
disagreement/asset.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
"""Utility class for Discord CDN assets."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import IO, Optional, Union, TYPE_CHECKING
|
||||||
|
|
||||||
|
import aiohttp # pylint: disable=import-error
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .client import Client
|
||||||
|
|
||||||
|
|
||||||
|
class Asset:
|
||||||
|
"""Represents a CDN asset such as an avatar or icon."""
|
||||||
|
|
||||||
|
def __init__(self, url: str, client_instance: Optional["Client"] = None) -> None:
|
||||||
|
self.url = url
|
||||||
|
self._client = client_instance
|
||||||
|
|
||||||
|
async def read(self) -> bytes:
|
||||||
|
"""Read the asset's bytes."""
|
||||||
|
|
||||||
|
session: Optional[aiohttp.ClientSession] = None
|
||||||
|
if self._client is not None:
|
||||||
|
await self._client._http._ensure_session() # type: ignore[attr-defined]
|
||||||
|
session = self._client._http._session # type: ignore[attr-defined]
|
||||||
|
if session is None:
|
||||||
|
session = aiohttp.ClientSession()
|
||||||
|
close = True
|
||||||
|
else:
|
||||||
|
close = False
|
||||||
|
async with session.get(self.url) as resp:
|
||||||
|
data = await resp.read()
|
||||||
|
if close:
|
||||||
|
await session.close()
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def save(self, fp: Union[str, os.PathLike[str], IO[bytes]]) -> None:
|
||||||
|
"""Save the asset to the given file path or file-like object."""
|
||||||
|
|
||||||
|
data = await self.read()
|
||||||
|
if isinstance(fp, (str, os.PathLike)):
|
||||||
|
path = os.fspath(fp)
|
||||||
|
with open(path, "wb") as file:
|
||||||
|
file.write(data)
|
||||||
|
else:
|
||||||
|
fp.write(data)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Asset url='{self.url}'>"
|
@ -788,11 +788,6 @@ class HTTPClient:
|
|||||||
|
|
||||||
return Webhook(data)
|
return Webhook(data)
|
||||||
|
|
||||||
async def get_webhook(self, webhook_id: "Snowflake") -> Dict[str, Any]:
|
|
||||||
"""Fetches a webhook by ID."""
|
|
||||||
|
|
||||||
return await self.request("GET", f"/webhooks/{webhook_id}")
|
|
||||||
|
|
||||||
async def edit_webhook(
|
async def edit_webhook(
|
||||||
self, webhook_id: "Snowflake", payload: Dict[str, Any]
|
self, webhook_id: "Snowflake", payload: Dict[str, Any]
|
||||||
) -> "Webhook":
|
) -> "Webhook":
|
||||||
@ -818,7 +813,10 @@ class HTTPClient:
|
|||||||
if token is not None:
|
if token is not None:
|
||||||
endpoint += f"/{token}"
|
endpoint += f"/{token}"
|
||||||
use_auth = False
|
use_auth = False
|
||||||
data = await self.request("GET", endpoint, use_auth_header=use_auth)
|
if use_auth:
|
||||||
|
data = await self.request("GET", endpoint)
|
||||||
|
else:
|
||||||
|
data = await self.request("GET", endpoint, use_auth_header=False)
|
||||||
from .models import Webhook
|
from .models import Webhook
|
||||||
|
|
||||||
return Webhook(data)
|
return Webhook(data)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Data models for Discord objects.
|
Data models for Discord objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
@ -47,13 +49,14 @@ from .enums import ( # These enums will need to be defined in disagreement/enum
|
|||||||
from .permissions import Permissions
|
from .permissions import Permissions
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .client import Client # For type hinting to avoid circular imports
|
from .client import Client # For type hinting to avoid circular imports
|
||||||
from .enums import OverwriteType # For PermissionOverwrite model
|
from .enums import OverwriteType # For PermissionOverwrite model
|
||||||
from .ui.view import View
|
from .ui.view import View
|
||||||
from .interactions import Snowflake
|
from .interactions import Snowflake
|
||||||
from .typing import Typing
|
from .typing import Typing
|
||||||
from .shard_manager import Shard
|
from .shard_manager import Shard
|
||||||
|
from .asset import Asset
|
||||||
|
|
||||||
# Forward reference Message if it were used in type hints before its definition
|
# Forward reference Message if it were used in type hints before its definition
|
||||||
# from .models import Message # Not needed as Message is defined before its use in TextChannel.send etc.
|
# from .models import Message # Not needed as Message is defined before its use in TextChannel.send etc.
|
||||||
@ -71,7 +74,12 @@ class User:
|
|||||||
self.username: Optional[str] = data.get("username")
|
self.username: Optional[str] = data.get("username")
|
||||||
self.discriminator: Optional[str] = data.get("discriminator")
|
self.discriminator: Optional[str] = data.get("discriminator")
|
||||||
self.bot: bool = data.get("bot", False)
|
self.bot: bool = data.get("bot", False)
|
||||||
self.avatar: Optional[str] = data.get("avatar")
|
avatar_hash = data.get("avatar")
|
||||||
|
self._avatar: Optional[str] = (
|
||||||
|
f"https://cdn.discordapp.com/avatars/{self.id}/{avatar_hash}.png"
|
||||||
|
if avatar_hash
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mention(self) -> str:
|
def mention(self) -> str:
|
||||||
@ -83,6 +91,25 @@ class User:
|
|||||||
disc = self.discriminator or "????"
|
disc = self.discriminator or "????"
|
||||||
return f"<User id='{self.id}' username='{username}' discriminator='{disc}'>"
|
return f"<User id='{self.id}' username='{username}' discriminator='{disc}'>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def avatar(self) -> Optional["Asset"]:
|
||||||
|
"""Return the user's avatar as an :class:`Asset`."""
|
||||||
|
|
||||||
|
if self._avatar:
|
||||||
|
from .asset import Asset
|
||||||
|
|
||||||
|
return Asset(self._avatar, self._client)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@avatar.setter
|
||||||
|
def avatar(self, value: Optional[Union[str, "Asset"]]) -> None:
|
||||||
|
if isinstance(value, str):
|
||||||
|
self._avatar = value
|
||||||
|
elif value is None:
|
||||||
|
self._avatar = None
|
||||||
|
else:
|
||||||
|
self._avatar = value.url
|
||||||
|
|
||||||
async def send(
|
async def send(
|
||||||
self,
|
self,
|
||||||
content: Optional[str] = None,
|
content: Optional[str] = None,
|
||||||
@ -780,7 +807,12 @@ class Role:
|
|||||||
self.name: str = data["name"]
|
self.name: str = data["name"]
|
||||||
self.color: int = data["color"]
|
self.color: int = data["color"]
|
||||||
self.hoist: bool = data["hoist"]
|
self.hoist: bool = data["hoist"]
|
||||||
self.icon: Optional[str] = data.get("icon")
|
icon_hash = data.get("icon")
|
||||||
|
self._icon: Optional[str] = (
|
||||||
|
f"https://cdn.discordapp.com/role-icons/{self.id}/{icon_hash}.png"
|
||||||
|
if icon_hash
|
||||||
|
else None
|
||||||
|
)
|
||||||
self.unicode_emoji: Optional[str] = data.get("unicode_emoji")
|
self.unicode_emoji: Optional[str] = data.get("unicode_emoji")
|
||||||
self.position: int = data["position"]
|
self.position: int = data["position"]
|
||||||
self.permissions: str = data["permissions"] # String of bitwise permissions
|
self.permissions: str = data["permissions"] # String of bitwise permissions
|
||||||
@ -798,6 +830,23 @@ class Role:
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<Role id='{self.id}' name='{self.name}'>"
|
return f"<Role id='{self.id}' name='{self.name}'>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> Optional["Asset"]:
|
||||||
|
if self._icon:
|
||||||
|
from .asset import Asset
|
||||||
|
|
||||||
|
return Asset(self._icon, None)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@icon.setter
|
||||||
|
def icon(self, value: Optional[Union[str, "Asset"]]) -> None:
|
||||||
|
if isinstance(value, str):
|
||||||
|
self._icon = value
|
||||||
|
elif value is None:
|
||||||
|
self._icon = None
|
||||||
|
else:
|
||||||
|
self._icon = value.url
|
||||||
|
|
||||||
|
|
||||||
class Member(User): # Member inherits from User
|
class Member(User): # Member inherits from User
|
||||||
"""Represents a Guild Member.
|
"""Represents a Guild Member.
|
||||||
@ -826,7 +875,15 @@ class Member(User): # Member inherits from User
|
|||||||
) # Pass user_data or data if user_data is empty
|
) # Pass user_data or data if user_data is empty
|
||||||
|
|
||||||
self.nick: Optional[str] = data.get("nick")
|
self.nick: Optional[str] = data.get("nick")
|
||||||
self.avatar: Optional[str] = data.get("avatar")
|
avatar_hash = data.get("avatar")
|
||||||
|
if avatar_hash:
|
||||||
|
guild_id = data.get("guild_id")
|
||||||
|
if guild_id:
|
||||||
|
self._avatar = f"https://cdn.discordapp.com/guilds/{guild_id}/users/{self.id}/avatars/{avatar_hash}.png"
|
||||||
|
else:
|
||||||
|
self._avatar = (
|
||||||
|
f"https://cdn.discordapp.com/avatars/{self.id}/{avatar_hash}.png"
|
||||||
|
)
|
||||||
self.roles: List[str] = data.get("roles", [])
|
self.roles: List[str] = data.get("roles", [])
|
||||||
self.joined_at: str = data["joined_at"]
|
self.joined_at: str = data["joined_at"]
|
||||||
self.premium_since: Optional[str] = data.get("premium_since")
|
self.premium_since: Optional[str] = data.get("premium_since")
|
||||||
@ -854,6 +911,25 @@ class Member(User): # Member inherits from User
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<Member id='{self.id}' username='{self.username}' nick='{self.nick}'>"
|
return f"<Member id='{self.id}' username='{self.username}' nick='{self.nick}'>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def avatar(self) -> Optional["Asset"]:
|
||||||
|
"""Return the member's avatar as an :class:`Asset`."""
|
||||||
|
|
||||||
|
if self._avatar:
|
||||||
|
from .asset import Asset
|
||||||
|
|
||||||
|
return Asset(self._avatar, self._client)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@avatar.setter
|
||||||
|
def avatar(self, value: Optional[Union[str, "Asset"]]) -> None:
|
||||||
|
if isinstance(value, str):
|
||||||
|
self._avatar = value
|
||||||
|
elif value is None:
|
||||||
|
self._avatar = None
|
||||||
|
else:
|
||||||
|
self._avatar = value.url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_name(self) -> str:
|
def display_name(self) -> str:
|
||||||
"""Return the nickname if set, otherwise the username."""
|
"""Return the nickname if set, otherwise the username."""
|
||||||
@ -921,7 +997,6 @@ class Member(User): # Member inherits from User
|
|||||||
return max(role_objects, key=lambda r: r.position)
|
return max(role_objects, key=lambda r: r.position)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
||||||
def guild_permissions(self) -> "Permissions":
|
def guild_permissions(self) -> "Permissions":
|
||||||
"""Return the member's guild-level permissions."""
|
"""Return the member's guild-level permissions."""
|
||||||
|
|
||||||
@ -948,8 +1023,9 @@ class Member(User): # Member inherits from User
|
|||||||
|
|
||||||
return base
|
return base
|
||||||
|
|
||||||
def voice(self) -> Optional["VoiceState"]:
|
@property
|
||||||
"""Return the member's cached voice state as a :class:`VoiceState`."""
|
def voice(self) -> Optional["VoiceState"]:
|
||||||
|
"""Return the member's cached voice state as a :class:`VoiceState`."""
|
||||||
|
|
||||||
if self.voice_state is None:
|
if self.voice_state is None:
|
||||||
return None
|
return None
|
||||||
@ -1210,9 +1286,24 @@ class Guild:
|
|||||||
)
|
)
|
||||||
self.id: str = data["id"]
|
self.id: str = data["id"]
|
||||||
self.name: str = data["name"]
|
self.name: str = data["name"]
|
||||||
self.icon: Optional[str] = data.get("icon")
|
icon_hash = data.get("icon")
|
||||||
self.splash: Optional[str] = data.get("splash")
|
self._icon: Optional[str] = (
|
||||||
self.discovery_splash: Optional[str] = data.get("discovery_splash")
|
f"https://cdn.discordapp.com/icons/{self.id}/{icon_hash}.png"
|
||||||
|
if icon_hash
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
splash_hash = data.get("splash")
|
||||||
|
self._splash: Optional[str] = (
|
||||||
|
f"https://cdn.discordapp.com/splashes/{self.id}/{splash_hash}.png"
|
||||||
|
if splash_hash
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
discovery_hash = data.get("discovery_splash")
|
||||||
|
self._discovery_splash: Optional[str] = (
|
||||||
|
f"https://cdn.discordapp.com/discovery-splashes/{self.id}/{discovery_hash}.png"
|
||||||
|
if discovery_hash
|
||||||
|
else None
|
||||||
|
)
|
||||||
self.owner: Optional[bool] = data.get("owner")
|
self.owner: Optional[bool] = data.get("owner")
|
||||||
self.owner_id: str = data["owner_id"]
|
self.owner_id: str = data["owner_id"]
|
||||||
self.permissions: Optional[str] = data.get("permissions")
|
self.permissions: Optional[str] = data.get("permissions")
|
||||||
@ -1249,7 +1340,12 @@ class Guild:
|
|||||||
self.max_members: Optional[int] = data.get("max_members")
|
self.max_members: Optional[int] = data.get("max_members")
|
||||||
self.vanity_url_code: Optional[str] = data.get("vanity_url_code")
|
self.vanity_url_code: Optional[str] = data.get("vanity_url_code")
|
||||||
self.description: Optional[str] = data.get("description")
|
self.description: Optional[str] = data.get("description")
|
||||||
self.banner: Optional[str] = data.get("banner")
|
banner_hash = data.get("banner")
|
||||||
|
self._banner: Optional[str] = (
|
||||||
|
f"https://cdn.discordapp.com/banners/{self.id}/{banner_hash}.png"
|
||||||
|
if banner_hash
|
||||||
|
else None
|
||||||
|
)
|
||||||
self.premium_tier: PremiumTier = PremiumTier(data["premium_tier"])
|
self.premium_tier: PremiumTier = PremiumTier(data["premium_tier"])
|
||||||
self.premium_subscription_count: Optional[int] = data.get(
|
self.premium_subscription_count: Optional[int] = data.get(
|
||||||
"premium_subscription_count"
|
"premium_subscription_count"
|
||||||
@ -1357,6 +1453,74 @@ class Guild:
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<Guild id='{self.id}' name='{self.name}'>"
|
return f"<Guild id='{self.id}' name='{self.name}'>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> Optional["Asset"]:
|
||||||
|
if self._icon:
|
||||||
|
from .asset import Asset
|
||||||
|
|
||||||
|
return Asset(self._icon, self._client)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@icon.setter
|
||||||
|
def icon(self, value: Optional[Union[str, "Asset"]]) -> None:
|
||||||
|
if isinstance(value, str):
|
||||||
|
self._icon = value
|
||||||
|
elif value is None:
|
||||||
|
self._icon = None
|
||||||
|
else:
|
||||||
|
self._icon = value.url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def splash(self) -> Optional["Asset"]:
|
||||||
|
if self._splash:
|
||||||
|
from .asset import Asset
|
||||||
|
|
||||||
|
return Asset(self._splash, self._client)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@splash.setter
|
||||||
|
def splash(self, value: Optional[Union[str, "Asset"]]) -> None:
|
||||||
|
if isinstance(value, str):
|
||||||
|
self._splash = value
|
||||||
|
elif value is None:
|
||||||
|
self._splash = None
|
||||||
|
else:
|
||||||
|
self._splash = value.url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def discovery_splash(self) -> Optional["Asset"]:
|
||||||
|
if self._discovery_splash:
|
||||||
|
from .asset import Asset
|
||||||
|
|
||||||
|
return Asset(self._discovery_splash, self._client)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@discovery_splash.setter
|
||||||
|
def discovery_splash(self, value: Optional[Union[str, "Asset"]]) -> None:
|
||||||
|
if isinstance(value, str):
|
||||||
|
self._discovery_splash = value
|
||||||
|
elif value is None:
|
||||||
|
self._discovery_splash = None
|
||||||
|
else:
|
||||||
|
self._discovery_splash = value.url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def banner(self) -> Optional["Asset"]:
|
||||||
|
if self._banner:
|
||||||
|
from .asset import Asset
|
||||||
|
|
||||||
|
return Asset(self._banner, self._client)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@banner.setter
|
||||||
|
def banner(self, value: Optional[Union[str, "Asset"]]) -> None:
|
||||||
|
if isinstance(value, str):
|
||||||
|
self._banner = value
|
||||||
|
elif value is None:
|
||||||
|
self._banner = None
|
||||||
|
else:
|
||||||
|
self._banner = value.url
|
||||||
|
|
||||||
async def fetch_widget(self) -> Dict[str, Any]:
|
async def fetch_widget(self) -> Dict[str, Any]:
|
||||||
"""|coro| Fetch this guild's widget settings."""
|
"""|coro| Fetch this guild's widget settings."""
|
||||||
|
|
||||||
@ -2089,7 +2253,12 @@ class Webhook:
|
|||||||
self.guild_id: Optional[str] = data.get("guild_id")
|
self.guild_id: Optional[str] = data.get("guild_id")
|
||||||
self.channel_id: Optional[str] = data.get("channel_id")
|
self.channel_id: Optional[str] = data.get("channel_id")
|
||||||
self.name: Optional[str] = data.get("name")
|
self.name: Optional[str] = data.get("name")
|
||||||
self.avatar: Optional[str] = data.get("avatar")
|
avatar_hash = data.get("avatar")
|
||||||
|
self._avatar: Optional[str] = (
|
||||||
|
f"https://cdn.discordapp.com/webhooks/{self.id}/{avatar_hash}.png"
|
||||||
|
if avatar_hash
|
||||||
|
else None
|
||||||
|
)
|
||||||
self.token: Optional[str] = data.get("token")
|
self.token: Optional[str] = data.get("token")
|
||||||
self.application_id: Optional[str] = data.get("application_id")
|
self.application_id: Optional[str] = data.get("application_id")
|
||||||
self.url: Optional[str] = data.get("url")
|
self.url: Optional[str] = data.get("url")
|
||||||
@ -2098,6 +2267,25 @@ class Webhook:
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<Webhook id='{self.id}' name='{self.name}'>"
|
return f"<Webhook id='{self.id}' name='{self.name}'>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def avatar(self) -> Optional["Asset"]:
|
||||||
|
"""Return the webhook's avatar as an :class:`Asset`."""
|
||||||
|
|
||||||
|
if self._avatar:
|
||||||
|
from .asset import Asset
|
||||||
|
|
||||||
|
return Asset(self._avatar, self._client)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@avatar.setter
|
||||||
|
def avatar(self, value: Optional[Union[str, "Asset"]]) -> None:
|
||||||
|
if isinstance(value, str):
|
||||||
|
self._avatar = value
|
||||||
|
elif value is None:
|
||||||
|
self._avatar = None
|
||||||
|
else:
|
||||||
|
self._avatar = value.url
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_url(
|
def from_url(
|
||||||
cls, url: str, session: Optional[aiohttp.ClientSession] = None
|
cls, url: str, session: Optional[aiohttp.ClientSession] = None
|
||||||
|
14
tests/test_asset.py
Normal file
14
tests/test_asset.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from disagreement.models import User
|
||||||
|
from disagreement.asset import Asset
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_avatar_returns_asset():
|
||||||
|
user = User({"id": "1", "username": "u", "discriminator": "0001", "avatar": "abc"})
|
||||||
|
avatar = user.avatar
|
||||||
|
assert isinstance(avatar, Asset)
|
||||||
|
assert avatar.url == "https://cdn.discordapp.com/avatars/1/abc.png"
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_avatar_none():
|
||||||
|
user = User({"id": "1", "username": "u", "discriminator": "0001"})
|
||||||
|
assert user.avatar is None
|
Loading…
x
Reference in New Issue
Block a user