import dataclasses from dataclasses import field from typing import Optional, Any from enum import IntEnum, IntFlag from json import JSONEncoder class PostType(IntEnum): UNKNOWN = 0 POST = 1 REPOST = 2 PREVIEW = 3 TOMBSTONE = 4 class DateType(IntEnum): UNKNOWN = 0 CREATED = 1 EDITED = 2 RETRIEVED = 3 class DateMeta(IntFlag): NONE = 0 ESTIMATE = 1 GUESS = 1 << 1 LOOSE_PRECISION = 1 << 2 EDITED_AT_UNKNOWN_TIME = 1 << 3 ASSUMED_TYPE = 1 << 4 class MediaType(IntEnum): UNKNOWN = 0 FILE = 1 AUDIO = 2 IMAGE = 3 VIDEO = 4 VIDEO_SPLIT = 5 ANIMATION = 6 class TagType(IntEnum): UNKNOWN = 0 GENERAL = 1 ARTIST = 2 CHARACTER = 3 COPYRIGHT = 4 META = 5 DEPRECATED = 6 def df(c: Any) -> Any: return field(default_factory=lambda: c) def default_url_ext_guess(url: str) -> str: slash = url.find('/') if slash < 0: return '' url = url[slash:] question = url.rfind('?') if question >= 0: url = url[0:question] s = url.split('.') if len(s) > 1: return s[-1] return '' @dataclasses.dataclass class Url(): url: str ext: str def __init__(self, url: str, ext: Optional[str] = None) -> None: self.url = url if not ext: ext = default_url_ext_guess(url) self.ext = ext @dataclasses.dataclass class Media(): type: MediaType = MediaType.UNKNOWN url: Url = df(Url('')) thumbnail_url: Url = df(Url('')) @dataclasses.dataclass class File(Media): type: MediaType = MediaType.FILE name: str = '' @dataclasses.dataclass class Audio(Media): type: MediaType = MediaType.AUDIO @dataclasses.dataclass class Image(Media): type: MediaType = MediaType.IMAGE @dataclasses.dataclass class Video(Media): type: MediaType = MediaType.VIDEO @dataclasses.dataclass class VideoSplit(Media): type: MediaType = MediaType.VIDEO_SPLIT audio_url: Url = df(Url('')) subtitle_url: Url = df(Url('')) @dataclasses.dataclass class Animation(Media): type: MediaType = MediaType.ANIMATION frames: list[tuple[str, int]] = df([]) @dataclasses.dataclass class Tag(): type: TagType = TagType.UNKNOWN name: str = '' alts: dict[str, str] = df({}) ''' Date(DateType.CREATED, (1396411200.0, 1396756800.0), DateMeta.ESTIMATE) - Post is estimated to have been created at some point between April 2nd and 6th 2014. Date(DateType.EDITED, (1396411200.0, 1550247862.0), DateMeta.EDITED_AT_UNKNOWN_TIME) - We have no clue when the post was edited, but it had to have been between the time it was created and now. - EDITED_AT_UNKNOWN_TIME should be used when either of CREATED and/or RETRIEVED are used as placeholders. It can be combined with GUESS or ESTIMATE if, for example, there is some indication about the end of the range. Date(DateType.RETRIEVED, (1550247862.0, 1550247862.0), DateMeta.NONE) - Time of processing this post. Will likely be slightly after it was actually retrived but that is negligible. - RETRIEVED can still have a range and meta if applicable. Date(DateType.CREATED, (1483228800.0, 1546300800.0), DateMeta.LOOSE_PRECISION) - Date is exactly between "01 Jan 2017 12:00:00 AM UTC" and "01 Jan 2019 12:00:00 AM UTC". Combined with LOOSE_PRECISION, the application will assume a meaning of "between 2017 and 2019". Date(DateType.CREATED, (1145059200.0, 1145059200.0), DateMeta.ASSUMED_TYPE) - The date of April 15th 2006 is associated with the resource. We are assuming that's the creation date. ''' @dataclasses.dataclass class Date(): type: DateType = DateType.UNKNOWN range: tuple[float, float] = (0.0, 0.0) meta: DateMeta = DateMeta.NONE note: str = '' @dataclasses.dataclass class User(): unique_id: str = '' username: str = '' display_name: str = '' profile_picture_url: Url = df(Url('')) @dataclasses.dataclass class PostRef(): unique_id: str = '' ''' V4: - Improved date. - Track origin module. - Ends when all current modules are ported and/or completed. V5: - Formatted body/text. ''' @dataclasses.dataclass class Post(): version: int = 4 module: str = '' type: PostType = PostType.UNKNOWN unique_id: str = '' raw_responses: dict[str, str] = df({}) url: str = '' dates: list[Date] = df([]) author: User = df(User()) title: str = '' text: str = '' likes: Optional[int] = None bookmarks: Optional[int] = None reposts: Optional[int] = None quotes: Optional[int] = None comments: Optional[int] = None views: Optional[int] = None tags: list[Tag] = df([]) links: list[str] = df([]) media: dict[str, Media] = df({}) post: PostRef = df(PostRef()) quoted: PostRef = df(PostRef()) in_reply_to: PostRef = df(PostRef()) class PostEncoder(JSONEncoder): def default(self, o: Any) -> Any: return o.__dict__