308 lines
6.3 KiB
Python
308 lines
6.3 KiB
Python
|
"""
|
||
|
Dataclasses which represent entities returned by the Mastodon API.
|
||
|
"""
|
||
|
|
||
|
import dataclasses
|
||
|
|
||
|
from dataclasses import dataclass, is_dataclass
|
||
|
from datetime import date, datetime
|
||
|
from typing import Dict, List, Optional, Type, TypeVar, Union
|
||
|
from typing import get_type_hints
|
||
|
|
||
|
from toot.typing_compat import get_args, get_origin
|
||
|
from toot.utils import get_text
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class AccountField:
|
||
|
"""
|
||
|
https://docs.joinmastodon.org/entities/Account/#Field
|
||
|
"""
|
||
|
name: str
|
||
|
value: str
|
||
|
verified_at: Optional[datetime]
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class CustomEmoji:
|
||
|
"""
|
||
|
https://docs.joinmastodon.org/entities/CustomEmoji/
|
||
|
"""
|
||
|
shortcode: str
|
||
|
url: str
|
||
|
static_url: str
|
||
|
visible_in_picker: bool
|
||
|
category: str
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class Account:
|
||
|
"""
|
||
|
https://docs.joinmastodon.org/entities/Account/
|
||
|
"""
|
||
|
id: str
|
||
|
username: str
|
||
|
acct: str
|
||
|
url: str
|
||
|
display_name: str
|
||
|
note: str
|
||
|
avatar: str
|
||
|
avatar_static: str
|
||
|
header: str
|
||
|
header_static: str
|
||
|
locked: bool
|
||
|
fields: List[AccountField]
|
||
|
emojis: List[CustomEmoji]
|
||
|
bot: bool
|
||
|
group: bool
|
||
|
discoverable: Optional[bool]
|
||
|
noindex: Optional[bool]
|
||
|
moved: Optional["Account"]
|
||
|
suspended: Optional[bool]
|
||
|
limited: Optional[bool]
|
||
|
created_at: datetime
|
||
|
last_status_at: Optional[date]
|
||
|
statuses_count: int
|
||
|
followers_count: int
|
||
|
following_count: int
|
||
|
|
||
|
@property
|
||
|
def note_plaintext(self) -> str:
|
||
|
return get_text(self.note)
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class Application:
|
||
|
"""
|
||
|
https://docs.joinmastodon.org/entities/Status/#application
|
||
|
"""
|
||
|
name: str
|
||
|
website: Optional[str]
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class MediaAttachment:
|
||
|
"""
|
||
|
https://docs.joinmastodon.org/entities/MediaAttachment/
|
||
|
"""
|
||
|
id: str
|
||
|
type: str
|
||
|
url: str
|
||
|
preview_url: str
|
||
|
remote_url: Optional[str]
|
||
|
meta: dict
|
||
|
description: str
|
||
|
blurhash: str
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class StatusMention:
|
||
|
"""
|
||
|
https://docs.joinmastodon.org/entities/Status/#Mention
|
||
|
"""
|
||
|
id: str
|
||
|
username: str
|
||
|
url: str
|
||
|
acct: str
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class StatusTag:
|
||
|
"""
|
||
|
https://docs.joinmastodon.org/entities/Status/#Tag
|
||
|
"""
|
||
|
name: str
|
||
|
url: str
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class PollOption:
|
||
|
"""
|
||
|
https://docs.joinmastodon.org/entities/Poll/#Option
|
||
|
"""
|
||
|
title: str
|
||
|
votes_count: Optional[int]
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class Poll:
|
||
|
"""
|
||
|
https://docs.joinmastodon.org/entities/Poll/
|
||
|
"""
|
||
|
id: str
|
||
|
expires_at: Optional[datetime]
|
||
|
expired: bool
|
||
|
multiple: bool
|
||
|
votes_count: int
|
||
|
voters_count: Optional[int]
|
||
|
options: List[PollOption]
|
||
|
emojis: List[CustomEmoji]
|
||
|
voted: Optional[bool]
|
||
|
own_votes: Optional[List[int]]
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class PreviewCard:
|
||
|
"""
|
||
|
https://docs.joinmastodon.org/entities/PreviewCard/
|
||
|
"""
|
||
|
url: str
|
||
|
title: str
|
||
|
description: str
|
||
|
type: str
|
||
|
author_name: str
|
||
|
author_url: str
|
||
|
provider_name: str
|
||
|
provider_url: str
|
||
|
html: str
|
||
|
width: int
|
||
|
height: int
|
||
|
image: Optional[str]
|
||
|
embed_url: str
|
||
|
blurhash: Optional[str]
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class FilterKeyword:
|
||
|
"""
|
||
|
https://docs.joinmastodon.org/entities/FilterKeyword/
|
||
|
"""
|
||
|
id: str
|
||
|
keyword: str
|
||
|
whole_word: str
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class FilterStatus:
|
||
|
"""
|
||
|
https://docs.joinmastodon.org/entities/FilterStatus/
|
||
|
"""
|
||
|
id: str
|
||
|
status_id: str
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class Filter:
|
||
|
"""
|
||
|
https://docs.joinmastodon.org/entities/Filter/
|
||
|
"""
|
||
|
id: str
|
||
|
title: str
|
||
|
context: List[str]
|
||
|
expires_at: Optional[datetime]
|
||
|
filter_action: str
|
||
|
keywords: List[FilterKeyword]
|
||
|
statuses: List[FilterStatus]
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class FilterResult:
|
||
|
"""
|
||
|
https://docs.joinmastodon.org/entities/FilterResult/
|
||
|
"""
|
||
|
filter: Filter
|
||
|
keyword_matches: Optional[List[str]]
|
||
|
status_matches: Optional[str]
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class Status:
|
||
|
"""
|
||
|
https://docs.joinmastodon.org/entities/Status/
|
||
|
"""
|
||
|
id: str
|
||
|
uri: str
|
||
|
created_at: datetime
|
||
|
account: Account
|
||
|
content: str
|
||
|
visibility: str
|
||
|
sensitive: bool
|
||
|
spoiler_text: str
|
||
|
media_attachments: List[MediaAttachment]
|
||
|
application: Optional[Application]
|
||
|
mentions: List[StatusMention]
|
||
|
tags: List[StatusTag]
|
||
|
emojis: List[CustomEmoji]
|
||
|
reblogs_count: int
|
||
|
favourites_count: int
|
||
|
replies_count: int
|
||
|
url: Optional[str]
|
||
|
in_reply_to_id: Optional[str]
|
||
|
in_reply_to_account_id: Optional[str]
|
||
|
reblog: Optional["Status"]
|
||
|
poll: Optional[Poll]
|
||
|
card: Optional[PreviewCard]
|
||
|
language: Optional[str]
|
||
|
text: Optional[str]
|
||
|
edited_at: Optional[datetime]
|
||
|
favourited: Optional[bool]
|
||
|
reblogged: Optional[bool]
|
||
|
muted: Optional[bool]
|
||
|
bookmarked: Optional[bool]
|
||
|
pinned: Optional[bool]
|
||
|
filtered: Optional[List[FilterResult]]
|
||
|
|
||
|
@property
|
||
|
def original(self) -> "Status":
|
||
|
return self.reblog or self
|
||
|
|
||
|
|
||
|
# Generic data class instance
|
||
|
T = TypeVar("T")
|
||
|
|
||
|
|
||
|
def from_dict(cls: Type[T], data: Dict) -> T:
|
||
|
"""Convert a nested dict into an instance of `cls`."""
|
||
|
def _fields():
|
||
|
hints = get_type_hints(cls)
|
||
|
for field in dataclasses.fields(cls):
|
||
|
field_type = _prune_optional(hints[field.name])
|
||
|
default_value = _get_default_value(field)
|
||
|
value = data.get(field.name, default_value)
|
||
|
yield field.name, _convert(field_type, value)
|
||
|
|
||
|
return cls(**dict(_fields()))
|
||
|
|
||
|
|
||
|
def _get_default_value(field):
|
||
|
if field.default is not dataclasses.MISSING:
|
||
|
return field.default
|
||
|
|
||
|
if field.default_factory is not dataclasses.MISSING:
|
||
|
return field.default_factory()
|
||
|
|
||
|
return None
|
||
|
|
||
|
|
||
|
def _convert(field_type, value):
|
||
|
if value is None:
|
||
|
return None
|
||
|
|
||
|
if field_type in [str, int, bool, dict]:
|
||
|
return value
|
||
|
|
||
|
if field_type == datetime:
|
||
|
return datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f%z")
|
||
|
|
||
|
if field_type == date:
|
||
|
return date.fromisoformat(value)
|
||
|
|
||
|
if get_origin(field_type) == list:
|
||
|
(inner_type,) = get_args(field_type)
|
||
|
return [_convert(inner_type, x) for x in value]
|
||
|
|
||
|
if is_dataclass(field_type):
|
||
|
return from_dict(field_type, value)
|
||
|
|
||
|
raise ValueError(f"Not implemented for type '{field_type}'")
|
||
|
|
||
|
|
||
|
def _prune_optional(field_type):
|
||
|
"""For `Optional[<type>]` returns the encapsulated `<type>`."""
|
||
|
if get_origin(field_type) == Union:
|
||
|
args = get_args(field_type)
|
||
|
if len(args) == 2 and args[1] == type(None):
|
||
|
return args[0]
|
||
|
|
||
|
return field_type
|