view_model.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. """
  2. Implementation of the syndication taxonomy in architecture.md
  3. """
  4. from dataclasses import dataclass, asdict, replace
  5. from typing import List, Dict, Optional, Tuple, Union
  6. import re
  7. @dataclass
  8. class ContentId:
  9. prefix: str
  10. id: str
  11. def __str__ (self):
  12. return self.prefix + self.id
  13. @staticmethod
  14. def parse (content_id, prefix, id_pattern = '(.+)'):
  15. raw_id = content_id[len(prefix):]
  16. id_matches = re.fullmatch(id_pattern, raw_id)
  17. if id_matches:
  18. raw_id = id_matches[0]
  19. return ContentId(prefix=prefix, id=raw_id)
  20. @dataclass
  21. class PublicMetrics:
  22. reply_count: Optional[int] = None
  23. quote_count: Optional[int] = None
  24. retweet_count: Optional[int] = None
  25. like_count: Optional[int] = None
  26. impression_count: Optional[int] = None
  27. bookmark_count: Optional[int] = None
  28. # may be video only
  29. view_count: Optional[int] = None
  30. def items (self):
  31. return asdict(self).items()
  32. @dataclass
  33. class NonPublicMetrics:
  34. impression_count: Optional[int] = None
  35. user_profile_clicks: Optional[int] = None
  36. url_link_clicks: Optional[int] = None
  37. def items (self):
  38. return asdict(self).items()
  39. @dataclass
  40. class MediaItem:
  41. type: str
  42. preview_image_url: str
  43. media_key: Optional[str] = None
  44. image_url: Optional[str] = None
  45. url: Optional[str] = None
  46. content_type: Optional[str] = None
  47. duration_ms: Optional[int] = None
  48. height: Optional[int] = None
  49. width: Optional[int] = None
  50. size: Optional[int] = None
  51. public_metrics: Optional[PublicMetrics] = None
  52. @dataclass
  53. class Card:
  54. display_url: Optional[str] = None
  55. source_url: Optional[str] = None
  56. content: Optional[str] = None
  57. title: Optional[str] = None
  58. preview_image_url: Optional[str] = None
  59. image_url: Optional[str] = None
  60. @dataclass
  61. class FeedItemAction:
  62. route: str
  63. route_params: Dict
  64. # intended as external resources,
  65. # not first class display.
  66. # PDF, chat, embedded image (iframe)
  67. @dataclass
  68. class FeedItemAttachment:
  69. name: str
  70. content_type: str
  71. url: Optional[str] = None
  72. content: Optional[object] = None
  73. size: Optional[int] = None
  74. @dataclass
  75. class UnrepliedSection:
  76. feed_item_id: Optional[str] = None
  77. description: Optional[str] = None
  78. text: Optional[str] = None
  79. span: Optional[Tuple[int, int]] = None
  80. def text_from (self, feed_item: 'FeedItem') -> str:
  81. if not self.span or not feed_item.text:
  82. return
  83. return feed_item.text[self.span[0]:self.span[1]]
  84. ReplyingToSection = UnrepliedSection
  85. @dataclass
  86. class FeedItem:
  87. id: str
  88. created_at: str
  89. display_name: str
  90. handle: str
  91. published_by: Optional['FeedServiceUser'] = None
  92. title: Optional[str] = None
  93. text: Optional[str] = None
  94. html: Optional[str] = None
  95. author_is_verified: Optional[bool] = None
  96. url: Optional[str] = None
  97. conversation_id: Optional[str] = None
  98. avi_icon_url: Optional[str] = None
  99. author_url: Optional[str] = None
  100. author_id: Optional[str] = None
  101. source_url: Optional[str] = None
  102. source_author_url: Optional[str] = None
  103. reply_depth: Optional[int] = 0
  104. is_marked: Optional[bool] = None
  105. card: Optional[Card] = None
  106. public_metrics: Optional[PublicMetrics] = None
  107. non_public_metrics: Optional[NonPublicMetrics] = None
  108. retweeted_tweet_id: Optional[str] = None
  109. source_retweeted_by_url: Optional[str] = None
  110. retweeted_by: Optional[str] = None
  111. retweeted_by_url: Optional[str] = None
  112. videos: Optional[List[MediaItem]] = None
  113. photos: Optional[List[MediaItem]] = None
  114. quoted_tweet_id: Optional[str] = None
  115. quoted_tweet: Optional['FeedItem'] = None
  116. replied_tweet_id: Optional[str] = None
  117. replied_tweet: Optional['FeedItem'] = None
  118. note: Optional[str] = None
  119. debug_source_data: Optional[Dict] = None
  120. attachments: Optional[List[FeedItemAttachment]] = None
  121. # At some point we may move to feed_item_actions when the set is known
  122. actions: Optional[Dict[str, FeedItemAction]] = None
  123. # This is a TBD concept to highlight unreplied parts of a message.
  124. unreplied: Optional[List[UnrepliedSection]] = None
  125. # This is a TBD concept to highlight parts of a message that are a reply.
  126. replying_to: Optional[List[ReplyingToSection]] = None
  127. # tm = FeedItem(id="1", text="aa", created_at="fs", display_name="fda", handle="fdsafas")
  128. @dataclass
  129. class ThreadItem:
  130. feed_item: FeedItem
  131. children: Optional[List['ThreadItem']] = None
  132. parent: Optional['ThreadItem'] = None
  133. parents: Optional[List['ThreadItem']] = None
  134. actions: Optional[Dict[str, FeedItemAction]] = None
  135. @dataclass
  136. class FeedServiceUser:
  137. id: str
  138. url: str
  139. name: str # display_name
  140. username: str # handle
  141. is_verified: Optional[bool] = None
  142. is_protected: Optional[bool] = None
  143. created_at: Optional[str] = None
  144. description: Optional[str] = None
  145. preview_image_url: Optional[str] = None # deprecated... rename to avatar_image_url
  146. poster_image_url: Optional[str] = None
  147. website: Optional[str] = None
  148. location: Optional[str] = None
  149. actions: Optional[Dict[str, FeedItemAction]] = None
  150. source_url: Optional[str] = None
  151. raw_user: Optional = None
  152. @property
  153. def display_name (self) -> str:
  154. return self.name
  155. @property
  156. def handle (self) -> str:
  157. return self.username
  158. @property
  159. def avatar_image_url (self) -> str:
  160. return self.preview_image_url
  161. @dataclass
  162. class Collection:
  163. """
  164. Works for both collections and lists for now
  165. """
  166. id:str
  167. name: str
  168. description: Optional[str] = None
  169. preview_image_url: Optional[str] = None
  170. created_at: Optional[str] = None
  171. updated_at: Optional[str] = None
  172. owner_id: Optional[str] = None
  173. owner: Optional[FeedServiceUser] = None
  174. url: Optional[str] = None
  175. total_count: Optional[int] = None
  176. current_page: Optional['CollectionPage'] = None
  177. @dataclass
  178. class CollectionItem:
  179. item: List[Union[FeedServiceUser,FeedItem,ThreadItem,Collection]] = None
  180. sort_order: Optional[int] = None
  181. after_id: Optional[str] = None
  182. @dataclass
  183. class CollectionPage:
  184. """
  185. Works for Collections and Lists for now, as well as Threads and nested Collections.
  186. Feed is a Collection.
  187. """
  188. id: str
  189. items: Optional[List[Union[FeedServiceUser,FeedItem,ThreadItem,CollectionItem,Collection]]] = None
  190. next_token: Optional[str] = None
  191. last_dt: Optional[str] = None
  192. total_count: Optional[int] = None
  193. includes: Optional[Dict] = None
  194. def cleandict(d):
  195. if isinstance(d, dict):
  196. return {k: cleandict(v) for k, v in d.items() if v is not None}
  197. elif isinstance(d, list):
  198. return [cleandict(v) for v in d]
  199. else:
  200. return d