├── models ├── __init__.py ├── common.py ├── search.py ├── post.py ├── topic_post.py ├── topic.py └── user.py ├── requirements.txt ├── exceptions.py ├── constants.py ├── examples ├── user_api_key.py ├── get_post_imgs.py ├── get_user_posts.py ├── get_topic_posts.py ├── get_post_retorts.py ├── newest_recruit.py ├── get_post_votes.py └── statistic_emoji_usage.py ├── .github ├── dependabot.yml └── workflows │ └── tests.yml ├── tests └── test_get_user.py ├── README.md ├── .gitignore ├── utils.py └── client.py /models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.12.2 2 | cryptography==39.0.1 3 | pandas==1.5.3 4 | pytesseract==0.3.10 5 | python_dateutil==2.8.2 6 | Requests==2.31.0 7 | selenium==4.13.0 8 | -------------------------------------------------------------------------------- /exceptions.py: -------------------------------------------------------------------------------- 1 | class ResponseDataFormatErrorException(Exception): 2 | def __init__(self) -> None: 3 | super().__init__('Response data format error! Please check whether cookies are correctly set.') 4 | 5 | class TooManyOperationsException(Exception): 6 | def __init__(self) -> None: 7 | super().__init__('Too many operations') -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | base_url = 'https://shuiyuan.sjtu.edu.cn' 2 | search_url = f'{base_url}/search.json?' 3 | user_actions_url = f'{base_url}/user_actions.json?' 4 | user_badges_url = f'{base_url}/user-badges' 5 | post_url = f'{base_url}/posts' 6 | not_found_error = '找不到请求的 URL 或资源。' 7 | topic_closed_blurb = '此话题已在最后回复的 90 天后被自动关闭。不再允许新回复。' 8 | too_many_operations_warning = '您执行此操作的次数过多,请稍后再试。' -------------------------------------------------------------------------------- /examples/user_api_key.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | currentdir = os.path.dirname(os.path.realpath(__file__)) 5 | parentdir = os.path.dirname(currentdir) 6 | sys.path.append(parentdir) 7 | 8 | from utils import generate_user_api_key 9 | from client import Client 10 | 11 | 12 | if __name__ == '__main__': 13 | result = generate_user_api_key('Shuiyuan Sample App') 14 | print(result.payload.key) 15 | cli = Client(user_api_key=result.payload.key) 16 | r = cli.list_user_badges('凤凰院真凶') 17 | print(r) 18 | 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /examples/get_post_imgs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from typing import List 4 | 5 | currentdir = os.path.dirname(os.path.realpath(__file__)) 6 | parentdir = os.path.dirname(currentdir) 7 | sys.path.append(parentdir) 8 | 9 | from client import Client 10 | 11 | if __name__ == '__main__': 12 | cookies = '' 13 | with open('YOUR_COOKIES.txt', 'r') as f: 14 | cookies = f.read() 15 | cli = Client(cookies=cookies) 16 | post = cli.retrieve_single_post(2110122) 17 | 18 | os.mkdir("good_stuff") 19 | dir = "good_stuff" 20 | 21 | imgs = post.get_imgs() 22 | for (i, img) in enumerate(imgs): 23 | save_path = os.path.join(dir, f'{i}.jpeg') 24 | cli.download_image(img.src, save_path) 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/get_user_posts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | currentdir = os.path.dirname(os.path.realpath(__file__)) 5 | parentdir = os.path.dirname(currentdir) 6 | sys.path.append(parentdir) 7 | 8 | from client import Client 9 | from models.search import SearchQuery 10 | 11 | 12 | if __name__ == '__main__': 13 | cookies = '' 14 | with open('YOUR_COOKIES.txt', 'r') as f: 15 | cookies = f.read() 16 | cli = Client(cookies=cookies) 17 | r = cli.search(SearchQuery(username='凤凰院真凶'), page=1) 18 | for post in r.posts: 19 | if post.is_full(): 20 | print(post.blurb) 21 | else: 22 | r2 = cli.retrieve_single_post(post.id) 23 | if r2 is not None: 24 | print(r2.raw) 25 | 26 | -------------------------------------------------------------------------------- /tests/test_get_user.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | currentdir = os.path.dirname(os.path.realpath(__file__)) 5 | parentdir = os.path.dirname(currentdir) 6 | sys.path.append(parentdir) 7 | 8 | from client import Client 9 | 10 | def test_get_user(): 11 | user_api_key = os.environ['USER_API_KEY'] 12 | cli = Client(user_api_key=user_api_key) 13 | r = cli.get_user_by_username('addda') 14 | print(r.user.name) 15 | r = cli.get_user_by_username('凤凰院真凶') 16 | print(r.user.name) 17 | 18 | def test_get_user_actions(): 19 | user_api_key = os.environ['USER_API_KEY'] 20 | cli = Client(user_api_key=user_api_key) 21 | r = cli.get_user_actions('addda') 22 | print(len(r.user_actions)) 23 | r = cli.get_user_actions('凤凰院真凶') 24 | print(len(r.user_actions)) -------------------------------------------------------------------------------- /examples/get_topic_posts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from typing import List 4 | 5 | currentdir = os.path.dirname(os.path.realpath(__file__)) 6 | parentdir = os.path.dirname(currentdir) 7 | sys.path.append(parentdir) 8 | 9 | from client import Client 10 | 11 | def get_post_stream(cli: Client, topic_id: int) -> List[int]: 12 | topic = cli.get_single_topic(topic_id) 13 | return topic.post_stream.stream 14 | 15 | if __name__ == '__main__': 16 | cookies = '' 17 | with open('YOUR_COOKIES.txt', 'r') as f: 18 | cookies = f.read() 19 | cli = Client(cookies=cookies) 20 | 21 | testcases = [35167, 205204, 22873, 194085] 22 | 23 | for testcase in testcases: 24 | post_ids = get_post_stream(cli, testcase) 25 | topic = cli.get_single_topic(testcase) 26 | print(f'主题"{topic.title}"共有{len(post_ids)}条讨论') 27 | 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | branches: 9 | - "main" 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout actions 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up Python 3.11 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: 3.11 23 | cache: 'pip' 24 | 25 | - name: Display Python version 26 | run: python -c "import sys; print(sys.version)" 27 | 28 | - name: Download requirements 29 | run: | 30 | pip install -r requirements.txt 31 | pip install pytest 32 | 33 | # Run the tests. I'm using pytest and the file is in the tests directory. 34 | - name: Run tests 35 | run: USER_API_KEY=${{ secrets.USER_API_KEY }} pytest tests/test* 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shuiyuan Client 2 | 3 | ![python 3.11](https://img.shields.io/badge/python-3.11-blue.svg) 4 | 5 | [水源社区](https://shuiyuan.sjtu.edu.cn/)是交大人日常必备的网站之一,社区里有非常丰富的资源和信息,开发一个便于调用接口的 client 比较有必要。 6 | 7 | ## 使用方法 8 | + 创建 client 9 | + 手动复制 cookies 10 | ```python 11 | cli = Client(cookies='COPY_YOUR_COOKIES_HERE') 12 | ``` 13 | + 提供 jaccount 和密码,通过 selenium 自动获取 cookies 14 | ```python 15 | cli = Client(jaccount='YOUR_JACCOUNT', pwd='YOUR_PASSWORD') 16 | ``` 17 | + 使用 User-Api-Key(见 https://shuiyuan.sjtu.edu.cn/t/topic/123808 ) 18 | ```python 19 | cli = Client(user_api_key='YOUR_USER_API_KEY') 20 | ``` 21 | 22 | ## 使用例子 23 | 请见 examples 目录: 24 | + [statistic_emoji_usage.py](./examples/statistic_emoji_usage.py): 统计用户使用了哪些 emoji 25 | + [get_topic_posts.py](./examples/get_topic_posts.py): 获取某一主题下所有的 posts 26 | + [get_post_imgs.py](./examples/get_post_imgs.py): 获取某一 post 的所有图片链接(荣光楼🥵) 27 | + [get_post_votes.py](./examples/get_post_votes.py): 获取某一主题下(投票水楼)的投票信息 28 | + [get_post_retorts.py](./examples/get_post_retorts.py): 获取某一 Post 被贴的表情 29 | + [get_user_posts.py](./examples//get_user_posts.py): 获取某一用户所有的 posts 30 | + [newest_recruit.py](./examples/newest_recruit.py): 获取最新的近半个月以来的招募信息 31 | + [user_api_key.py](./examples/user_api_key.py): 获取并使用 User-Api-Key 32 | -------------------------------------------------------------------------------- /examples/get_post_retorts.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | from typing import List 5 | 6 | currentdir = os.path.dirname(os.path.realpath(__file__)) 7 | parentdir = os.path.dirname(currentdir) 8 | sys.path.append(parentdir) 9 | 10 | from client import Client 11 | 12 | def unicode_str_to_emoji(unicode_str): 13 | unicode_list = unicode_str.split() 14 | emoji = "" 15 | for code_point in unicode_list: 16 | try: 17 | code_point_int = int(code_point, 16) 18 | emoji += chr(code_point_int) 19 | except ValueError: 20 | emoji += f"\\u{code_point}" 21 | return emoji 22 | 23 | if __name__ == '__main__': 24 | cookies = '' 25 | with open('YOUR_COOKIES.txt', 'r') as f: 26 | cookies = f.read() 27 | cli = Client(cookies=cookies) 28 | 29 | with open('./db.json', 'r') as f: 30 | db_json = f.read() 31 | db = json.loads(db_json) 32 | emojis = db['emojis'] 33 | emoji_code_map = {} 34 | 35 | for emoji in emojis: 36 | emoji_code_map[emoji['name']] = emoji['code'] 37 | 38 | post = cli.retrieve_single_post(2404119) 39 | for retort in post.retorts: 40 | usernames = retort.usernames 41 | emoji = retort.emoji 42 | if emoji in emoji_code_map: 43 | emoji = unicode_str_to_emoji(emoji_code_map[emoji]) 44 | 45 | print(f'{usernames[0]}等{len(usernames)}人贴了{emoji}') 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /examples/newest_recruit.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import pandas as pd 3 | 4 | import os 5 | import sys 6 | currentdir = os.path.dirname(os.path.realpath(__file__)) 7 | parentdir = os.path.dirname(currentdir) 8 | sys.path.append(parentdir) 9 | 10 | from client import Client 11 | from constants import base_url 12 | from models.search import SearchQuery, SearchQueryOrder, SearchQueryStatus 13 | 14 | 15 | if __name__ == '__main__': 16 | cookies = '' 17 | with open('YOUR_COOKIES.txt', 'r') as f: 18 | cookies = f.read() 19 | cli = Client(cookies=cookies) 20 | 21 | out_data = { 22 | '标题': [], 23 | '发布日期': [], 24 | 'url': [] 25 | } 26 | current_date = datetime.now() 27 | fifteen_days_ago = current_date - timedelta(days=15) 28 | fifteen_days_ago = fifteen_days_ago.strftime("%Y-%m-%d") 29 | 30 | data = cli.search(SearchQuery('招募', order=SearchQueryOrder.LATEST, 31 | after=fifteen_days_ago, status=SearchQueryStatus.OPEN)) 32 | 33 | for topic in data.topics: 34 | created_at = topic.created_at 35 | created_at = created_at.strftime("%Y年%m月%d日 %H时%M分%S秒") 36 | 37 | url = f'{base_url}/t/topic/{topic.id}' 38 | out_data['标题'].append(topic.title) 39 | out_data['url'].append(url) 40 | out_data['发布日期'].append(created_at) 41 | 42 | out_data = pd.DataFrame(out_data) 43 | out_data['raw_url'] = out_data['url'] 44 | out_data['url'] = out_data['url'].apply( 45 | lambda x: f'=HYPERLINK("{x}", "{x}")') 46 | out_data.to_excel('./latest.xlsx') 47 | -------------------------------------------------------------------------------- /models/common.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List, TypeVar, Type, Callable, cast 3 | from datetime import datetime 4 | 5 | import dateutil.parser 6 | 7 | T = TypeVar("T") 8 | EnumT = TypeVar("EnumT", bound=Enum) 9 | 10 | 11 | def from_none(x: Any) -> Any: 12 | assert x is None 13 | return x 14 | 15 | 16 | def from_str(x: Any) -> str: 17 | if x is None: 18 | return '' 19 | assert isinstance(x, str) 20 | return x 21 | 22 | 23 | def from_int(x: Any) -> int: 24 | if x is None: 25 | return 0 26 | assert isinstance(x, int) and not isinstance(x, bool) 27 | return x 28 | 29 | 30 | def from_bool(x: Any) -> bool: 31 | if x is None: 32 | return False 33 | assert isinstance(x, bool) 34 | return x 35 | 36 | 37 | def from_list(f: Callable[[Any], T], x: Any) -> List[T]: 38 | if x is None: 39 | return [] 40 | assert isinstance(x, list) 41 | return [f(y) for y in x] 42 | 43 | 44 | def from_datetime(x: Any) -> datetime: 45 | return dateutil.parser.parse(x) 46 | 47 | 48 | def from_float(x: Any) -> float: 49 | if x is None: 50 | return 0 51 | assert isinstance(x, (float, int)) and not isinstance(x, bool) 52 | return float(x) 53 | 54 | 55 | def from_union(fs, x): 56 | for f in fs: 57 | try: 58 | return f(x) 59 | except: 60 | pass 61 | assert False 62 | 63 | 64 | def to_enum(c: Type[EnumT], x: Any) -> EnumT: 65 | assert isinstance(x, c) 66 | return x.value 67 | 68 | 69 | def to_class(c: Type[T], x: Any) -> dict: 70 | assert isinstance(x, c) 71 | return cast(Any, x).to_dict() 72 | -------------------------------------------------------------------------------- /examples/get_post_votes.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | from typing import List 5 | 6 | from bs4 import BeautifulSoup, Tag 7 | 8 | 9 | currentdir = os.path.dirname(os.path.realpath(__file__)) 10 | parentdir = os.path.dirname(currentdir) 11 | sys.path.append(parentdir) 12 | from client import Client 13 | from models.topic_post import Option 14 | 15 | def plot_votes(title: str, options: List[Option], out_dir: str): 16 | import matplotlib.pyplot as plt 17 | plt.clf() 18 | candidates = [] 19 | votes = [] 20 | 21 | for option in options: 22 | candidates.append(option.html) 23 | votes.append(option.votes) 24 | 25 | plt.rcParams['font.sans-serif'] = ['Arial Unicode MS'] 26 | plt.rcParams['axes.unicode_minus'] = False 27 | plt.title(title) 28 | plt.bar(candidates, votes, color='skyblue') 29 | 30 | out_path = os.path.join(out_dir, f'{title}.png') 31 | plt.savefig(out_path) 32 | 33 | 34 | if __name__ == '__main__': 35 | cookies = '' 36 | with open('YOUR_COOKIES.txt', 'r') as f: 37 | cookies = f.read() 38 | 39 | cli = Client(cookies=cookies) 40 | post_id = 1412235 41 | post = cli.retrieve_single_post(post_id) 42 | topic_id = post.topic_id 43 | posts = cli.get_topic_posts(topic_id, post_ids=[post_id]) 44 | 45 | if not os.path.exists('votes'): 46 | os.mkdir("votes") 47 | 48 | for post in posts.post_stream.posts: 49 | soup = BeautifulSoup(post.cooked, 'html.parser') 50 | poll_elements: List[Tag] = soup.find_all("div", attrs={"class": "poll"}) 51 | 52 | vote_title_map = {} 53 | for poll_element in poll_elements: 54 | title = poll_element.find_previous("p").get_text() 55 | poll_name = poll_element.get("data-poll-name") 56 | vote_title_map[poll_name] = title 57 | 58 | for poll in post.polls: 59 | plot_votes(vote_title_map[poll.name], poll.options, "votes") 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /examples/statistic_emoji_usage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import re 5 | import time 6 | 7 | currentdir = os.path.dirname(os.path.realpath(__file__)) 8 | parentdir = os.path.dirname(currentdir) 9 | sys.path.append(parentdir) 10 | 11 | from client import Client 12 | from models.search import SearchQuery 13 | from exceptions import TooManyOperationsException 14 | 15 | def unicode_str_to_emoji(unicode_str): 16 | unicode_list = unicode_str.split() 17 | emoji = "" 18 | for code_point in unicode_list: 19 | try: 20 | code_point_int = int(code_point, 16) 21 | emoji += chr(code_point_int) 22 | except ValueError: 23 | emoji += f"\\u{code_point}" 24 | return emoji 25 | 26 | if __name__ == '__main__': 27 | cookies = '' 28 | with open('YOUR_COOKIES.txt', 'r') as f: 29 | cookies = f.read() 30 | cli = Client(cookies=cookies) 31 | db_json = '' 32 | with open('./db.json', 'r') as f: 33 | db_json = f.read() 34 | db = json.loads(db_json) 35 | emojis = db['emojis'] 36 | emoji_code_map = {} 37 | 38 | for emoji in emojis: 39 | emoji_code_map[emoji['name']] = emoji['code'] 40 | 41 | emoji_usage_statistic = {} 42 | 43 | last_post_ids = [] 44 | page_idx = 1 45 | # todo: fix failed 46 | while True: 47 | print(f'统计第{page_idx}页...') 48 | try: 49 | r = cli.search(SearchQuery(username='凤凰院真凶'), page=page_idx) 50 | except TooManyOperationsException: 51 | time.sleep(5) 52 | continue 53 | 54 | if last_post_ids == r.grouped_search_result.post_ids: 55 | break 56 | page_idx += 1 57 | last_post_ids = r.grouped_search_result.post_ids 58 | for post in r.posts: 59 | post_content = '' 60 | if post.is_full(): 61 | post_content = post.blurb 62 | else: 63 | r2 = cli.retrieve_single_post(post.id) 64 | if r2 is not None: 65 | post_content = r2.raw 66 | matched_emojis = re.findall(r':(\w+):', post_content) 67 | 68 | for matched_emoji in matched_emojis: 69 | if matched_emoji in emoji_code_map: 70 | code: str = emoji_code_map[matched_emoji] 71 | code = code.replace('-', ' ') 72 | emoji = unicode_str_to_emoji(code) 73 | if emoji not in emoji_usage_statistic: 74 | emoji_usage_statistic[emoji] = 0 75 | emoji_usage_statistic[emoji] += 1 76 | print(emoji_usage_statistic) 77 | time.sleep(0.5) 78 | sorted_dict = sorted(emoji_usage_statistic.items(), key=lambda x: x[1], reverse=True) 79 | 80 | for key, value in sorted_dict: 81 | print(f"{key}: {value}") 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /examples/YOUR_COOKIES.txt 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/#use-with-ide 112 | .pdm.toml 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import base64 3 | import json 4 | import secrets 5 | import urllib.parse 6 | import uuid 7 | import webbrowser 8 | from collections.abc import Iterable 9 | from dataclasses import dataclass 10 | 11 | from cryptography.hazmat.primitives import serialization 12 | from cryptography.hazmat.primitives.asymmetric import padding, rsa 13 | 14 | from models.search import SearchQuery 15 | 16 | 17 | def is_valid_date(date_str: str): 18 | try: 19 | datetime.strptime(date_str, '%Y-%m-%d') 20 | return True 21 | except ValueError: 22 | return False 23 | 24 | 25 | def pack_query(q: SearchQuery) -> str: 26 | terms = [] 27 | if len(q.term) > 0: 28 | terms.append(q.term) 29 | if len(q.username) > 0: 30 | terms.append(f'@{q.username}') 31 | if len(q.category) > 0: 32 | terms.append(f'category:{q.category}') 33 | if len(q.before) > 0 and is_valid_date(q.before): 34 | terms.append(f'before:{q.before}') 35 | if len(q.after) > 0 and is_valid_date(q.after): 36 | terms.append(f'after:{q.after}') 37 | if len(q.order.value) > 0: 38 | terms.append(f'order:{q.order.value}') 39 | if len(q.status.value) > 0: 40 | terms.append(f'status:{q.status.value}') 41 | if len(q.in_whats) > 0: 42 | for in_what in q.in_whats: 43 | terms.append(f'in:{in_what.value}') 44 | return ' '.join(terms) 45 | 46 | 47 | SITE_URL_BASE = 'https://shuiyuan.sjtu.edu.cn' 48 | ALL_SCOPES = [ 49 | 'read', 50 | 'write', 51 | 'message_bus', 52 | 'push', 53 | 'one_time_password', 54 | 'notifications', 55 | 'session_info', 56 | 'bookmarks_calendar', 57 | 'user_status', 58 | ] 59 | DEFAULT_SCOPES = ['read'] 60 | 61 | 62 | @dataclass 63 | class UserApiKeyPayload: 64 | key: str 65 | nonce: str 66 | push: bool 67 | api: int 68 | 69 | 70 | @dataclass 71 | class UserApiKeyRequestResult: 72 | client_id: str 73 | payload: UserApiKeyPayload 74 | 75 | # author: https://shuiyuan.sjtu.edu.cn/t/topic/123808 76 | def generate_user_api_key( 77 | application_name: str, *, 78 | client_id: str | None = None, 79 | scopes: Iterable[str] | None = None, 80 | ) -> UserApiKeyRequestResult: 81 | # Generate RSA key pair. 82 | private_key = rsa.generate_private_key( 83 | public_exponent=65537, 84 | key_size=4096, 85 | ) 86 | public_key = private_key.public_key() 87 | public_key_pem = public_key.public_bytes( 88 | encoding=serialization.Encoding.PEM, 89 | format=serialization.PublicFormat.SubjectPublicKeyInfo, 90 | ).decode('ascii') 91 | 92 | # Generate a random client ID if not provided. 93 | client_id_to_use = str(uuid.uuid4()) if client_id is None else client_id 94 | nonce = secrets.token_urlsafe(32) 95 | 96 | # Validate scopes. 97 | scopes_list = DEFAULT_SCOPES if scopes is None else list(scopes) 98 | if not set(scopes_list) <= set(ALL_SCOPES): 99 | raise ValueError('Invalid scopes') 100 | 101 | # Build request URL and open in browser. 102 | params_dict: dict[str, str] = { 103 | 'application_name': application_name, 104 | 'client_id': client_id_to_use, 105 | 'scopes': ','.join(scopes_list), 106 | 'public_key': public_key_pem, 107 | 'nonce': nonce, 108 | } 109 | params_str = '&'.join( 110 | f'{k}={urllib.parse.quote(v)}' for k, v in params_dict.items()) 111 | webbrowser.open(f'{SITE_URL_BASE}/user-api-key/new?{params_str}') 112 | 113 | # Receive, decrypt and check response payload from server. 114 | enc_payload = input('Paste the response payload here: ') 115 | dec_payload = UserApiKeyPayload(**json.loads(private_key.decrypt( 116 | base64.b64decode(enc_payload), 117 | padding.PKCS1v15(), 118 | ))) 119 | if dec_payload.nonce != nonce: 120 | raise ValueError('Nonce mismatch') 121 | 122 | # Return client ID and response payload. 123 | return UserApiKeyRequestResult( 124 | client_id=client_id_to_use, 125 | payload=dec_payload, 126 | ) 127 | -------------------------------------------------------------------------------- /client.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Any, Callable, List, Optional, TypeVar, Union 3 | from urllib.parse import urlencode 4 | import requests 5 | from exceptions import ResponseDataFormatErrorException, TooManyOperationsException 6 | 7 | from models.search import SearchQuery, SearchResult 8 | from models.topic import Topic 9 | from models.topic_post import TopicPosts 10 | from models.user import UserActionsInfo, UserBadgesInfo, UserEmailInfo, UserInfo 11 | from models.post import Post 12 | 13 | from selenium import webdriver 14 | 15 | from utils import pack_query 16 | from constants import search_url, user_badges_url, not_found_error, post_url, user_actions_url, base_url, too_many_operations_warning 17 | from selenium.webdriver.common.by import By 18 | import pytesseract 19 | 20 | T = TypeVar("T") 21 | 22 | 23 | class Client(): 24 | def __init__(self, cookies: str = '', user_api_key: str = '', jaccount: str = '', pwd: str = '') -> None: 25 | if user_api_key != '': 26 | self.headers = { 27 | 'User-Api-Key': user_api_key 28 | } 29 | return 30 | 31 | if cookies == '': 32 | cookies = self._get_cookies(jaccount, pwd) 33 | if cookies is None: 34 | raise RuntimeError( 35 | 'Please provide correct jaccount and password!') 36 | 37 | self.headers = { 38 | 'Cookie': cookies, 39 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' 40 | } 41 | 42 | @staticmethod 43 | def _get_supported_webdriver() -> Union[None, webdriver.Chrome, webdriver.Edge, webdriver.Safari]: 44 | try: 45 | return webdriver.Chrome() 46 | except: 47 | pass 48 | 49 | try: 50 | return webdriver.Edge() 51 | except: 52 | pass 53 | 54 | try: 55 | return webdriver.Safari() 56 | except: 57 | pass 58 | return None 59 | 60 | @staticmethod 61 | def _get_cookies(jaccount: str, password: str) -> Optional[str]: 62 | driver = Client._get_supported_webdriver() 63 | if driver is None: 64 | raise RuntimeError('No available web driver!') 65 | 66 | driver.implicitly_wait(10) 67 | driver.get(base_url) 68 | 69 | max_retries = 5 70 | retry_cnt = 0 71 | while retry_cnt < max_retries: 72 | driver.find_element(By.ID, "user").send_keys(jaccount) 73 | driver.find_element(By.ID, "pass").send_keys(password) 74 | 75 | captcha = driver.find_element(By.ID, "captcha-img") 76 | with open('captcha.png', 'wb') as f: 77 | f.write(captcha.screenshot_as_png) 78 | captcha = pytesseract.image_to_string( 79 | 'captcha.png', lang='eng', config='--psm 7') 80 | 81 | driver.find_element(By.ID, "captcha").send_keys(captcha) 82 | 83 | time.sleep(0.5) 84 | 85 | if driver.current_url.find(base_url) != -1: 86 | break 87 | 88 | retry_cnt += 1 89 | 90 | if retry_cnt == max_retries: 91 | return None 92 | 93 | cookies = driver.get_cookies() 94 | cookies = '; '.join( 95 | [f"{cookie['name']}={cookie['value']}" for cookie in cookies]) 96 | return cookies 97 | 98 | def _get_request(self, url: str): 99 | return requests.get(url=url, headers=self.headers) 100 | 101 | def _check_error(self, data: Any) -> bool: 102 | if 'failed' in data and 'message' in data: 103 | if data['message'] == too_many_operations_warning: 104 | raise TooManyOperationsException() 105 | 106 | raise RuntimeError(data['message']) 107 | 108 | if 'errors' in data and not_found_error in data['errors']: 109 | return False 110 | return True 111 | 112 | def _json_response_wrapper(self, r: requests.Response, f: Callable[[Any], T]) -> T | None: 113 | try: 114 | data = r.json() 115 | if self._check_error(data): 116 | return f(data) 117 | return None 118 | except TooManyOperationsException as e: 119 | raise e 120 | except: 121 | print(r.text) 122 | raise ResponseDataFormatErrorException() 123 | 124 | def search(self, query: SearchQuery, page: int = 1) -> SearchResult: 125 | q = pack_query(query) 126 | params = { 127 | 'q': q, 128 | 'page': page 129 | } 130 | url = search_url + urlencode(params) 131 | r = self._get_request(url) 132 | return self._json_response_wrapper(r, SearchResult.from_dict) 133 | 134 | def get_user_by_username(self, username: str) -> Optional[UserInfo]: 135 | url = f'{base_url}/u/{username}.json' 136 | r = self._get_request(url) 137 | return self._json_response_wrapper(r, UserInfo.from_dict) 138 | 139 | def list_user_badges(self, username: str) -> Optional[UserBadgesInfo]: 140 | url = f'{user_badges_url}/{username}.json' 141 | r = self._get_request(url) 142 | return self._json_response_wrapper(r, UserBadgesInfo.from_dict) 143 | 144 | def retrieve_single_post(self, id: str) -> Optional[Post]: 145 | url = f'{post_url}/{id}.json' 146 | r = self._get_request(url) 147 | return self._json_response_wrapper(r, Post.from_dict) 148 | 149 | def get_user_email(self, username: str) -> Optional[UserEmailInfo]: 150 | url = f'{base_url}/u/{username}/emails.json' 151 | r = self._get_request(url) 152 | return self._json_response_wrapper(r, UserEmailInfo.from_dict) 153 | 154 | def get_single_topic(self, id: int) -> Optional[Topic]: 155 | url = f'{base_url}/t/{id}.json' 156 | r = self._get_request(url) 157 | return self._json_response_wrapper(r, Topic.from_dict) 158 | 159 | def get_topic_posts(self, topic_id: int, post_ids: List[int] = []) -> Optional[TopicPosts]: 160 | url = f'{base_url}/t/{topic_id}/posts.json?' 161 | if len(post_ids) > 20: 162 | raise RuntimeError("only support retrieving 20 posts per request!") 163 | for (i, post_id) in enumerate(post_ids): 164 | if i > 0: 165 | url += '&' 166 | url += urlencode({'post_ids[]': post_id}) 167 | r = self._get_request(url) 168 | return self._json_response_wrapper(r, TopicPosts.from_dict) 169 | 170 | def get_user_actions(self, username: str, filter: str = '', offset: Optional[int] = None) -> Optional[UserActionsInfo]: 171 | params = { 172 | 'username': username 173 | } 174 | if len(filter) > 0: 175 | params['filter'] = filter 176 | if offset is not None: 177 | params['offset'] = offset 178 | 179 | url = user_actions_url + urlencode(params) 180 | r = self._get_request(url) 181 | return self._json_response_wrapper(r, UserActionsInfo.from_dict) 182 | 183 | def download_image(self, imgsrc: str, path: str): 184 | r = self._get_request(imgsrc) 185 | with open(path, 'wb') as f: 186 | f.write(r.content) 187 | -------------------------------------------------------------------------------- /models/search.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from enum import Enum 3 | from typing import Any, List, Optional 4 | from models.common import * 5 | from dataclasses import dataclass, field 6 | 7 | 8 | class SearchQueryOrder(Enum): 9 | LATEST = 'latest' 10 | LIKES = 'likes' 11 | VIEWS = 'views' 12 | LATEST_TOPIC = 'latest_topic' 13 | NONE = '' 14 | 15 | 16 | class SearchQueryStatus(Enum): 17 | OPEN = 'open' 18 | CLOSED = 'closed' 19 | PUBLIC = 'public' 20 | ARCHIVED = 'archived' 21 | NO_REPLIES = 'noreplies' 22 | SINGLE_USER = 'single_user' 23 | SOLVED = 'solved' 24 | UNSOLVED = 'unsolved' 25 | NONE = '' 26 | 27 | 28 | class SearchQueryIn(Enum): 29 | TITLE = 'title' 30 | LIKES = 'likes' 31 | PERSONAL = 'personal' 32 | MESSAGES = 'messages' 33 | SEEN = 'seen' 34 | UNSEEN = 'unseen' 35 | POSTED = 'posted' 36 | CREATED = 'created' 37 | WATCHING = 'watching' 38 | TRACKING = 'tracking' 39 | BOOKMARKS = 'bookmarks' 40 | ASSIGNED = 'assigned' 41 | UNASSIGNED = 'unassigned' 42 | FIRST = 'first' 43 | PINNED = 'pinned' 44 | WIKI = 'wiki' 45 | 46 | 47 | @dataclass 48 | class SearchQuery(): 49 | term: str = '' 50 | username: str = '' 51 | category: str = '' 52 | tags: List[str] = field(default_factory=lambda: []) 53 | before: str = '' 54 | after: str = '' 55 | order: SearchQueryOrder = SearchQueryOrder.NONE 56 | status: SearchQueryStatus = SearchQueryStatus.NONE 57 | in_whats: List[SearchQueryIn] = field(default_factory=lambda: []) 58 | 59 | 60 | @dataclass 61 | class GroupedSearchResult: 62 | more_posts: None 63 | more_users: None 64 | more_categories: None 65 | term: str 66 | search_log_id: int 67 | more_full_page_results: bool 68 | can_create_topic: bool 69 | error: None 70 | post_ids: List[int] 71 | user_ids: List[Any] 72 | category_ids: List[Any] 73 | tag_ids: List[Any] 74 | group_ids: List[Any] 75 | 76 | @staticmethod 77 | def from_dict(obj: Any) -> 'GroupedSearchResult': 78 | assert isinstance(obj, dict) 79 | more_posts = from_none(obj.get("more_posts")) 80 | more_users = from_none(obj.get("more_users")) 81 | more_categories = from_none(obj.get("more_categories")) 82 | term = from_str(obj.get("term")) 83 | search_log_id = from_int(obj.get("search_log_id")) 84 | more_full_page_results = from_bool(obj.get("more_full_page_results")) 85 | can_create_topic = from_bool(obj.get("can_create_topic")) 86 | error = from_none(obj.get("error")) 87 | post_ids = from_list(from_int, obj.get("post_ids")) 88 | user_ids = from_list(lambda x: x, obj.get("user_ids")) 89 | category_ids = from_list(lambda x: x, obj.get("category_ids")) 90 | tag_ids = from_list(lambda x: x, obj.get("tag_ids")) 91 | group_ids = from_list(lambda x: x, obj.get("group_ids")) 92 | return GroupedSearchResult(more_posts, more_users, more_categories, term, search_log_id, more_full_page_results, can_create_topic, error, post_ids, user_ids, category_ids, tag_ids, group_ids) 93 | 94 | def to_dict(self) -> dict: 95 | result: dict = {} 96 | result["more_posts"] = from_none(self.more_posts) 97 | result["more_users"] = from_none(self.more_users) 98 | result["more_categories"] = from_none(self.more_categories) 99 | result["term"] = from_str(self.term) 100 | result["search_log_id"] = from_int(self.search_log_id) 101 | result["more_full_page_results"] = from_bool(self.more_full_page_results) 102 | result["can_create_topic"] = from_bool(self.can_create_topic) 103 | result["error"] = from_none(self.error) 104 | result["post_ids"] = from_list(from_int, self.post_ids) 105 | result["user_ids"] = from_list(lambda x: x, self.user_ids) 106 | result["category_ids"] = from_list(lambda x: x, self.category_ids) 107 | result["tag_ids"] = from_list(lambda x: x, self.tag_ids) 108 | result["group_ids"] = from_list(lambda x: x, self.group_ids) 109 | return result 110 | 111 | 112 | @dataclass 113 | class Post: 114 | id: int 115 | name: str 116 | username: str 117 | avatar_template: str 118 | created_at: datetime 119 | like_count: int 120 | blurb: str 121 | post_number: int 122 | topic_id: int 123 | 124 | def is_full(self) -> bool: 125 | return not self.blurb.endswith('...') 126 | 127 | @staticmethod 128 | def from_dict(obj: Any) -> 'Post': 129 | assert isinstance(obj, dict) 130 | id = from_int(obj.get("id")) 131 | name = from_str(obj.get("name")) 132 | username = from_str(obj.get("username")) 133 | avatar_template = from_str(obj.get("avatar_template")) 134 | created_at = from_datetime(obj.get("created_at")) 135 | like_count = from_int(obj.get("like_count")) 136 | blurb = from_str(obj.get("blurb")) 137 | post_number = from_int(obj.get("post_number")) 138 | topic_id = from_int(obj.get("topic_id")) 139 | return Post(id, name, username, avatar_template, created_at, like_count, blurb, post_number, topic_id) 140 | 141 | def to_dict(self) -> dict: 142 | result: dict = {} 143 | result["id"] = from_int(self.id) 144 | result["name"] = from_str(self.name) 145 | result["username"] = from_str(self.username) 146 | result["avatar_template"] = from_str(self.avatar_template) 147 | result["created_at"] = self.created_at.isoformat() 148 | result["like_count"] = from_int(self.like_count) 149 | result["blurb"] = from_str(self.blurb) 150 | result["post_number"] = from_int(self.post_number) 151 | result["topic_id"] = from_int(self.topic_id) 152 | return result 153 | 154 | 155 | @dataclass 156 | class Topic: 157 | id: int 158 | title: str 159 | fancy_title: str 160 | slug: str 161 | posts_count: int 162 | reply_count: int 163 | highest_post_number: int 164 | created_at: datetime 165 | last_posted_at: datetime 166 | bumped: bool 167 | bumped_at: datetime 168 | archetype: str 169 | unseen: bool 170 | pinned: bool 171 | unpinned: None 172 | visible: bool 173 | closed: bool 174 | archived: bool 175 | bookmarked: bool 176 | liked: bool 177 | tags: List[str] 178 | tags_descriptions: Any 179 | category_id: int 180 | has_accepted_answer: bool 181 | last_read_post_number: Optional[int] 182 | unread: Optional[int] 183 | new_posts: Optional[int] 184 | unread_posts: Optional[int] 185 | notification_level: Optional[int] 186 | 187 | @staticmethod 188 | def from_dict(obj: Any) -> 'Topic': 189 | assert isinstance(obj, dict) 190 | id = from_int(obj.get("id")) 191 | title = from_str(obj.get("title")) 192 | fancy_title = from_str(obj.get("fancy_title")) 193 | slug = from_str(obj.get("slug")) 194 | posts_count = from_int(obj.get("posts_count")) 195 | reply_count = from_int(obj.get("reply_count")) 196 | highest_post_number = from_int(obj.get("highest_post_number")) 197 | created_at = from_datetime(obj.get("created_at")) 198 | last_posted_at = from_datetime(obj.get("last_posted_at")) 199 | bumped = from_bool(obj.get("bumped")) 200 | bumped_at = from_datetime(obj.get("bumped_at")) 201 | archetype = from_str(obj.get("archetype")) 202 | unseen = from_bool(obj.get("unseen")) 203 | pinned = from_bool(obj.get("pinned")) 204 | unpinned = from_none(obj.get("unpinned")) 205 | visible = from_bool(obj.get("visible")) 206 | closed = from_bool(obj.get("closed")) 207 | archived = from_bool(obj.get("archived")) 208 | bookmarked = from_bool(obj.get("bookmarked")) 209 | liked = from_bool(obj.get("liked")) 210 | tags = from_list(from_str, obj.get("tags")) 211 | tags_descriptions = obj.get("tags_descriptions") 212 | category_id = from_int(obj.get("category_id")) 213 | has_accepted_answer = from_bool(obj.get("has_accepted_answer")) 214 | last_read_post_number = from_int(obj.get("last_read_post_number")) 215 | unread = from_int( obj.get("unread")) 216 | new_posts = from_int( obj.get("new_posts")) 217 | unread_posts = from_int(obj.get("unread_posts")) 218 | notification_level = from_int(obj.get("notification_level")) 219 | return Topic(id, title, fancy_title, slug, posts_count, reply_count, highest_post_number, created_at, last_posted_at, bumped, bumped_at, archetype, unseen, pinned, unpinned, visible, closed, archived, bookmarked, liked, tags, tags_descriptions, category_id, has_accepted_answer, last_read_post_number, unread, new_posts, unread_posts, notification_level) 220 | 221 | def to_dict(self) -> dict: 222 | result: dict = {} 223 | result["id"] = from_int(self.id) 224 | result["title"] = from_str(self.title) 225 | result["fancy_title"] = from_str(self.fancy_title) 226 | result["slug"] = from_str(self.slug) 227 | result["posts_count"] = from_int(self.posts_count) 228 | result["reply_count"] = from_int(self.reply_count) 229 | result["highest_post_number"] = from_int(self.highest_post_number) 230 | result["created_at"] = self.created_at.isoformat() 231 | result["last_posted_at"] = self.last_posted_at.isoformat() 232 | result["bumped"] = from_bool(self.bumped) 233 | result["bumped_at"] = self.bumped_at.isoformat() 234 | result["archetype"] = from_str(self.archetype) 235 | result["pinned"] = from_bool(self.pinned) 236 | result["unpinned"] = from_none(self.unpinned) 237 | result["visible"] = from_bool(self.visible) 238 | result["closed"] = from_bool(self.closed) 239 | result["archived"] = from_bool(self.archived) 240 | result["bookmarked"] = from_bool(self.bookmarked) 241 | result["liked"] = from_bool(self.liked) 242 | result["tags"] = from_list(from_str, self.tags) 243 | result["tags_descriptions"] = self.tags_descriptions, 244 | result["category_id"] = from_int(self.category_id) 245 | result["has_accepted_answer"] = from_bool(self.has_accepted_answer) 246 | result["last_read_post_number"] = from_int(self.last_read_post_number) 247 | result["unread"] = from_int( self.unread) 248 | result["new_posts"] = from_int( self.new_posts) 249 | result["unread_posts"] = from_int(self.unread_posts) 250 | result["notification_level"] = from_int(self.notification_level) 251 | return result 252 | 253 | 254 | @dataclass 255 | class SearchResult: 256 | posts: List[Post] 257 | topics: List[Topic] 258 | users: List[Any] 259 | categories: List[Any] 260 | tags: List[Any] 261 | groups: List[Any] 262 | grouped_search_result: GroupedSearchResult 263 | 264 | @staticmethod 265 | def from_dict(obj: Any) -> 'SearchResult': 266 | assert isinstance(obj, dict) 267 | posts = from_list(Post.from_dict, obj.get("posts")) 268 | topics = from_list(Topic.from_dict, obj.get("topics")) 269 | users = from_list(lambda x: x, obj.get("users")) 270 | categories = from_list(lambda x: x, obj.get("categories")) 271 | tags = from_list(lambda x: x, obj.get("tags")) 272 | groups = from_list(lambda x: x, obj.get("groups")) 273 | grouped_search_result = GroupedSearchResult.from_dict( 274 | obj.get("grouped_search_result")) 275 | return SearchResult(posts, topics, users, categories, tags, groups, grouped_search_result) 276 | 277 | def to_dict(self) -> dict: 278 | result: dict = {} 279 | result["posts"] = from_list(lambda x: to_class(Post, x), self.posts) 280 | result["topics"] = from_list(Topic.from_dict, self.topics) 281 | result["users"] = from_list(lambda x: x, self.users) 282 | result["categories"] = from_list(lambda x: x, self.categories) 283 | result["tags"] = from_list(lambda x: x, self.tags) 284 | result["groups"] = from_list(lambda x: x, self.groups) 285 | result["grouped_search_result"] = to_class( 286 | GroupedSearchResult, self.grouped_search_result) 287 | return result 288 | 289 | 290 | def search_result_from_dict(s: Any) -> SearchResult: 291 | return SearchResult.from_dict(s) 292 | 293 | 294 | def search_result_to_dict(x: SearchResult) -> Any: 295 | return to_class(SearchResult, x) 296 | -------------------------------------------------------------------------------- /models/post.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict, Optional 3 | 4 | import requests 5 | from models.common import * 6 | from dataclasses import dataclass 7 | from bs4 import BeautifulSoup 8 | 9 | 10 | @dataclass 11 | class PostImage: 12 | src: str 13 | src_set: Dict[str, str] 14 | height: int 15 | width: int 16 | 17 | 18 | @dataclass 19 | class ActionsSummary: 20 | id: int 21 | can_act: bool 22 | count: Optional[int] 23 | 24 | @staticmethod 25 | def from_dict(obj: Any) -> 'ActionsSummary': 26 | assert isinstance(obj, dict) 27 | id = from_int(obj.get("id")) 28 | can_act = from_bool(obj.get("can_act")) 29 | count = from_int(obj.get("count")) 30 | return ActionsSummary(id, can_act, count) 31 | 32 | def to_dict(self) -> dict: 33 | result: dict = {} 34 | result["id"] = from_int(self.id) 35 | result["can_act"] = from_bool(self.can_act) 36 | result["count"] = from_int(self.count) 37 | return result 38 | 39 | 40 | @dataclass 41 | class ReplyToUser: 42 | username: str 43 | name: str 44 | avatar_template: str 45 | 46 | @staticmethod 47 | def from_dict(obj: Any) -> 'ReplyToUser': 48 | assert isinstance(obj, dict) 49 | username = from_str(obj.get("username")) 50 | name = from_str(obj.get("name")) 51 | avatar_template = from_str(obj.get("avatar_template")) 52 | return ReplyToUser(username, name, avatar_template) 53 | 54 | def to_dict(self) -> dict: 55 | result: dict = {} 56 | result["username"] = from_str(self.username) 57 | result["name"] = from_str(self.name) 58 | result["avatar_template"] = from_str(self.avatar_template) 59 | return result 60 | 61 | 62 | @dataclass 63 | class Retort: 64 | post_id: int 65 | usernames: List[str] 66 | emoji: str 67 | 68 | @staticmethod 69 | def from_dict(obj: Any) -> 'Retort': 70 | assert isinstance(obj, dict) 71 | post_id = from_int(obj.get("post_id")) 72 | usernames = from_list(from_str, obj.get("usernames")) 73 | emoji = from_str(obj.get("emoji")) 74 | return Retort(post_id, usernames, emoji) 75 | 76 | def to_dict(self) -> dict: 77 | result: dict = {} 78 | result["post_id"] = from_int(self.post_id) 79 | result["usernames"] = from_list(from_str, self.usernames) 80 | result["emoji"] = from_str(self.emoji) 81 | return result 82 | 83 | 84 | @dataclass 85 | class Post: 86 | id: int 87 | name: str 88 | username: str 89 | avatar_template: str 90 | created_at: datetime 91 | cooked: str 92 | post_number: int 93 | post_type: int 94 | updated_at: datetime 95 | reply_count: int 96 | reply_to_post_number: Optional[int] 97 | quote_count: int 98 | incoming_link_count: int 99 | reads: int 100 | readers_count: int 101 | score: int 102 | yours: bool 103 | topic_id: int 104 | topic_slug: str 105 | display_username: str 106 | primary_group_name: None 107 | flair_name: None 108 | flair_url: None 109 | flair_bg_color: None 110 | flair_color: None 111 | flair_group_id: None 112 | version: int 113 | can_edit: bool 114 | can_delete: bool 115 | can_recover: bool 116 | can_see_hidden_post: bool 117 | can_wiki: bool 118 | user_title: str 119 | reply_to_user: Optional[ReplyToUser] 120 | bookmarked: bool 121 | raw: str 122 | actions_summary: List[ActionsSummary] 123 | moderator: bool 124 | admin: bool 125 | staff: bool 126 | user_id: int 127 | hidden: bool 128 | trust_level: int 129 | deleted_at: None 130 | user_deleted: bool 131 | edit_reason: None 132 | can_view_edit_history: bool 133 | wiki: bool 134 | user_cakedate: datetime 135 | can_accept_answer: bool 136 | can_unaccept_answer: bool 137 | accepted_answer: bool 138 | topic_accepted_answer: bool 139 | retorts: List[Retort] 140 | 141 | @staticmethod 142 | def from_dict(obj: Any) -> 'Post': 143 | assert isinstance(obj, dict) 144 | id = from_int(obj.get("id")) 145 | name = from_str(obj.get("name")) 146 | username = from_str(obj.get("username")) 147 | avatar_template = from_str(obj.get("avatar_template")) 148 | created_at = from_datetime(obj.get("created_at")) 149 | cooked = from_str(obj.get("cooked")) 150 | post_number = from_int(obj.get("post_number")) 151 | post_type = from_int(obj.get("post_type")) 152 | updated_at = from_datetime(obj.get("updated_at")) 153 | reply_count = from_int(obj.get("reply_count")) 154 | reply_to_post_number = from_int(obj.get("reply_to_post_number")) 155 | quote_count = from_int(obj.get("quote_count")) 156 | incoming_link_count = from_int(obj.get("incoming_link_count")) 157 | reads = from_int(obj.get("reads")) 158 | readers_count = from_int(obj.get("readers_count")) 159 | score = from_float(obj.get("score")) 160 | yours = from_bool(obj.get("yours")) 161 | topic_id = from_int(obj.get("topic_id")) 162 | topic_slug = from_str(obj.get("topic_slug")) 163 | display_username = from_str(obj.get("display_username")) 164 | primary_group_name = from_none(obj.get("primary_group_name")) 165 | flair_name = from_none(obj.get("flair_name")) 166 | flair_url = from_none(obj.get("flair_url")) 167 | flair_bg_color = from_none(obj.get("flair_bg_color")) 168 | flair_color = from_none(obj.get("flair_color")) 169 | flair_group_id = from_none(obj.get("flair_group_id")) 170 | version = from_int(obj.get("version")) 171 | can_edit = from_bool(obj.get("can_edit")) 172 | can_delete = from_bool(obj.get("can_delete")) 173 | can_recover = from_bool(obj.get("can_recover")) 174 | can_see_hidden_post = from_bool(obj.get("can_see_hidden_post")) 175 | can_wiki = from_bool(obj.get("can_wiki")) 176 | user_title = from_str(obj.get("user_title")) 177 | reply_to_user = from_union( 178 | [ReplyToUser.from_dict, from_none], obj.get("reply_to_user")) 179 | bookmarked = from_bool(obj.get("bookmarked")) 180 | raw = from_str(obj.get("raw")) 181 | actions_summary = from_list( 182 | ActionsSummary.from_dict, obj.get("actions_summary")) 183 | moderator = from_bool(obj.get("moderator")) 184 | admin = from_bool(obj.get("admin")) 185 | staff = from_bool(obj.get("staff")) 186 | user_id = from_int(obj.get("user_id")) 187 | hidden = from_bool(obj.get("hidden")) 188 | trust_level = from_int(obj.get("trust_level")) 189 | deleted_at = from_none(obj.get("deleted_at")) 190 | user_deleted = from_bool(obj.get("user_deleted")) 191 | edit_reason = from_none(obj.get("edit_reason")) 192 | can_view_edit_history = from_bool(obj.get("can_view_edit_history")) 193 | wiki = from_bool(obj.get("wiki")) 194 | user_cakedate = from_datetime(obj.get("user_cakedate")) 195 | can_accept_answer = from_bool(obj.get("can_accept_answer")) 196 | can_unaccept_answer = from_bool(obj.get("can_unaccept_answer")) 197 | accepted_answer = from_bool(obj.get("accepted_answer")) 198 | topic_accepted_answer = from_bool(obj.get("topic_accepted_answer")) 199 | retorts = from_list(Retort.from_dict, obj.get("retorts")) 200 | return Post(id, name, username, avatar_template, created_at, cooked, post_number, post_type, updated_at, reply_count, reply_to_post_number, quote_count, incoming_link_count, reads, readers_count, score, yours, topic_id, topic_slug, display_username, primary_group_name, flair_name, flair_url, flair_bg_color, flair_color, flair_group_id, version, can_edit, can_delete, can_recover, can_see_hidden_post, can_wiki, user_title, reply_to_user, bookmarked, raw, actions_summary, moderator, admin, staff, user_id, hidden, trust_level, deleted_at, user_deleted, edit_reason, can_view_edit_history, wiki, user_cakedate, can_accept_answer, can_unaccept_answer, accepted_answer, topic_accepted_answer, retorts) 201 | 202 | def to_dict(self) -> dict: 203 | result: dict = {} 204 | result["id"] = from_int(self.id) 205 | result["name"] = from_str(self.name) 206 | result["username"] = from_str(self.username) 207 | result["avatar_template"] = from_str(self.avatar_template) 208 | result["created_at"] = self.created_at.isoformat() 209 | result["cooked"] = from_str(self.cooked) 210 | result["post_number"] = from_int(self.post_number) 211 | result["post_type"] = from_int(self.post_type) 212 | result["updated_at"] = self.updated_at.isoformat() 213 | result["reply_count"] = from_int(self.reply_count) 214 | result["reply_to_post_number"] = from_int(self.reply_to_post_number) 215 | result["quote_count"] = from_int(self.quote_count) 216 | result["incoming_link_count"] = from_int(self.incoming_link_count) 217 | result["reads"] = from_int(self.reads) 218 | result["readers_count"] = from_int(self.readers_count) 219 | result["score"] = from_float(self.score) 220 | result["yours"] = from_bool(self.yours) 221 | result["topic_id"] = from_int(self.topic_id) 222 | result["topic_slug"] = from_str(self.topic_slug) 223 | result["display_username"] = from_str(self.display_username) 224 | result["primary_group_name"] = from_none(self.primary_group_name) 225 | result["flair_name"] = from_none(self.flair_name) 226 | result["flair_url"] = from_none(self.flair_url) 227 | result["flair_bg_color"] = from_none(self.flair_bg_color) 228 | result["flair_color"] = from_none(self.flair_color) 229 | result["flair_group_id"] = from_none(self.flair_group_id) 230 | result["version"] = from_int(self.version) 231 | result["can_edit"] = from_bool(self.can_edit) 232 | result["can_delete"] = from_bool(self.can_delete) 233 | result["can_recover"] = from_bool(self.can_recover) 234 | result["can_see_hidden_post"] = from_bool(self.can_see_hidden_post) 235 | result["can_wiki"] = from_bool(self.can_wiki) 236 | result["user_title"] = from_str(self.user_title) 237 | result["reply_to_user"] = to_class(ReplyToUser, self.reply_to_user) 238 | result["bookmarked"] = from_bool(self.bookmarked) 239 | result["raw"] = from_str(self.raw) 240 | result["actions_summary"] = from_list( 241 | lambda x: to_class(ActionsSummary, x), self.actions_summary) 242 | result["moderator"] = from_bool(self.moderator) 243 | result["admin"] = from_bool(self.admin) 244 | result["staff"] = from_bool(self.staff) 245 | result["user_id"] = from_int(self.user_id) 246 | result["hidden"] = from_bool(self.hidden) 247 | result["trust_level"] = from_int(self.trust_level) 248 | result["deleted_at"] = from_none(self.deleted_at) 249 | result["user_deleted"] = from_bool(self.user_deleted) 250 | result["edit_reason"] = from_none(self.edit_reason) 251 | result["can_view_edit_history"] = from_bool(self.can_view_edit_history) 252 | result["wiki"] = from_bool(self.wiki) 253 | result["user_cakedate"] = self.user_cakedate.isoformat() 254 | result["can_accept_answer"] = from_bool(self.can_accept_answer) 255 | result["can_unaccept_answer"] = from_bool(self.can_unaccept_answer) 256 | result["accepted_answer"] = from_bool(self.accepted_answer) 257 | result["topic_accepted_answer"] = from_bool(self.topic_accepted_answer) 258 | result["retorts"] = from_list(Retort.from_dict, self.retorts) 259 | return result 260 | 261 | def get_imgs(self) -> List[PostImage]: 262 | soup = BeautifulSoup(self.cooked, 'html.parser') 263 | imgs = soup.find_all("img", attrs={"src": True}) 264 | ret = [] 265 | for img in imgs: 266 | src = img.get("src") 267 | src_set_attr: str = img.get("srcset") 268 | src_set_parts = src_set_attr.split(',') 269 | src_set = {} 270 | for part in src_set_parts: 271 | part = part.strip() 272 | pair = part.split(' ') 273 | if len(pair) == 2: 274 | url, times = pair[0], pair[1] 275 | src_set[times] = url 276 | height_attr: str = img.get("height") 277 | width_attr: str = img.get("width") 278 | height = width = 0 279 | if height_attr.isdigit(): 280 | height = int(height_attr) 281 | if width_attr.isdigit(): 282 | width = int(width_attr) 283 | ret.append(PostImage(src, src_set, height, width)) 284 | 285 | return ret 286 | 287 | 288 | def post_from_dict(s: Any) -> Post: 289 | return Post.from_dict(s) 290 | 291 | 292 | def post_to_dict(x: Post) -> Any: 293 | return to_class(Post, x) 294 | -------------------------------------------------------------------------------- /models/topic_post.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from models.common import * 3 | from dataclasses import dataclass 4 | 5 | 6 | @dataclass 7 | class ActionsSummary: 8 | id: int 9 | can_act: bool 10 | count: int 11 | 12 | @staticmethod 13 | def from_dict(obj: Any) -> 'ActionsSummary': 14 | assert isinstance(obj, dict) 15 | id = from_int(obj.get("id")) 16 | can_act = from_bool(obj.get("can_act")) 17 | count = from_int(obj.get("count")) 18 | return ActionsSummary(id, can_act, count) 19 | 20 | def to_dict(self) -> dict: 21 | result: dict = {} 22 | result["id"] = from_int(self.id) 23 | result["can_act"] = from_bool(self.can_act) 24 | result["count"] = from_int(self.count) 25 | return result 26 | 27 | 28 | @dataclass 29 | class LinkCount: 30 | url: str 31 | internal: bool 32 | reflection: bool 33 | title: str 34 | clicks: int 35 | 36 | @staticmethod 37 | def from_dict(obj: Any) -> 'LinkCount': 38 | assert isinstance(obj, dict) 39 | url = from_str(obj.get("url")) 40 | internal = from_bool(obj.get("internal")) 41 | reflection = from_bool(obj.get("reflection")) 42 | title = from_str(obj.get("title")) 43 | clicks = from_int(obj.get("clicks")) 44 | return LinkCount(url, internal, reflection, title, clicks) 45 | 46 | def to_dict(self) -> dict: 47 | result: dict = {} 48 | result["url"] = from_str(self.url) 49 | result["internal"] = from_bool(self.internal) 50 | result["reflection"] = from_bool(self.reflection) 51 | result["title"] = from_str(self.title) 52 | result["clicks"] = from_int(self.clicks) 53 | return result 54 | 55 | 56 | @dataclass 57 | class Option: 58 | id: str 59 | html: str 60 | votes: int 61 | 62 | @staticmethod 63 | def from_dict(obj: Any) -> 'Option': 64 | assert isinstance(obj, dict) 65 | id = from_str(obj.get("id")) 66 | html = from_str(obj.get("html")) 67 | votes = from_int(obj.get("votes")) 68 | return Option(id, html, votes) 69 | 70 | def to_dict(self) -> dict: 71 | result: dict = {} 72 | result["id"] = from_str(self.id) 73 | result["html"] = from_str(self.html) 74 | result["votes"] = from_int(self.votes) 75 | return result 76 | 77 | 78 | @dataclass 79 | class Poll: 80 | name: str 81 | type: str 82 | status: str 83 | results: str 84 | options: List[Option] 85 | voters: int 86 | chart_type: str 87 | title: str 88 | 89 | @staticmethod 90 | def from_dict(obj: Any) -> 'Poll': 91 | assert isinstance(obj, dict) 92 | name = from_str(obj.get("name")) 93 | type = from_str(obj.get("type")) 94 | status = from_str(obj.get("status")) 95 | results = from_str(obj.get("results")) 96 | options = from_list(Option.from_dict, obj.get("options")) 97 | voters = from_int(obj.get("voters")) 98 | chart_type = from_str(obj.get("chart_type")) 99 | title = from_str(obj.get("title")) 100 | return Poll(name, type, status, results, options, voters, chart_type, title) 101 | 102 | def to_dict(self) -> dict: 103 | result: dict = {} 104 | result["name"] = from_str(self.name) 105 | result["type"] = from_str(self.type) 106 | result["status"] = from_str(self.status) 107 | result["results"] = from_str(self.results) 108 | result["options"] = from_list( 109 | lambda x: to_class(Option, x), self.options) 110 | result["voters"] = from_int(self.voters) 111 | result["chart_type"] = from_str(self.chart_type) 112 | result["title"] = from_str(self.title) 113 | return result 114 | 115 | 116 | @dataclass 117 | class PollsVotes: 118 | poll: List[str] 119 | 120 | @staticmethod 121 | def from_dict(obj: Any) -> 'PollsVotes': 122 | assert isinstance(obj, dict) 123 | poll = from_list(from_str, obj.get("poll")) 124 | return PollsVotes(poll) 125 | 126 | def to_dict(self) -> dict: 127 | result: dict = {} 128 | result["poll"] = from_list(from_str, self.poll) 129 | return result 130 | 131 | 132 | @dataclass 133 | class ReplyToUser: 134 | username: str 135 | name: str 136 | avatar_template: str 137 | 138 | @staticmethod 139 | def from_dict(obj: Any) -> 'ReplyToUser': 140 | assert isinstance(obj, dict) 141 | username = from_str(obj.get("username")) 142 | name = from_str(obj.get("name")) 143 | avatar_template = from_str(obj.get("avatar_template")) 144 | return ReplyToUser(username, name, avatar_template) 145 | 146 | def to_dict(self) -> dict: 147 | result: dict = {} 148 | result["username"] = from_str(self.username) 149 | result["name"] = from_str(self.name) 150 | result["avatar_template"] = from_str(self.avatar_template) 151 | return result 152 | 153 | 154 | @dataclass 155 | class Retort: 156 | post_id: int 157 | usernames: List[str] 158 | emoji: str 159 | 160 | @staticmethod 161 | def from_dict(obj: Any) -> 'Retort': 162 | assert isinstance(obj, dict) 163 | post_id = from_int(obj.get("post_id")) 164 | usernames = from_list(from_str, obj.get("usernames")) 165 | emoji = from_str(obj.get("emoji")) 166 | return Retort(post_id, usernames, emoji) 167 | 168 | def to_dict(self) -> dict: 169 | result: dict = {} 170 | result["post_id"] = from_int(self.post_id) 171 | result["usernames"] = from_list(from_str, self.usernames) 172 | result["emoji"] = from_str(self.emoji) 173 | return result 174 | 175 | 176 | @dataclass 177 | class Post: 178 | id: int 179 | name: str 180 | username: str 181 | avatar_template: str 182 | created_at: datetime 183 | cooked: str 184 | post_number: int 185 | post_type: int 186 | updated_at: datetime 187 | reply_count: int 188 | quote_count: int 189 | incoming_link_count: int 190 | reads: int 191 | readers_count: int 192 | score: float 193 | yours: bool 194 | topic_id: int 195 | topic_slug: str 196 | display_username: str 197 | version: int 198 | can_edit: bool 199 | can_delete: bool 200 | can_recover: bool 201 | can_see_hidden_post: bool 202 | can_wiki: bool 203 | read: bool 204 | bookmarked: bool 205 | actions_summary: List[ActionsSummary] 206 | moderator: bool 207 | admin: bool 208 | staff: bool 209 | user_id: int 210 | hidden: bool 211 | trust_level: int 212 | deleted_at: str 213 | user_deleted: bool 214 | edit_reason: str 215 | can_view_edit_history: bool 216 | wiki: bool 217 | user_cakedate: datetime 218 | can_accept_answer: bool 219 | can_unaccept_answer: bool 220 | accepted_answer: bool 221 | topic_accepted_answer: bool 222 | retorts: List[Retort] 223 | reply_to_post_number: int 224 | primary_group_name: str 225 | flair_name: str 226 | flair_url: str 227 | flair_bg_color: str 228 | flair_color: str 229 | flair_group_id: int 230 | link_counts: List[LinkCount] 231 | user_title: str 232 | title_is_group: bool 233 | polls: List[Poll] 234 | polls_votes: Optional[PollsVotes] = None 235 | reply_to_user: Optional[ReplyToUser] = None 236 | user_birthdate: Optional[datetime] = None 237 | 238 | @staticmethod 239 | def from_dict(obj: Any) -> 'Post': 240 | assert isinstance(obj, dict) 241 | id = from_int(obj.get("id")) 242 | name = from_str(obj.get("name")) 243 | username = from_str(obj.get("username")) 244 | avatar_template = from_str(obj.get("avatar_template")) 245 | created_at = from_datetime(obj.get("created_at")) 246 | cooked = from_str(obj.get("cooked")) 247 | post_number = from_int(obj.get("post_number")) 248 | post_type = from_int(obj.get("post_type")) 249 | updated_at = from_datetime(obj.get("updated_at")) 250 | reply_count = from_int(obj.get("reply_count")) 251 | quote_count = from_int(obj.get("quote_count")) 252 | incoming_link_count = from_int(obj.get("incoming_link_count")) 253 | reads = from_int(obj.get("reads")) 254 | readers_count = from_int(obj.get("readers_count")) 255 | score = from_float(obj.get("score")) 256 | yours = from_bool(obj.get("yours")) 257 | topic_id = from_int(obj.get("topic_id")) 258 | topic_slug = from_str(obj.get("topic_slug")) 259 | display_username = from_str(obj.get("display_username")) 260 | version = from_int(obj.get("version")) 261 | can_edit = from_bool(obj.get("can_edit")) 262 | can_delete = from_bool(obj.get("can_delete")) 263 | can_recover = from_bool(obj.get("can_recover")) 264 | can_see_hidden_post = from_bool(obj.get("can_see_hidden_post")) 265 | can_wiki = from_bool(obj.get("can_wiki")) 266 | read = from_bool(obj.get("read")) 267 | bookmarked = from_bool(obj.get("bookmarked")) 268 | actions_summary = from_list( 269 | ActionsSummary.from_dict, obj.get("actions_summary")) 270 | moderator = from_bool(obj.get("moderator")) 271 | admin = from_bool(obj.get("admin")) 272 | staff = from_bool(obj.get("staff")) 273 | user_id = from_int(obj.get("user_id")) 274 | hidden = from_bool(obj.get("hidden")) 275 | trust_level = from_int(obj.get("trust_level")) 276 | deleted_at = from_str(obj.get("deleted_at")) 277 | user_deleted = from_bool(obj.get("user_deleted")) 278 | edit_reason = from_str(obj.get("edit_reason")) 279 | can_view_edit_history = from_bool(obj.get("can_view_edit_history")) 280 | wiki = from_bool(obj.get("wiki")) 281 | user_cakedate = from_datetime(obj.get("user_cakedate")) 282 | can_accept_answer = from_bool(obj.get("can_accept_answer")) 283 | can_unaccept_answer = from_bool(obj.get("can_unaccept_answer")) 284 | accepted_answer = from_bool(obj.get("accepted_answer")) 285 | topic_accepted_answer = from_bool(obj.get("topic_accepted_answer")) 286 | retorts = from_list(Retort.from_dict, obj.get("retorts")) 287 | reply_to_post_number = from_int(obj.get("reply_to_post_number")) 288 | primary_group_name = from_str(obj.get("primary_group_name")) 289 | flair_name = from_str(obj.get("flair_name")) 290 | flair_url = from_str(obj.get("flair_url")) 291 | flair_bg_color = from_str(obj.get("flair_bg_color")) 292 | flair_color = from_str(obj.get("flair_color")) 293 | flair_group_id = from_int(obj.get("flair_group_id")) 294 | link_counts = from_list(LinkCount.from_dict, obj.get("link_counts")) 295 | user_title = from_str(obj.get("user_title")) 296 | title_is_group = from_bool(obj.get("title_is_group")) 297 | polls = from_list(Poll.from_dict, obj.get("polls")) 298 | polls_votes = from_union( 299 | [PollsVotes.from_dict, from_none], obj.get("polls_votes")) 300 | reply_to_user = from_union( 301 | [ReplyToUser.from_dict, from_none], obj.get("reply_to_user")) 302 | user_birthdate = from_union( 303 | [from_datetime, from_none], obj.get("user_birthdate")) 304 | return Post(id, name, username, avatar_template, created_at, cooked, post_number, post_type, updated_at, reply_count, quote_count, incoming_link_count, reads, readers_count, score, yours, topic_id, topic_slug, display_username, version, can_edit, can_delete, can_recover, can_see_hidden_post, can_wiki, read, bookmarked, actions_summary, moderator, admin, staff, user_id, hidden, trust_level, deleted_at, user_deleted, edit_reason, can_view_edit_history, wiki, user_cakedate, can_accept_answer, can_unaccept_answer, accepted_answer, topic_accepted_answer, retorts, reply_to_post_number, primary_group_name, flair_name, flair_url, flair_bg_color, flair_color, flair_group_id, link_counts, user_title, title_is_group, polls, polls_votes, reply_to_user, user_birthdate) 305 | 306 | def to_dict(self) -> dict: 307 | result: dict = {} 308 | result["id"] = from_int(self.id) 309 | result["name"] = from_str(self.name) 310 | result["username"] = from_str(self.username) 311 | result["avatar_template"] = from_str(self.avatar_template) 312 | result["created_at"] = self.created_at.isoformat() 313 | result["cooked"] = from_str(self.cooked) 314 | result["post_number"] = from_int(self.post_number) 315 | result["post_type"] = from_int(self.post_type) 316 | result["updated_at"] = self.updated_at.isoformat() 317 | result["reply_count"] = from_int(self.reply_count) 318 | result["quote_count"] = from_int(self.quote_count) 319 | result["incoming_link_count"] = from_int(self.incoming_link_count) 320 | result["reads"] = from_int(self.reads) 321 | result["readers_count"] = from_int(self.readers_count) 322 | result["score"] = from_float(self.score) 323 | result["yours"] = from_bool(self.yours) 324 | result["topic_id"] = from_int(self.topic_id) 325 | result["topic_slug"] = from_str(self.topic_slug) 326 | result["display_username"] = from_str(self.display_username) 327 | result["version"] = from_int(self.version) 328 | result["can_edit"] = from_bool(self.can_edit) 329 | result["can_delete"] = from_bool(self.can_delete) 330 | result["can_recover"] = from_bool(self.can_recover) 331 | result["can_see_hidden_post"] = from_bool(self.can_see_hidden_post) 332 | result["can_wiki"] = from_bool(self.can_wiki) 333 | result["read"] = from_bool(self.read) 334 | result["bookmarked"] = from_bool(self.bookmarked) 335 | result["actions_summary"] = from_list( 336 | lambda x: to_class(ActionsSummary, x), self.actions_summary) 337 | result["moderator"] = from_bool(self.moderator) 338 | result["admin"] = from_bool(self.admin) 339 | result["staff"] = from_bool(self.staff) 340 | result["user_id"] = from_int(self.user_id) 341 | result["hidden"] = from_bool(self.hidden) 342 | result["trust_level"] = from_int(self.trust_level) 343 | result["deleted_at"] = from_str(self.deleted_at) 344 | result["user_deleted"] = from_bool(self.user_deleted) 345 | result["edit_reason"] = from_str(self.edit_reason) 346 | result["can_view_edit_history"] = from_bool(self.can_view_edit_history) 347 | result["wiki"] = from_bool(self.wiki) 348 | result["user_cakedate"] = self.user_cakedate.isoformat() 349 | result["can_accept_answer"] = from_bool(self.can_accept_answer) 350 | result["can_unaccept_answer"] = from_bool(self.can_unaccept_answer) 351 | result["accepted_answer"] = from_bool(self.accepted_answer) 352 | result["topic_accepted_answer"] = from_bool(self.topic_accepted_answer) 353 | result["retorts"] = from_list( 354 | lambda x: to_class(Retort, x), self.retorts) 355 | result["reply_to_post_number"] = from_int(self.reply_to_post_number) 356 | result["primary_group_name"] = from_str(self.primary_group_name) 357 | result["flair_name"] = from_str(self.flair_name) 358 | result["flair_url"] = from_str(self.flair_url) 359 | result["flair_bg_color"] = from_str(self.flair_bg_color) 360 | result["flair_color"] = from_str(self.flair_color) 361 | result["flair_group_id"] = from_int(self.flair_group_id) 362 | result["link_counts"] = from_list( 363 | lambda x: to_class(LinkCount, x), self.link_counts) 364 | result["user_title"] = from_str(self.user_title) 365 | result["title_is_group"] = from_bool(self.title_is_group) 366 | result["polls"] = from_list(lambda x: to_class(Poll, x), self.polls) 367 | result["polls_votes"] = from_union( 368 | [lambda x: to_class(PollsVotes, x), from_none], self.polls_votes) 369 | result["reply_to_user"] = from_union( 370 | [lambda x: to_class(ReplyToUser, x), from_none], self.reply_to_user) 371 | result["user_birthdate"] = from_union( 372 | [lambda x: x.isoformat(), from_none], self.user_birthdate) 373 | return result 374 | 375 | 376 | @dataclass 377 | class PostStream: 378 | posts: List[Post] 379 | 380 | @staticmethod 381 | def from_dict(obj: Any) -> 'PostStream': 382 | assert isinstance(obj, dict) 383 | posts = from_list(Post.from_dict, obj.get("posts")) 384 | return PostStream(posts) 385 | 386 | def to_dict(self) -> dict: 387 | result: dict = {} 388 | result["posts"] = from_list(lambda x: to_class(Post, x), self.posts) 389 | return result 390 | 391 | 392 | @dataclass 393 | class TopicPosts: 394 | post_stream: PostStream 395 | id: int 396 | 397 | @staticmethod 398 | def from_dict(obj: Any) -> 'TopicPosts': 399 | assert isinstance(obj, dict) 400 | post_stream = PostStream.from_dict(obj.get("post_stream")) 401 | id = from_int(obj.get("id")) 402 | return TopicPosts(post_stream, id) 403 | 404 | def to_dict(self) -> dict: 405 | result: dict = {} 406 | result["post_stream"] = to_class(PostStream, self.post_stream) 407 | result["id"] = from_int(self.id) 408 | return result 409 | 410 | 411 | def topic_posts_from_dict(s: Any) -> TopicPosts: 412 | return TopicPosts.from_dict(s) 413 | 414 | 415 | def topic_posts_to_dict(x: TopicPosts) -> Any: 416 | return to_class(TopicPosts, x) 417 | -------------------------------------------------------------------------------- /models/topic.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from models.common import * 3 | from dataclasses import dataclass 4 | 5 | 6 | @dataclass 7 | class TopicActionsSummary: 8 | id: int 9 | count: int 10 | hidden: bool 11 | can_act: bool 12 | 13 | @staticmethod 14 | def from_dict(obj: Any) -> 'TopicActionsSummary': 15 | assert isinstance(obj, dict) 16 | id = from_int(obj.get("id")) 17 | count = from_int(obj.get("count")) 18 | hidden = from_bool(obj.get("hidden")) 19 | can_act = from_bool(obj.get("can_act")) 20 | return TopicActionsSummary(id, count, hidden, can_act) 21 | 22 | def to_dict(self) -> dict: 23 | result: dict = {} 24 | result["id"] = from_int(self.id) 25 | result["count"] = from_int(self.count) 26 | result["hidden"] = from_bool(self.hidden) 27 | result["can_act"] = from_bool(self.can_act) 28 | return result 29 | 30 | 31 | @dataclass 32 | class CreatedBy: 33 | id: int 34 | username: str 35 | name: str 36 | avatar_template: str 37 | 38 | @staticmethod 39 | def from_dict(obj: Any) -> 'CreatedBy': 40 | assert isinstance(obj, dict) 41 | id = from_int(obj.get("id")) 42 | username = from_str(obj.get("username")) 43 | name = from_str(obj.get("name")) 44 | avatar_template = from_str(obj.get("avatar_template")) 45 | return CreatedBy(id, username, name, avatar_template) 46 | 47 | def to_dict(self) -> dict: 48 | result: dict = {} 49 | result["id"] = from_int(self.id) 50 | result["username"] = from_str(self.username) 51 | result["name"] = from_str(self.name) 52 | result["avatar_template"] = from_str(self.avatar_template) 53 | return result 54 | 55 | 56 | @dataclass 57 | class Participant: 58 | id: int 59 | username: str 60 | name: str 61 | avatar_template: str 62 | post_count: int 63 | primary_group_name: str 64 | flair_name: str 65 | flair_url: str 66 | flair_color: str 67 | flair_bg_color: str 68 | flair_group_id: int 69 | admin: bool 70 | moderator: bool 71 | trust_level: int 72 | 73 | @staticmethod 74 | def from_dict(obj: Any) -> 'Participant': 75 | assert isinstance(obj, dict) 76 | id = from_int(obj.get("id")) 77 | username = from_str(obj.get("username")) 78 | name = from_str(obj.get("name")) 79 | avatar_template = from_str(obj.get("avatar_template")) 80 | post_count = from_int(obj.get("post_count")) 81 | primary_group_name = from_str(obj.get("primary_group_name")) 82 | flair_name = from_str(obj.get("flair_name")) 83 | flair_url = from_str(obj.get("flair_url")) 84 | flair_color = from_str(obj.get("flair_color")) 85 | flair_bg_color = from_str(obj.get("flair_bg_color")) 86 | flair_group_id = from_int(obj.get("flair_group_id")) 87 | admin = from_bool(obj.get("admin")) 88 | moderator = from_bool(obj.get("moderator")) 89 | trust_level = from_int(obj.get("trust_level")) 90 | return Participant(id, username, name, avatar_template, post_count, primary_group_name, flair_name, flair_url, flair_color, flair_bg_color, flair_group_id, admin, moderator, trust_level) 91 | 92 | def to_dict(self) -> dict: 93 | result: dict = {} 94 | result["id"] = from_int(self.id) 95 | result["username"] = from_str(self.username) 96 | result["name"] = from_str(self.name) 97 | result["avatar_template"] = from_str(self.avatar_template) 98 | result["post_count"] = from_int(self.post_count) 99 | result["primary_group_name"] = from_str(self.primary_group_name) 100 | result["flair_name"] = from_str(self.flair_name) 101 | result["flair_url"] = from_str(self.flair_url) 102 | result["flair_color"] = from_str(self.flair_color) 103 | result["flair_bg_color"] = from_str(self.flair_bg_color) 104 | result["flair_group_id"] = from_int(self.flair_group_id) 105 | result["admin"] = from_bool(self.admin) 106 | result["moderator"] = from_bool(self.moderator) 107 | result["trust_level"] = from_int(self.trust_level) 108 | return result 109 | 110 | 111 | @dataclass 112 | class Details: 113 | can_edit: bool 114 | notification_level: int 115 | can_move_posts: bool 116 | can_delete: bool 117 | can_remove_allowed_users: bool 118 | can_create_post: bool 119 | can_reply_as_new_topic: bool 120 | can_invite_to: bool 121 | can_invite_via_email: bool 122 | can_flag_topic: bool 123 | can_convert_topic: bool 124 | can_review_topic: bool 125 | can_close_topic: bool 126 | can_archive_topic: bool 127 | can_split_merge_topic: bool 128 | can_edit_staff_notes: bool 129 | can_toggle_topic_visibility: bool 130 | can_pin_unpin_topic: bool 131 | can_moderate_category: bool 132 | can_remove_self_id: int 133 | participants: List[Participant] 134 | created_by: CreatedBy 135 | last_poster: CreatedBy 136 | 137 | @staticmethod 138 | def from_dict(obj: Any) -> 'Details': 139 | assert isinstance(obj, dict) 140 | can_edit = from_bool(obj.get("can_edit")) 141 | notification_level = from_int(obj.get("notification_level")) 142 | can_move_posts = from_bool(obj.get("can_move_posts")) 143 | can_delete = from_bool(obj.get("can_delete")) 144 | can_remove_allowed_users = from_bool( 145 | obj.get("can_remove_allowed_users")) 146 | can_create_post = from_bool(obj.get("can_create_post")) 147 | can_reply_as_new_topic = from_bool(obj.get("can_reply_as_new_topic")) 148 | can_invite_to = from_bool(obj.get("can_invite_to")) 149 | can_invite_via_email = from_bool(obj.get("can_invite_via_email")) 150 | can_flag_topic = from_bool(obj.get("can_flag_topic")) 151 | can_convert_topic = from_bool(obj.get("can_convert_topic")) 152 | can_review_topic = from_bool(obj.get("can_review_topic")) 153 | can_close_topic = from_bool(obj.get("can_close_topic")) 154 | can_archive_topic = from_bool(obj.get("can_archive_topic")) 155 | can_split_merge_topic = from_bool(obj.get("can_split_merge_topic")) 156 | can_edit_staff_notes = from_bool(obj.get("can_edit_staff_notes")) 157 | can_toggle_topic_visibility = from_bool( 158 | obj.get("can_toggle_topic_visibility")) 159 | can_pin_unpin_topic = from_bool(obj.get("can_pin_unpin_topic")) 160 | can_moderate_category = from_bool(obj.get("can_moderate_category")) 161 | can_remove_self_id = from_int(obj.get("can_remove_self_id")) 162 | participants = from_list( 163 | Participant.from_dict, obj.get("participants")) 164 | created_by = CreatedBy.from_dict(obj.get("created_by")) 165 | last_poster = CreatedBy.from_dict(obj.get("last_poster")) 166 | return Details(can_edit, notification_level, can_move_posts, can_delete, can_remove_allowed_users, can_create_post, can_reply_as_new_topic, can_invite_to, can_invite_via_email, can_flag_topic, can_convert_topic, can_review_topic, can_close_topic, can_archive_topic, can_split_merge_topic, can_edit_staff_notes, can_toggle_topic_visibility, can_pin_unpin_topic, can_moderate_category, can_remove_self_id, participants, created_by, last_poster) 167 | 168 | def to_dict(self) -> dict: 169 | result: dict = {} 170 | result["can_edit"] = from_bool(self.can_edit) 171 | result["notification_level"] = from_int(self.notification_level) 172 | result["can_move_posts"] = from_bool(self.can_move_posts) 173 | result["can_delete"] = from_bool(self.can_delete) 174 | result["can_remove_allowed_users"] = from_bool( 175 | self.can_remove_allowed_users) 176 | result["can_create_post"] = from_bool(self.can_create_post) 177 | result["can_reply_as_new_topic"] = from_bool( 178 | self.can_reply_as_new_topic) 179 | result["can_invite_to"] = from_bool(self.can_invite_to) 180 | result["can_invite_via_email"] = from_bool(self.can_invite_via_email) 181 | result["can_flag_topic"] = from_bool(self.can_flag_topic) 182 | result["can_convert_topic"] = from_bool(self.can_convert_topic) 183 | result["can_review_topic"] = from_bool(self.can_review_topic) 184 | result["can_close_topic"] = from_bool(self.can_close_topic) 185 | result["can_archive_topic"] = from_bool(self.can_archive_topic) 186 | result["can_split_merge_topic"] = from_bool(self.can_split_merge_topic) 187 | result["can_edit_staff_notes"] = from_bool(self.can_edit_staff_notes) 188 | result["can_toggle_topic_visibility"] = from_bool( 189 | self.can_toggle_topic_visibility) 190 | result["can_pin_unpin_topic"] = from_bool(self.can_pin_unpin_topic) 191 | result["can_moderate_category"] = from_bool(self.can_moderate_category) 192 | result["can_remove_self_id"] = from_int(self.can_remove_self_id) 193 | result["participants"] = from_list( 194 | lambda x: to_class(Participant, x), self.participants) 195 | result["created_by"] = to_class(CreatedBy, self.created_by) 196 | result["last_poster"] = to_class(CreatedBy, self.last_poster) 197 | return result 198 | 199 | 200 | @dataclass 201 | class PostActionsSummary: 202 | id: int 203 | can_act: bool 204 | 205 | @staticmethod 206 | def from_dict(obj: Any) -> 'PostActionsSummary': 207 | assert isinstance(obj, dict) 208 | id = from_int(obj.get("id")) 209 | can_act = from_bool(obj.get("can_act")) 210 | return PostActionsSummary(id, can_act) 211 | 212 | def to_dict(self) -> dict: 213 | result: dict = {} 214 | result["id"] = from_int(self.id) 215 | result["can_act"] = from_bool(self.can_act) 216 | return result 217 | 218 | 219 | @dataclass 220 | class LinkCount: 221 | url: str 222 | internal: bool 223 | reflection: bool 224 | title: str 225 | clicks: int 226 | 227 | @staticmethod 228 | def from_dict(obj: Any) -> 'LinkCount': 229 | assert isinstance(obj, dict) 230 | url = from_str(obj.get("url")) 231 | internal = from_bool(obj.get("internal")) 232 | reflection = from_bool(obj.get("reflection")) 233 | title = from_str(obj.get("title")) 234 | clicks = from_int(obj.get("clicks")) 235 | return LinkCount(url, internal, reflection, title, clicks) 236 | 237 | def to_dict(self) -> dict: 238 | result: dict = {} 239 | result["url"] = from_str(self.url) 240 | result["internal"] = from_bool(self.internal) 241 | result["reflection"] = from_bool(self.reflection) 242 | result["title"] = from_str(self.title) 243 | result["clicks"] = from_int(self.clicks) 244 | return result 245 | 246 | 247 | @dataclass 248 | class Post: 249 | id: int 250 | name: str 251 | username: str 252 | avatar_template: str 253 | created_at: str 254 | cooked: str 255 | post_number: int 256 | post_type: int 257 | updated_at: str 258 | reply_count: int 259 | reply_to_post_number: int 260 | quote_count: int 261 | incoming_link_count: int 262 | reads: int 263 | readers_count: int 264 | score: float 265 | yours: bool 266 | topic_id: int 267 | topic_slug: str 268 | display_username: str 269 | primary_group_name: str 270 | flair_name: str 271 | flair_url: str 272 | flair_bg_color: str 273 | flair_color: str 274 | version: int 275 | can_edit: bool 276 | can_delete: bool 277 | can_recover: bool 278 | can_see_hidden_post: bool 279 | can_wiki: bool 280 | link_counts: List[LinkCount] 281 | read: bool 282 | user_title: str 283 | bookmarked: bool 284 | actions_summary: List[PostActionsSummary] 285 | moderator: bool 286 | admin: bool 287 | staff: bool 288 | user_id: int 289 | hidden: bool 290 | trust_level: int 291 | deleted_at: str 292 | user_deleted: bool 293 | edit_reason: str 294 | can_view_edit_history: bool 295 | wiki: bool 296 | reviewable_id: int 297 | reviewable_score_count: int 298 | reviewable_score_pending_count: int 299 | 300 | @staticmethod 301 | def from_dict(obj: Any) -> 'Post': 302 | assert isinstance(obj, dict) 303 | id = from_int(obj.get("id")) 304 | name = from_str(obj.get("name")) 305 | username = from_str(obj.get("username")) 306 | avatar_template = from_str(obj.get("avatar_template")) 307 | created_at = from_str(obj.get("created_at")) 308 | cooked = from_str(obj.get("cooked")) 309 | post_number = from_int(obj.get("post_number")) 310 | post_type = from_int(obj.get("post_type")) 311 | updated_at = from_str(obj.get("updated_at")) 312 | reply_count = from_int(obj.get("reply_count")) 313 | reply_to_post_number = from_int(obj.get("reply_to_post_number")) 314 | quote_count = from_int(obj.get("quote_count")) 315 | incoming_link_count = from_int(obj.get("incoming_link_count")) 316 | reads = from_int(obj.get("reads")) 317 | readers_count = from_int(obj.get("readers_count")) 318 | score = from_float(obj.get("score")) 319 | yours = from_bool(obj.get("yours")) 320 | topic_id = from_int(obj.get("topic_id")) 321 | topic_slug = from_str(obj.get("topic_slug")) 322 | display_username = from_str(obj.get("display_username")) 323 | primary_group_name = from_str(obj.get("primary_group_name")) 324 | flair_name = from_str(obj.get("flair_name")) 325 | flair_url = from_str(obj.get("flair_url")) 326 | flair_bg_color = from_str(obj.get("flair_bg_color")) 327 | flair_color = from_str(obj.get("flair_color")) 328 | version = from_int(obj.get("version")) 329 | can_edit = from_bool(obj.get("can_edit")) 330 | can_delete = from_bool(obj.get("can_delete")) 331 | can_recover = from_bool(obj.get("can_recover")) 332 | can_see_hidden_post = from_bool(obj.get("can_see_hidden_post")) 333 | can_wiki = from_bool(obj.get("can_wiki")) 334 | link_counts = from_list(LinkCount.from_dict, obj.get("link_counts")) 335 | read = from_bool(obj.get("read")) 336 | user_title = from_str(obj.get("user_title")) 337 | bookmarked = from_bool(obj.get("bookmarked")) 338 | actions_summary = from_list( 339 | PostActionsSummary.from_dict, obj.get("actions_summary")) 340 | moderator = from_bool(obj.get("moderator")) 341 | admin = from_bool(obj.get("admin")) 342 | staff = from_bool(obj.get("staff")) 343 | user_id = from_int(obj.get("user_id")) 344 | hidden = from_bool(obj.get("hidden")) 345 | trust_level = from_int(obj.get("trust_level")) 346 | deleted_at = from_str(obj.get("deleted_at")) 347 | user_deleted = from_bool(obj.get("user_deleted")) 348 | edit_reason = from_str(obj.get("edit_reason")) 349 | can_view_edit_history = from_bool(obj.get("can_view_edit_history")) 350 | wiki = from_bool(obj.get("wiki")) 351 | reviewable_id = from_int(obj.get("reviewable_id")) 352 | reviewable_score_count = from_int(obj.get("reviewable_score_count")) 353 | reviewable_score_pending_count = from_int( 354 | obj.get("reviewable_score_pending_count")) 355 | return Post(id, name, username, avatar_template, created_at, cooked, post_number, post_type, updated_at, reply_count, reply_to_post_number, quote_count, incoming_link_count, reads, readers_count, score, yours, topic_id, topic_slug, display_username, primary_group_name, flair_name, flair_url, flair_bg_color, flair_color, version, can_edit, can_delete, can_recover, can_see_hidden_post, can_wiki, link_counts, read, user_title, bookmarked, actions_summary, moderator, admin, staff, user_id, hidden, trust_level, deleted_at, user_deleted, edit_reason, can_view_edit_history, wiki, reviewable_id, reviewable_score_count, reviewable_score_pending_count) 356 | 357 | def to_dict(self) -> dict: 358 | result: dict = {} 359 | result["id"] = from_int(self.id) 360 | result["name"] = from_str(self.name) 361 | result["username"] = from_str(self.username) 362 | result["avatar_template"] = from_str(self.avatar_template) 363 | result["created_at"] = from_str(self.created_at) 364 | result["cooked"] = from_str(self.cooked) 365 | result["post_number"] = from_int(self.post_number) 366 | result["post_type"] = from_int(self.post_type) 367 | result["updated_at"] = from_str(self.updated_at) 368 | result["reply_count"] = from_int(self.reply_count) 369 | result["reply_to_post_number"] = from_int(self.reply_to_post_number) 370 | result["quote_count"] = from_int(self.quote_count) 371 | result["incoming_link_count"] = from_int(self.incoming_link_count) 372 | result["reads"] = from_int(self.reads) 373 | result["readers_count"] = from_int(self.readers_count) 374 | result["score"] = from_float(self.score) 375 | result["yours"] = from_bool(self.yours) 376 | result["topic_id"] = from_int(self.topic_id) 377 | result["topic_slug"] = from_str(self.topic_slug) 378 | result["display_username"] = from_str(self.display_username) 379 | result["primary_group_name"] = from_str(self.primary_group_name) 380 | result["flair_name"] = from_str(self.flair_name) 381 | result["flair_url"] = from_str(self.flair_url) 382 | result["flair_bg_color"] = from_str(self.flair_bg_color) 383 | result["flair_color"] = from_str(self.flair_color) 384 | result["version"] = from_int(self.version) 385 | result["can_edit"] = from_bool(self.can_edit) 386 | result["can_delete"] = from_bool(self.can_delete) 387 | result["can_recover"] = from_bool(self.can_recover) 388 | result["can_see_hidden_post"] = from_bool(self.can_see_hidden_post) 389 | result["can_wiki"] = from_bool(self.can_wiki) 390 | result["link_counts"] = from_list( 391 | lambda x: to_class(LinkCount, x), self.link_counts) 392 | result["read"] = from_bool(self.read) 393 | result["user_title"] = from_str(self.user_title) 394 | result["bookmarked"] = from_bool(self.bookmarked) 395 | result["actions_summary"] = from_list(lambda x: to_class( 396 | PostActionsSummary, x), self.actions_summary) 397 | result["moderator"] = from_bool(self.moderator) 398 | result["admin"] = from_bool(self.admin) 399 | result["staff"] = from_bool(self.staff) 400 | result["user_id"] = from_int(self.user_id) 401 | result["hidden"] = from_bool(self.hidden) 402 | result["trust_level"] = from_int(self.trust_level) 403 | result["deleted_at"] = from_str(self.deleted_at) 404 | result["user_deleted"] = from_bool(self.user_deleted) 405 | result["edit_reason"] = from_str(self.edit_reason) 406 | result["can_view_edit_history"] = from_bool(self.can_view_edit_history) 407 | result["wiki"] = from_bool(self.wiki) 408 | result["reviewable_id"] = from_int(self.reviewable_id) 409 | result["reviewable_score_count"] = from_int( 410 | self.reviewable_score_count) 411 | result["reviewable_score_pending_count"] = from_int( 412 | self.reviewable_score_pending_count) 413 | return result 414 | 415 | 416 | @dataclass 417 | class PostStream: 418 | posts: List[Post] 419 | stream: List[int] 420 | 421 | @staticmethod 422 | def from_dict(obj: Any) -> 'PostStream': 423 | assert isinstance(obj, dict) 424 | posts = from_list(Post.from_dict, obj.get("posts")) 425 | stream = from_list(from_int, obj.get("stream")) 426 | return PostStream(posts, stream) 427 | 428 | def to_dict(self) -> dict: 429 | result: dict = {} 430 | result["posts"] = from_list(lambda x: to_class(Post, x), self.posts) 431 | result["stream"] = from_list(from_int, self.stream) 432 | return result 433 | 434 | 435 | @dataclass 436 | class Poster: 437 | extras: str 438 | description: str 439 | user: CreatedBy 440 | 441 | @staticmethod 442 | def from_dict(obj: Any) -> 'Poster': 443 | assert isinstance(obj, dict) 444 | extras = from_str(obj.get("extras")) 445 | description = from_str(obj.get("description")) 446 | user = CreatedBy.from_dict(obj.get("user")) 447 | return Poster(extras, description, user) 448 | 449 | def to_dict(self) -> dict: 450 | result: dict = {} 451 | result["extras"] = from_str(self.extras) 452 | result["description"] = from_str(self.description) 453 | result["user"] = to_class(CreatedBy, self.user) 454 | return result 455 | 456 | 457 | @dataclass 458 | class TagsDescriptions: 459 | pass 460 | 461 | @staticmethod 462 | def from_dict(obj: Any) -> 'TagsDescriptions': 463 | assert isinstance(obj, dict) 464 | return TagsDescriptions() 465 | 466 | def to_dict(self) -> dict: 467 | result: dict = {} 468 | return result 469 | 470 | 471 | @dataclass 472 | class SuggestedTopic: 473 | id: int 474 | title: str 475 | fancy_title: str 476 | slug: str 477 | posts_count: int 478 | reply_count: int 479 | highest_post_number: int 480 | image_url: str 481 | created_at: str 482 | last_posted_at: str 483 | bumped: bool 484 | bumped_at: str 485 | archetype: str 486 | unseen: bool 487 | pinned: bool 488 | unpinned: str 489 | excerpt: str 490 | visible: bool 491 | closed: bool 492 | archived: bool 493 | bookmarked: bool 494 | liked: bool 495 | tags: List[Any] 496 | tags_descriptions: TagsDescriptions 497 | like_count: int 498 | views: int 499 | category_id: int 500 | featured_link: str 501 | posters: List[Poster] 502 | 503 | @staticmethod 504 | def from_dict(obj: Any) -> 'SuggestedTopic': 505 | assert isinstance(obj, dict) 506 | id = from_int(obj.get("id")) 507 | title = from_str(obj.get("title")) 508 | fancy_title = from_str(obj.get("fancy_title")) 509 | slug = from_str(obj.get("slug")) 510 | posts_count = from_int(obj.get("posts_count")) 511 | reply_count = from_int(obj.get("reply_count")) 512 | highest_post_number = from_int(obj.get("highest_post_number")) 513 | image_url = from_str(obj.get("image_url")) 514 | created_at = from_str(obj.get("created_at")) 515 | last_posted_at = from_str(obj.get("last_posted_at")) 516 | bumped = from_bool(obj.get("bumped")) 517 | bumped_at = from_str(obj.get("bumped_at")) 518 | archetype = from_str(obj.get("archetype")) 519 | unseen = from_bool(obj.get("unseen")) 520 | pinned = from_bool(obj.get("pinned")) 521 | unpinned = from_str(obj.get("unpinned")) 522 | excerpt = from_str(obj.get("excerpt")) 523 | visible = from_bool(obj.get("visible")) 524 | closed = from_bool(obj.get("closed")) 525 | archived = from_bool(obj.get("archived")) 526 | bookmarked = from_bool(obj.get("bookmarked")) 527 | liked = from_bool(obj.get("liked")) 528 | tags = from_list(lambda x: x, obj.get("tags")) 529 | tags_descriptions = TagsDescriptions.from_dict( 530 | obj.get("tags_descriptions")) 531 | like_count = from_int(obj.get("like_count")) 532 | views = from_int(obj.get("views")) 533 | category_id = from_int(obj.get("category_id")) 534 | featured_link = from_str(obj.get("featured_link")) 535 | posters = from_list(Poster.from_dict, obj.get("posters")) 536 | return SuggestedTopic(id, title, fancy_title, slug, posts_count, reply_count, highest_post_number, image_url, created_at, last_posted_at, bumped, bumped_at, archetype, unseen, pinned, unpinned, excerpt, visible, closed, archived, bookmarked, liked, tags, tags_descriptions, like_count, views, category_id, featured_link, posters) 537 | 538 | def to_dict(self) -> dict: 539 | result: dict = {} 540 | result["id"] = from_int(self.id) 541 | result["title"] = from_str(self.title) 542 | result["fancy_title"] = from_str(self.fancy_title) 543 | result["slug"] = from_str(self.slug) 544 | result["posts_count"] = from_int(self.posts_count) 545 | result["reply_count"] = from_int(self.reply_count) 546 | result["highest_post_number"] = from_int(self.highest_post_number) 547 | result["image_url"] = from_str(self.image_url) 548 | result["created_at"] = from_str(self.created_at) 549 | result["last_posted_at"] = from_str(self.last_posted_at) 550 | result["bumped"] = from_bool(self.bumped) 551 | result["bumped_at"] = from_str(self.bumped_at) 552 | result["archetype"] = from_str(self.archetype) 553 | result["unseen"] = from_bool(self.unseen) 554 | result["pinned"] = from_bool(self.pinned) 555 | result["unpinned"] = from_str(self.unpinned) 556 | result["excerpt"] = from_str(self.excerpt) 557 | result["visible"] = from_bool(self.visible) 558 | result["closed"] = from_bool(self.closed) 559 | result["archived"] = from_bool(self.archived) 560 | result["bookmarked"] = from_bool(self.bookmarked) 561 | result["liked"] = from_bool(self.liked) 562 | result["tags"] = from_list(lambda x: x, self.tags) 563 | result["tags_descriptions"] = to_class( 564 | TagsDescriptions, self.tags_descriptions) 565 | result["like_count"] = from_int(self.like_count) 566 | result["views"] = from_int(self.views) 567 | result["category_id"] = from_int(self.category_id) 568 | result["featured_link"] = from_str(self.featured_link) 569 | result["posters"] = from_list( 570 | lambda x: to_class(Poster, x), self.posters) 571 | return result 572 | 573 | 574 | @dataclass 575 | class Thumbnail: 576 | max_width: int 577 | max_height: int 578 | width: int 579 | height: int 580 | url: str 581 | 582 | @staticmethod 583 | def from_dict(obj: Any) -> 'Thumbnail': 584 | assert isinstance(obj, dict) 585 | max_width = from_int(obj.get("max_width")) 586 | max_height = from_int(obj.get("max_height")) 587 | width = from_int(obj.get("width")) 588 | height = from_int(obj.get("height")) 589 | url = from_str(obj.get("url")) 590 | return Thumbnail(max_width, max_height, width, height, url) 591 | 592 | def to_dict(self) -> dict: 593 | result: dict = {} 594 | result["max_width"] = from_int(self.max_width) 595 | result["max_height"] = from_int(self.max_height) 596 | result["width"] = from_int(self.width) 597 | result["height"] = from_int(self.height) 598 | result["url"] = from_str(self.url) 599 | return result 600 | 601 | 602 | @dataclass 603 | class Topic: 604 | post_stream: PostStream 605 | timeline_lookup: List[List[int]] 606 | suggested_topics: List[SuggestedTopic] 607 | tags: List[str] 608 | tags_descriptions: TagsDescriptions 609 | id: int 610 | title: str 611 | fancy_title: str 612 | posts_count: int 613 | created_at: str 614 | views: int 615 | reply_count: int 616 | like_count: int 617 | last_posted_at: str 618 | visible: bool 619 | closed: bool 620 | archived: bool 621 | has_summary: bool 622 | archetype: str 623 | slug: str 624 | category_id: int 625 | word_count: int 626 | deleted_at: str 627 | user_id: int 628 | featured_link: str 629 | pinned_globally: bool 630 | pinned_at: str 631 | pinned_until: str 632 | image_url: str 633 | slow_mode_seconds: int 634 | draft: str 635 | draft_key: str 636 | draft_sequence: int 637 | unpinned: str 638 | pinned: bool 639 | current_post_number: int 640 | highest_post_number: int 641 | deleted_by: str 642 | has_deleted: bool 643 | actions_summary: List[TopicActionsSummary] 644 | chunk_size: int 645 | bookmarked: bool 646 | bookmarks: List[Any] 647 | topic_timer: str 648 | message_bus_last_id: int 649 | participant_count: int 650 | show_read_indicator: bool 651 | thumbnails: List[Thumbnail] 652 | slow_mode_enabled_until: str 653 | summarizable: bool 654 | details: Details 655 | 656 | @staticmethod 657 | def from_dict(obj: Any) -> 'Topic': 658 | assert isinstance(obj, dict) 659 | post_stream = PostStream.from_dict(obj.get("post_stream")) 660 | timeline_lookup = from_list( 661 | lambda x: from_list(from_int, x), obj.get("timeline_lookup")) 662 | suggested_topics = from_list( 663 | SuggestedTopic.from_dict, obj.get("suggested_topics")) 664 | tags = from_list(from_str, obj.get("tags")) 665 | tags_descriptions = TagsDescriptions.from_dict( 666 | obj.get("tags_descriptions")) 667 | id = from_int(obj.get("id")) 668 | title = from_str(obj.get("title")) 669 | fancy_title = from_str(obj.get("fancy_title")) 670 | posts_count = from_int(obj.get("posts_count")) 671 | created_at = from_str(obj.get("created_at")) 672 | views = from_int(obj.get("views")) 673 | reply_count = from_int(obj.get("reply_count")) 674 | like_count = from_int(obj.get("like_count")) 675 | last_posted_at = from_str(obj.get("last_posted_at")) 676 | visible = from_bool(obj.get("visible")) 677 | closed = from_bool(obj.get("closed")) 678 | archived = from_bool(obj.get("archived")) 679 | has_summary = from_bool(obj.get("has_summary")) 680 | archetype = from_str(obj.get("archetype")) 681 | slug = from_str(obj.get("slug")) 682 | category_id = from_int(obj.get("category_id")) 683 | word_count = from_int(obj.get("word_count")) 684 | deleted_at = from_str(obj.get("deleted_at")) 685 | user_id = from_int(obj.get("user_id")) 686 | featured_link = from_str(obj.get("featured_link")) 687 | pinned_globally = from_bool(obj.get("pinned_globally")) 688 | pinned_at = from_str(obj.get("pinned_at")) 689 | pinned_until = from_str(obj.get("pinned_until")) 690 | image_url = from_str(obj.get("image_url")) 691 | slow_mode_seconds = from_int(obj.get("slow_mode_seconds")) 692 | draft = from_str(obj.get("draft")) 693 | draft_key = from_str(obj.get("draft_key")) 694 | draft_sequence = from_int(obj.get("draft_sequence")) 695 | unpinned = from_str(obj.get("unpinned")) 696 | pinned = from_bool(obj.get("pinned")) 697 | current_post_number = from_int(obj.get("current_post_number")) 698 | highest_post_number = from_int(obj.get("highest_post_number")) 699 | deleted_by = from_str(obj.get("deleted_by")) 700 | has_deleted = from_bool(obj.get("has_deleted")) 701 | actions_summary = from_list( 702 | TopicActionsSummary.from_dict, obj.get("actions_summary")) 703 | chunk_size = from_int(obj.get("chunk_size")) 704 | bookmarked = from_bool(obj.get("bookmarked")) 705 | bookmarks = from_list(lambda x: x, obj.get("bookmarks")) 706 | topic_timer = from_str(obj.get("topic_timer")) 707 | message_bus_last_id = from_int(obj.get("message_bus_last_id")) 708 | participant_count = from_int(obj.get("participant_count")) 709 | show_read_indicator = from_bool(obj.get("show_read_indicator")) 710 | thumbnails = from_list(Thumbnail.from_dict, obj.get("thumbnails")) 711 | slow_mode_enabled_until = from_str(obj.get("slow_mode_enabled_until")) 712 | summarizable = from_bool(obj.get("summarizable")) 713 | details = Details.from_dict(obj.get("details")) 714 | return Topic(post_stream, timeline_lookup, suggested_topics, tags, tags_descriptions, id, title, fancy_title, posts_count, created_at, views, reply_count, like_count, last_posted_at, visible, closed, archived, has_summary, archetype, slug, category_id, word_count, deleted_at, user_id, featured_link, pinned_globally, pinned_at, pinned_until, image_url, slow_mode_seconds, draft, draft_key, draft_sequence, unpinned, pinned, current_post_number, highest_post_number, deleted_by, has_deleted, actions_summary, chunk_size, bookmarked, bookmarks, topic_timer, message_bus_last_id, participant_count, show_read_indicator, thumbnails, slow_mode_enabled_until, summarizable, details) 715 | 716 | def to_dict(self) -> dict: 717 | result: dict = {} 718 | result["post_stream"] = to_class(PostStream, self.post_stream) 719 | result["timeline_lookup"] = from_list( 720 | lambda x: from_list(from_int, x), self.timeline_lookup) 721 | result["suggested_topics"] = from_list( 722 | lambda x: to_class(SuggestedTopic, x), self.suggested_topics) 723 | result["tags"] = from_list(from_str, self.tags) 724 | result["tags_descriptions"] = to_class( 725 | TagsDescriptions, self.tags_descriptions) 726 | result["id"] = from_int(self.id) 727 | result["title"] = from_str(self.title) 728 | result["fancy_title"] = from_str(self.fancy_title) 729 | result["posts_count"] = from_int(self.posts_count) 730 | result["created_at"] = from_str(self.created_at) 731 | result["views"] = from_int(self.views) 732 | result["reply_count"] = from_int(self.reply_count) 733 | result["like_count"] = from_int(self.like_count) 734 | result["last_posted_at"] = from_str(self.last_posted_at) 735 | result["visible"] = from_bool(self.visible) 736 | result["closed"] = from_bool(self.closed) 737 | result["archived"] = from_bool(self.archived) 738 | result["has_summary"] = from_bool(self.has_summary) 739 | result["archetype"] = from_str(self.archetype) 740 | result["slug"] = from_str(self.slug) 741 | result["category_id"] = from_int(self.category_id) 742 | result["word_count"] = from_int(self.word_count) 743 | result["deleted_at"] = from_str(self.deleted_at) 744 | result["user_id"] = from_int(self.user_id) 745 | result["featured_link"] = from_str(self.featured_link) 746 | result["pinned_globally"] = from_bool(self.pinned_globally) 747 | result["pinned_at"] = from_str(self.pinned_at) 748 | result["pinned_until"] = from_str(self.pinned_until) 749 | result["image_url"] = from_str(self.image_url) 750 | result["slow_mode_seconds"] = from_int(self.slow_mode_seconds) 751 | result["draft"] = from_str(self.draft) 752 | result["draft_key"] = from_str(self.draft_key) 753 | result["draft_sequence"] = from_int(self.draft_sequence) 754 | result["unpinned"] = from_str(self.unpinned) 755 | result["pinned"] = from_bool(self.pinned) 756 | result["current_post_number"] = from_int(self.current_post_number) 757 | result["highest_post_number"] = from_int(self.highest_post_number) 758 | result["deleted_by"] = from_str(self.deleted_by) 759 | result["has_deleted"] = from_bool(self.has_deleted) 760 | result["actions_summary"] = from_list(lambda x: to_class( 761 | TopicActionsSummary, x), self.actions_summary) 762 | result["chunk_size"] = from_int(self.chunk_size) 763 | result["bookmarked"] = from_bool(self.bookmarked) 764 | result["bookmarks"] = from_list(lambda x: x, self.bookmarks) 765 | result["topic_timer"] = from_str(self.topic_timer) 766 | result["message_bus_last_id"] = from_int(self.message_bus_last_id) 767 | result["participant_count"] = from_int(self.participant_count) 768 | result["show_read_indicator"] = from_bool(self.show_read_indicator) 769 | result["thumbnails"] = from_list(Thumbnail.from_dict, self.thumbnails) 770 | result["slow_mode_enabled_until"] = from_str( 771 | self.slow_mode_enabled_until) 772 | result["summarizable"] = from_bool(self.summarizable) 773 | result["details"] = to_class(Details, self.details) 774 | return result 775 | 776 | 777 | def topic_from_dict(s: Any) -> Topic: 778 | return Topic.from_dict(s) 779 | 780 | 781 | def topic_to_dict(x: Topic) -> Any: 782 | return to_class(Topic, x) -------------------------------------------------------------------------------- /models/user.py: -------------------------------------------------------------------------------- 1 | from constants import base_url 2 | from typing import List 3 | from dataclasses import dataclass 4 | from datetime import datetime 5 | from typing import List, Any, Optional 6 | from models.common import * 7 | from dataclasses import dataclass 8 | 9 | 10 | @dataclass 11 | class BadgeType: 12 | id: int 13 | name: str 14 | sort_order: int 15 | 16 | @staticmethod 17 | def from_dict(obj: Any) -> 'BadgeType': 18 | assert isinstance(obj, dict) 19 | id = from_int(obj.get("id")) 20 | name = from_str(obj.get("name")) 21 | sort_order = from_int(obj.get("sort_order")) 22 | return BadgeType(id, name, sort_order) 23 | 24 | def to_dict(self) -> dict: 25 | result: dict = {} 26 | result["id"] = from_int(self.id) 27 | result["name"] = from_str(self.name) 28 | result["sort_order"] = from_int(self.sort_order) 29 | return result 30 | 31 | 32 | @dataclass 33 | class Badge: 34 | id: int 35 | name: str 36 | description: str 37 | grant_count: int 38 | allow_title: bool 39 | multiple_grant: bool 40 | icon: str 41 | image_url: None 42 | listable: bool 43 | enabled: bool 44 | badge_grouping_id: int 45 | system: bool 46 | slug: str 47 | manually_grantable: bool 48 | badge_type_id: int 49 | 50 | @staticmethod 51 | def from_dict(obj: Any) -> 'Badge': 52 | assert isinstance(obj, dict) 53 | id = from_int(obj.get("id")) 54 | name = from_str(obj.get("name")) 55 | description = from_str(obj.get("description")) 56 | grant_count = from_int(obj.get("grant_count")) 57 | allow_title = from_bool(obj.get("allow_title")) 58 | multiple_grant = from_bool(obj.get("multiple_grant")) 59 | icon = from_str(obj.get("icon")) 60 | image_url = from_none(obj.get("image_url")) 61 | listable = from_bool(obj.get("listable")) 62 | enabled = from_bool(obj.get("enabled")) 63 | badge_grouping_id = from_int(obj.get("badge_grouping_id")) 64 | system = from_bool(obj.get("system")) 65 | slug = from_str(obj.get("slug")) 66 | manually_grantable = from_bool(obj.get("manually_grantable")) 67 | badge_type_id = from_int(obj.get("badge_type_id")) 68 | return Badge(id, name, description, grant_count, allow_title, multiple_grant, icon, image_url, listable, enabled, badge_grouping_id, system, slug, manually_grantable, badge_type_id) 69 | 70 | def to_dict(self) -> dict: 71 | result: dict = {} 72 | result["id"] = from_int(self.id) 73 | result["name"] = from_str(self.name) 74 | result["description"] = from_str(self.description) 75 | result["grant_count"] = from_int(self.grant_count) 76 | result["allow_title"] = from_bool(self.allow_title) 77 | result["multiple_grant"] = from_bool(self.multiple_grant) 78 | result["icon"] = from_str(self.icon) 79 | result["image_url"] = from_none(self.image_url) 80 | result["listable"] = from_bool(self.listable) 81 | result["enabled"] = from_bool(self.enabled) 82 | result["badge_grouping_id"] = from_int(self.badge_grouping_id) 83 | result["system"] = from_bool(self.system) 84 | result["slug"] = from_str(self.slug) 85 | result["manually_grantable"] = from_bool(self.manually_grantable) 86 | result["badge_type_id"] = from_int(self.badge_type_id) 87 | return result 88 | 89 | 90 | @dataclass 91 | class GrantedBy: 92 | id: int 93 | username: str 94 | name: str 95 | avatar_template: str 96 | admin: bool 97 | moderator: bool 98 | trust_level: int 99 | 100 | @staticmethod 101 | def from_dict(obj: Any) -> 'GrantedBy': 102 | assert isinstance(obj, dict) 103 | id = from_int(obj.get("id")) 104 | username = from_str(obj.get("username")) 105 | name = from_str(obj.get("name")) 106 | avatar_template = from_str(obj.get("avatar_template")) 107 | admin = from_bool(obj.get("admin")) 108 | moderator = from_bool(obj.get("moderator")) 109 | trust_level = from_int(obj.get("trust_level")) 110 | return GrantedBy(id, username, name, avatar_template, admin, moderator, trust_level) 111 | 112 | def to_dict(self) -> dict: 113 | result: dict = {} 114 | result["id"] = from_int(self.id) 115 | result["username"] = from_str(self.username) 116 | result["name"] = from_str(self.name) 117 | result["avatar_template"] = from_str(self.avatar_template) 118 | result["admin"] = from_bool(self.admin) 119 | result["moderator"] = from_bool(self.moderator) 120 | result["trust_level"] = from_int(self.trust_level) 121 | return result 122 | 123 | 124 | @dataclass 125 | class UserBadge: 126 | id: int 127 | granted_at: datetime 128 | grouping_position: int 129 | post_number: int 130 | topic_id: int 131 | topic_title: str 132 | is_favorite: None 133 | can_favorite: bool 134 | badge_id: int 135 | granted_by_id: int 136 | 137 | @staticmethod 138 | def from_dict(obj: Any) -> 'UserBadge': 139 | assert isinstance(obj, dict) 140 | id = from_int(obj.get("id")) 141 | granted_at = from_datetime(obj.get("granted_at")) 142 | grouping_position = from_int(obj.get("grouping_position")) 143 | post_number = from_int(obj.get("post_number")) 144 | topic_id = from_int(obj.get("topic_id")) 145 | topic_title = from_str(obj.get("topic_title")) 146 | is_favorite = from_none(obj.get("is_favorite")) 147 | can_favorite = from_bool(obj.get("can_favorite")) 148 | badge_id = from_int(obj.get("badge_id")) 149 | granted_by_id = from_int(obj.get("granted_by_id")) 150 | return UserBadge(id, granted_at, grouping_position, post_number, topic_id, topic_title, is_favorite, can_favorite, badge_id, granted_by_id) 151 | 152 | def to_dict(self) -> dict: 153 | result: dict = {} 154 | result["id"] = from_int(self.id) 155 | result["granted_at"] = self.granted_at.isoformat() 156 | result["grouping_position"] = from_int(self.grouping_position) 157 | result["post_number"] = from_int(self.post_number) 158 | result["topic_id"] = from_int(self.topic_id) 159 | result["topic_title"] = from_str(self.topic_title) 160 | result["is_favorite"] = from_none(self.is_favorite) 161 | result["can_favorite"] = from_bool(self.can_favorite) 162 | result["badge_id"] = from_int(self.badge_id) 163 | result["granted_by_id"] = from_int(self.granted_by_id) 164 | return result 165 | 166 | 167 | @dataclass 168 | class UserBadgesInfo: 169 | badges: List[Badge] 170 | badge_types: List[BadgeType] 171 | granted_bies: List[GrantedBy] 172 | user_badges: List[UserBadge] 173 | 174 | @staticmethod 175 | def from_dict(obj: Any) -> 'UserBadgesInfo': 176 | assert isinstance(obj, dict) 177 | badges = from_list(Badge.from_dict, obj.get("badges")) 178 | badge_types = from_list(BadgeType.from_dict, obj.get("badge_types")) 179 | granted_bies = from_list(GrantedBy.from_dict, obj.get("granted_bies")) 180 | user_badges = from_list(UserBadge.from_dict, obj.get("user_badges")) 181 | return UserBadgesInfo(badges, badge_types, granted_bies, user_badges) 182 | 183 | def to_dict(self) -> dict: 184 | result: dict = {} 185 | result["badges"] = from_list(lambda x: to_class(Badge, x), self.badges) 186 | result["badge_types"] = from_list( 187 | lambda x: to_class(BadgeType, x), self.badge_types) 188 | result["granted_bies"] = from_list( 189 | lambda x: to_class(GrantedBy, x), self.granted_bies) 190 | result["user_badges"] = from_list( 191 | lambda x: to_class(UserBadge, x), self.user_badges) 192 | return result 193 | 194 | 195 | def user_badges_info_from_dict(s: Any) -> UserBadgesInfo: 196 | return UserBadgesInfo.from_dict(s) 197 | 198 | 199 | def user_badges_info_to_dict(x: UserBadgesInfo) -> Any: 200 | return to_class(UserBadgesInfo, x) 201 | 202 | 203 | @dataclass 204 | class AssociatedAccount: 205 | name: str 206 | description: str 207 | 208 | @staticmethod 209 | def from_dict(obj: Any) -> 'AssociatedAccount': 210 | assert isinstance(obj, dict) 211 | name = from_str(obj.get("name")) 212 | description = from_str(obj.get("description")) 213 | return AssociatedAccount(name, description) 214 | 215 | def to_dict(self) -> dict: 216 | result: dict = {} 217 | result["name"] = from_str(self.name) 218 | result["description"] = from_str(self.description) 219 | return result 220 | 221 | 222 | @dataclass 223 | class CustomFields: 224 | pass 225 | 226 | @staticmethod 227 | def from_dict(obj: Any) -> 'CustomFields': 228 | assert isinstance(obj, dict) 229 | return CustomFields() 230 | 231 | def to_dict(self) -> dict: 232 | result: dict = {} 233 | return result 234 | 235 | 236 | @dataclass 237 | class GroupUser: 238 | group_id: int 239 | user_id: int 240 | notification_level: int 241 | owner: bool 242 | 243 | @staticmethod 244 | def from_dict(obj: Any) -> 'GroupUser': 245 | assert isinstance(obj, dict) 246 | group_id = from_int(obj.get("group_id")) 247 | user_id = from_int(obj.get("user_id")) 248 | notification_level = from_int(obj.get("notification_level")) 249 | owner = from_bool(obj.get("owner")) 250 | return GroupUser(group_id, user_id, notification_level, owner) 251 | 252 | def to_dict(self) -> dict: 253 | result: dict = {} 254 | result["group_id"] = from_int(self.group_id) 255 | result["user_id"] = from_int(self.user_id) 256 | result["notification_level"] = from_int(self.notification_level) 257 | result["owner"] = from_bool(self.owner) 258 | return result 259 | 260 | 261 | @dataclass 262 | class Group: 263 | id: int 264 | automatic: bool 265 | name: str 266 | display_name: str 267 | user_count: int 268 | mentionable_level: int 269 | messageable_level: int 270 | visibility_level: int 271 | primary_group: bool 272 | title: None 273 | grant_trust_level: None 274 | has_messages: bool 275 | flair_url: None 276 | flair_bg_color: str 277 | flair_color: str 278 | bio_cooked: None 279 | bio_excerpt: None 280 | public_admission: bool 281 | public_exit: bool 282 | allow_membership_requests: bool 283 | full_name: None 284 | default_notification_level: int 285 | membership_request_template: None 286 | members_visibility_level: int 287 | can_see_members: bool 288 | publish_read_state: bool 289 | 290 | @staticmethod 291 | def from_dict(obj: Any) -> 'Group': 292 | assert isinstance(obj, dict) 293 | id = from_int(obj.get("id")) 294 | automatic = from_bool(obj.get("automatic")) 295 | name = from_str(obj.get("name")) 296 | display_name = from_str(obj.get("display_name")) 297 | user_count = from_int(obj.get("user_count")) 298 | mentionable_level = from_int(obj.get("mentionable_level")) 299 | messageable_level = from_int(obj.get("messageable_level")) 300 | visibility_level = from_int(obj.get("visibility_level")) 301 | primary_group = from_bool(obj.get("primary_group")) 302 | title = from_none(obj.get("title")) 303 | grant_trust_level = from_none(obj.get("grant_trust_level")) 304 | has_messages = from_bool(obj.get("has_messages")) 305 | flair_url = from_none(obj.get("flair_url")) 306 | flair_bg_color = from_str(obj.get("flair_bg_color")) 307 | flair_color = from_str(obj.get("flair_color")) 308 | bio_cooked = from_none(obj.get("bio_cooked")) 309 | bio_excerpt = from_none(obj.get("bio_excerpt")) 310 | public_admission = from_bool(obj.get("public_admission")) 311 | public_exit = from_bool(obj.get("public_exit")) 312 | allow_membership_requests = from_bool( 313 | obj.get("allow_membership_requests")) 314 | full_name = from_none(obj.get("full_name")) 315 | default_notification_level = from_int( 316 | obj.get("default_notification_level")) 317 | membership_request_template = from_none( 318 | obj.get("membership_request_template")) 319 | members_visibility_level = from_int( 320 | obj.get("members_visibility_level")) 321 | can_see_members = from_bool(obj.get("can_see_members")) 322 | publish_read_state = from_bool(obj.get("publish_read_state")) 323 | return Group(id, automatic, name, display_name, user_count, mentionable_level, messageable_level, visibility_level, primary_group, title, grant_trust_level, has_messages, flair_url, flair_bg_color, flair_color, bio_cooked, bio_excerpt, public_admission, public_exit, allow_membership_requests, full_name, default_notification_level, membership_request_template, members_visibility_level, can_see_members, publish_read_state) 324 | 325 | def to_dict(self) -> dict: 326 | result: dict = {} 327 | result["id"] = from_int(self.id) 328 | result["automatic"] = from_bool(self.automatic) 329 | result["name"] = from_str(self.name) 330 | result["display_name"] = from_str(self.display_name) 331 | result["user_count"] = from_int(self.user_count) 332 | result["mentionable_level"] = from_int(self.mentionable_level) 333 | result["messageable_level"] = from_int(self.messageable_level) 334 | result["visibility_level"] = from_int(self.visibility_level) 335 | result["primary_group"] = from_bool(self.primary_group) 336 | result["title"] = from_none(self.title) 337 | result["grant_trust_level"] = from_none(self.grant_trust_level) 338 | result["has_messages"] = from_bool(self.has_messages) 339 | result["flair_url"] = from_none(self.flair_url) 340 | result["flair_bg_color"] = from_str(self.flair_bg_color) 341 | result["flair_color"] = from_str(self.flair_color) 342 | result["bio_cooked"] = from_none(self.bio_cooked) 343 | result["bio_excerpt"] = from_none(self.bio_excerpt) 344 | result["public_admission"] = from_bool(self.public_admission) 345 | result["public_exit"] = from_bool(self.public_exit) 346 | result["allow_membership_requests"] = from_bool( 347 | self.allow_membership_requests) 348 | result["full_name"] = from_none(self.full_name) 349 | result["default_notification_level"] = from_int( 350 | self.default_notification_level) 351 | result["membership_request_template"] = from_none( 352 | self.membership_request_template) 353 | result["members_visibility_level"] = from_int( 354 | self.members_visibility_level) 355 | result["can_see_members"] = from_bool(self.can_see_members) 356 | result["publish_read_state"] = from_bool(self.publish_read_state) 357 | return result 358 | 359 | 360 | @ dataclass 361 | class SidebarTag: 362 | name: str 363 | description: None 364 | pm_only: bool 365 | 366 | @ staticmethod 367 | def from_dict(obj: Any) -> 'SidebarTag': 368 | assert isinstance(obj, dict) 369 | name = from_str(obj.get("name")) 370 | description = from_none(obj.get("description")) 371 | pm_only = from_bool(obj.get("pm_only")) 372 | return SidebarTag(name, description, pm_only) 373 | 374 | def to_dict(self) -> dict: 375 | result: dict = {} 376 | result["name"] = from_str(self.name) 377 | result["description"] = from_none(self.description) 378 | result["pm_only"] = from_bool(self.pm_only) 379 | return result 380 | 381 | 382 | @ dataclass 383 | class UserAPIKey: 384 | id: int 385 | application_name: str 386 | scopes: List[str] 387 | created_at: datetime 388 | last_used_at: datetime 389 | 390 | @ staticmethod 391 | def from_dict(obj: Any) -> 'UserAPIKey': 392 | assert isinstance(obj, dict) 393 | id = from_int(obj.get("id")) 394 | application_name = from_str(obj.get("application_name")) 395 | scopes = from_list(from_str, obj.get("scopes")) 396 | created_at = from_datetime(obj.get("created_at")) 397 | last_used_at = from_datetime(obj.get("last_used_at")) 398 | return UserAPIKey(id, application_name, scopes, created_at, last_used_at) 399 | 400 | def to_dict(self) -> dict: 401 | result: dict = {} 402 | result["id"] = from_int(self.id) 403 | result["application_name"] = from_str(self.application_name) 404 | result["scopes"] = from_list(from_str, self.scopes) 405 | result["created_at"] = self.created_at.isoformat() 406 | result["last_used_at"] = self.last_used_at.isoformat() 407 | return result 408 | 409 | 410 | @ dataclass 411 | class UserAuthToken: 412 | id: int 413 | client_ip: str 414 | location: str 415 | browser: str 416 | device: str 417 | os: str 418 | icon: str 419 | created_at: datetime 420 | seen_at: datetime 421 | is_active: bool 422 | 423 | @ staticmethod 424 | def from_dict(obj: Any) -> 'UserAuthToken': 425 | assert isinstance(obj, dict) 426 | id = from_int(obj.get("id")) 427 | client_ip = from_str(obj.get("client_ip")) 428 | location = from_str(obj.get("location")) 429 | browser = from_str(obj.get("browser")) 430 | device = from_str(obj.get("device")) 431 | os = from_str(obj.get("os")) 432 | icon = from_str(obj.get("icon")) 433 | created_at = from_datetime(obj.get("created_at")) 434 | seen_at = from_datetime(obj.get("seen_at")) 435 | is_active = from_bool(obj.get("is_active")) 436 | return UserAuthToken(id, client_ip, location, browser, device, os, icon, created_at, seen_at, is_active) 437 | 438 | def to_dict(self) -> dict: 439 | result: dict = {} 440 | result["id"] = from_int(self.id) 441 | result["client_ip"] = from_str(self.client_ip) 442 | result["location"] = from_str(self.location) 443 | result["browser"] = from_str(self.browser) 444 | result["device"] = from_str(self.device) 445 | result["os"] = from_str(self.os) 446 | result["icon"] = from_str(self.icon) 447 | result["created_at"] = self.created_at.isoformat() 448 | result["seen_at"] = self.seen_at.isoformat() 449 | result["is_active"] = from_bool(self.is_active) 450 | return result 451 | 452 | 453 | @ dataclass 454 | class UserNotificationSchedule: 455 | enabled: bool 456 | day_0__start_time: int 457 | day_0__end_time: int 458 | day_1__start_time: int 459 | day_1__end_time: int 460 | day_2__start_time: int 461 | day_2__end_time: int 462 | day_3__start_time: int 463 | day_3__end_time: int 464 | day_4__start_time: int 465 | day_4__end_time: int 466 | day_5__start_time: int 467 | day_5__end_time: int 468 | day_6__start_time: int 469 | day_6__end_time: int 470 | 471 | @ staticmethod 472 | def from_dict(obj: Any) -> 'UserNotificationSchedule': 473 | assert isinstance(obj, dict) 474 | enabled = from_bool(obj.get("enabled")) 475 | day_0__start_time = from_int(obj.get("day_0_start_time")) 476 | day_0__end_time = from_int(obj.get("day_0_end_time")) 477 | day_1__start_time = from_int(obj.get("day_1_start_time")) 478 | day_1__end_time = from_int(obj.get("day_1_end_time")) 479 | day_2__start_time = from_int(obj.get("day_2_start_time")) 480 | day_2__end_time = from_int(obj.get("day_2_end_time")) 481 | day_3__start_time = from_int(obj.get("day_3_start_time")) 482 | day_3__end_time = from_int(obj.get("day_3_end_time")) 483 | day_4__start_time = from_int(obj.get("day_4_start_time")) 484 | day_4__end_time = from_int(obj.get("day_4_end_time")) 485 | day_5__start_time = from_int(obj.get("day_5_start_time")) 486 | day_5__end_time = from_int(obj.get("day_5_end_time")) 487 | day_6__start_time = from_int(obj.get("day_6_start_time")) 488 | day_6__end_time = from_int(obj.get("day_6_end_time")) 489 | return UserNotificationSchedule(enabled, day_0__start_time, day_0__end_time, day_1__start_time, day_1__end_time, day_2__start_time, day_2__end_time, day_3__start_time, day_3__end_time, day_4__start_time, day_4__end_time, day_5__start_time, day_5__end_time, day_6__start_time, day_6__end_time) 490 | 491 | def to_dict(self) -> dict: 492 | result: dict = {} 493 | result["enabled"] = from_bool(self.enabled) 494 | result["day_0_start_time"] = from_int(self.day_0__start_time) 495 | result["day_0_end_time"] = from_int(self.day_0__end_time) 496 | result["day_1_start_time"] = from_int(self.day_1__start_time) 497 | result["day_1_end_time"] = from_int(self.day_1__end_time) 498 | result["day_2_start_time"] = from_int(self.day_2__start_time) 499 | result["day_2_end_time"] = from_int(self.day_2__end_time) 500 | result["day_3_start_time"] = from_int(self.day_3__start_time) 501 | result["day_3_end_time"] = from_int(self.day_3__end_time) 502 | result["day_4_start_time"] = from_int(self.day_4__start_time) 503 | result["day_4_end_time"] = from_int(self.day_4__end_time) 504 | result["day_5_start_time"] = from_int(self.day_5__start_time) 505 | result["day_5_end_time"] = from_int(self.day_5__end_time) 506 | result["day_6_start_time"] = from_int(self.day_6__start_time) 507 | result["day_6_end_time"] = from_int(self.day_6__end_time) 508 | return result 509 | 510 | 511 | @ dataclass 512 | class UserOption: 513 | user_id: int 514 | mailing_list_mode: bool 515 | mailing_list_mode_frequency: int 516 | email_digests: bool 517 | email_level: int 518 | email_messages_level: int 519 | external_links_in_new_tab: bool 520 | color_scheme_id: None 521 | dark_scheme_id: None 522 | dynamic_favicon: bool 523 | enable_quoting: bool 524 | enable_defer: bool 525 | digest_after_minutes: None 526 | automatically_unpin_topics: bool 527 | auto_track_topics_after_msecs: int 528 | notification_level_when_replying: int 529 | new_topic_duration_minutes: int 530 | email_previous_replies: int 531 | email_in_reply_to: bool 532 | like_notification_frequency: int 533 | include_tl0_in_digests: bool 534 | theme_ids: List[int] 535 | theme_key_seq: int 536 | allow_private_messages: bool 537 | enable_allowed_pm_users: bool 538 | homepage_id: None 539 | hide_profile_and_presence: bool 540 | text_size: str 541 | text_size_seq: int 542 | title_count_mode: str 543 | bookmark_auto_delete_preference: int 544 | timezone: str 545 | skip_new_user_tips: bool 546 | default_calendar: str 547 | oldest_search_log_date: None 548 | seen_popups: List[int] 549 | sidebar_link_to_filtered_list: bool 550 | sidebar_show_count_of_new_items: bool 551 | watched_precedence_over_muted: None 552 | 553 | @ staticmethod 554 | def from_dict(obj: Any) -> 'UserOption': 555 | assert isinstance(obj, dict) 556 | user_id = from_int(obj.get("user_id")) 557 | mailing_list_mode = from_bool(obj.get("mailing_list_mode")) 558 | mailing_list_mode_frequency = from_int( 559 | obj.get("mailing_list_mode_frequency")) 560 | email_digests = from_bool(obj.get("email_digests")) 561 | email_level = from_int(obj.get("email_level")) 562 | email_messages_level = from_int(obj.get("email_messages_level")) 563 | external_links_in_new_tab = from_bool( 564 | obj.get("external_links_in_new_tab")) 565 | color_scheme_id = from_none(obj.get("color_scheme_id")) 566 | dark_scheme_id = from_none(obj.get("dark_scheme_id")) 567 | dynamic_favicon = from_bool(obj.get("dynamic_favicon")) 568 | enable_quoting = from_bool(obj.get("enable_quoting")) 569 | enable_defer = from_bool(obj.get("enable_defer")) 570 | digest_after_minutes = from_none(obj.get("digest_after_minutes")) 571 | automatically_unpin_topics = from_bool( 572 | obj.get("automatically_unpin_topics")) 573 | auto_track_topics_after_msecs = from_int( 574 | obj.get("auto_track_topics_after_msecs")) 575 | notification_level_when_replying = from_int( 576 | obj.get("notification_level_when_replying")) 577 | new_topic_duration_minutes = from_int( 578 | obj.get("new_topic_duration_minutes")) 579 | email_previous_replies = from_int(obj.get("email_previous_replies")) 580 | email_in_reply_to = from_bool(obj.get("email_in_reply_to")) 581 | like_notification_frequency = from_int( 582 | obj.get("like_notification_frequency")) 583 | include_tl0_in_digests = from_bool(obj.get("include_tl0_in_digests")) 584 | theme_ids = from_list(from_int, obj.get("theme_ids")) 585 | theme_key_seq = from_int(obj.get("theme_key_seq")) 586 | allow_private_messages = from_bool(obj.get("allow_private_messages")) 587 | enable_allowed_pm_users = from_bool(obj.get("enable_allowed_pm_users")) 588 | homepage_id = from_none(obj.get("homepage_id")) 589 | hide_profile_and_presence = from_bool( 590 | obj.get("hide_profile_and_presence")) 591 | text_size = from_str(obj.get("text_size")) 592 | text_size_seq = from_int(obj.get("text_size_seq")) 593 | title_count_mode = from_str(obj.get("title_count_mode")) 594 | bookmark_auto_delete_preference = from_int( 595 | obj.get("bookmark_auto_delete_preference")) 596 | timezone = from_str(obj.get("timezone")) 597 | skip_new_user_tips = from_bool(obj.get("skip_new_user_tips")) 598 | default_calendar = from_str(obj.get("default_calendar")) 599 | oldest_search_log_date = from_none(obj.get("oldest_search_log_date")) 600 | seen_popups = from_list(from_int, obj.get("seen_popups")) 601 | sidebar_link_to_filtered_list = from_bool( 602 | obj.get("sidebar_link_to_filtered_list")) 603 | sidebar_show_count_of_new_items = from_bool( 604 | obj.get("sidebar_show_count_of_new_items")) 605 | watched_precedence_over_muted = from_none( 606 | obj.get("watched_precedence_over_muted")) 607 | return UserOption(user_id, mailing_list_mode, mailing_list_mode_frequency, email_digests, email_level, email_messages_level, external_links_in_new_tab, color_scheme_id, dark_scheme_id, dynamic_favicon, enable_quoting, enable_defer, digest_after_minutes, automatically_unpin_topics, auto_track_topics_after_msecs, notification_level_when_replying, new_topic_duration_minutes, email_previous_replies, email_in_reply_to, like_notification_frequency, include_tl0_in_digests, theme_ids, theme_key_seq, allow_private_messages, enable_allowed_pm_users, homepage_id, hide_profile_and_presence, text_size, text_size_seq, title_count_mode, bookmark_auto_delete_preference, timezone, skip_new_user_tips, default_calendar, oldest_search_log_date, seen_popups, sidebar_link_to_filtered_list, sidebar_show_count_of_new_items, watched_precedence_over_muted) 608 | 609 | def to_dict(self) -> dict: 610 | result: dict = {} 611 | result["user_id"] = from_int(self.user_id) 612 | result["mailing_list_mode"] = from_bool(self.mailing_list_mode) 613 | result["mailing_list_mode_frequency"] = from_int( 614 | self.mailing_list_mode_frequency) 615 | result["email_digests"] = from_bool(self.email_digests) 616 | result["email_level"] = from_int(self.email_level) 617 | result["email_messages_level"] = from_int(self.email_messages_level) 618 | result["external_links_in_new_tab"] = from_bool( 619 | self.external_links_in_new_tab) 620 | result["color_scheme_id"] = from_none(self.color_scheme_id) 621 | result["dark_scheme_id"] = from_none(self.dark_scheme_id) 622 | result["dynamic_favicon"] = from_bool(self.dynamic_favicon) 623 | result["enable_quoting"] = from_bool(self.enable_quoting) 624 | result["enable_defer"] = from_bool(self.enable_defer) 625 | result["digest_after_minutes"] = from_none(self.digest_after_minutes) 626 | result["automatically_unpin_topics"] = from_bool( 627 | self.automatically_unpin_topics) 628 | result["auto_track_topics_after_msecs"] = from_int( 629 | self.auto_track_topics_after_msecs) 630 | result["notification_level_when_replying"] = from_int( 631 | self.notification_level_when_replying) 632 | result["new_topic_duration_minutes"] = from_int( 633 | self.new_topic_duration_minutes) 634 | result["email_previous_replies"] = from_int( 635 | self.email_previous_replies) 636 | result["email_in_reply_to"] = from_bool(self.email_in_reply_to) 637 | result["like_notification_frequency"] = from_int( 638 | self.like_notification_frequency) 639 | result["include_tl0_in_digests"] = from_bool( 640 | self.include_tl0_in_digests) 641 | result["theme_ids"] = from_list(from_int, self.theme_ids) 642 | result["theme_key_seq"] = from_int(self.theme_key_seq) 643 | result["allow_private_messages"] = from_bool( 644 | self.allow_private_messages) 645 | result["enable_allowed_pm_users"] = from_bool( 646 | self.enable_allowed_pm_users) 647 | result["homepage_id"] = from_none(self.homepage_id) 648 | result["hide_profile_and_presence"] = from_bool( 649 | self.hide_profile_and_presence) 650 | result["text_size"] = from_str(self.text_size) 651 | result["text_size_seq"] = from_int(self.text_size_seq) 652 | result["title_count_mode"] = from_str(self.title_count_mode) 653 | result["bookmark_auto_delete_preference"] = from_int( 654 | self.bookmark_auto_delete_preference) 655 | result["timezone"] = from_str(self.timezone) 656 | result["skip_new_user_tips"] = from_bool(self.skip_new_user_tips) 657 | result["default_calendar"] = from_str(self.default_calendar) 658 | result["oldest_search_log_date"] = from_none( 659 | self.oldest_search_log_date) 660 | result["seen_popups"] = from_list(from_int, self.seen_popups) 661 | result["sidebar_link_to_filtered_list"] = from_bool( 662 | self.sidebar_link_to_filtered_list) 663 | result["sidebar_show_count_of_new_items"] = from_bool( 664 | self.sidebar_show_count_of_new_items) 665 | result["watched_precedence_over_muted"] = from_none( 666 | self.watched_precedence_over_muted) 667 | return result 668 | 669 | 670 | @ dataclass 671 | class User: 672 | id: int 673 | username: str 674 | name: str 675 | avatar_template: str 676 | email: str 677 | secondary_emails: List[Any] 678 | unconfirmed_emails: List[Any] 679 | last_posted_at: datetime 680 | last_seen_at: datetime 681 | created_at: datetime 682 | ignored: bool 683 | muted: bool 684 | can_ignore_user: bool 685 | can_mute_user: bool 686 | can_send_private_messages: bool 687 | can_send_private_message_to_user: bool 688 | trust_level: int 689 | moderator: bool 690 | admin: bool 691 | title: str 692 | badge_count: int 693 | custom_fields: CustomFields 694 | time_read: int 695 | recent_time_read: int 696 | primary_group_id: None 697 | primary_group_name: None 698 | flair_group_id: None 699 | flair_name: None 700 | flair_url: None 701 | flair_bg_color: None 702 | flair_color: None 703 | featured_topic: None 704 | timezone: str 705 | pending_posts_count: int 706 | can_edit: bool 707 | can_edit_username: bool 708 | can_edit_email: bool 709 | can_edit_name: bool 710 | uploaded_avatar_id: int 711 | has_title_badges: bool 712 | pending_count: int 713 | profile_view_count: int 714 | second_factor_enabled: bool 715 | second_factor_backup_enabled: bool 716 | associated_accounts: List[AssociatedAccount] 717 | can_upload_profile_header: bool 718 | can_upload_user_card_background: bool 719 | locale: str 720 | muted_category_ids: List[int] 721 | regular_category_ids: List[Any] 722 | watched_tags: List[Any] 723 | watching_first_post_tags: List[Any] 724 | tracked_tags: List[Any] 725 | muted_tags: List[Any] 726 | tracked_category_ids: List[Any] 727 | watched_category_ids: List[Any] 728 | watched_first_post_category_ids: List[int] 729 | system_avatar_upload_id: None 730 | system_avatar_template: str 731 | custom_avatar_upload_id: int 732 | custom_avatar_template: str 733 | muted_usernames: List[Any] 734 | ignored_usernames: List[Any] 735 | allowed_pm_usernames: List[Any] 736 | mailing_list_posts_per_day: int 737 | can_change_bio: bool 738 | can_change_location: bool 739 | can_change_website: bool 740 | can_change_tracking_preferences: bool 741 | user_api_keys: List[UserAPIKey] 742 | user_auth_tokens: List[UserAuthToken] 743 | user_notification_schedule: Optional[UserNotificationSchedule] 744 | use_logo_small_as_avatar: bool 745 | sidebar_tags: List[SidebarTag] 746 | sidebar_category_ids: List[int] 747 | display_sidebar_tags: bool 748 | cakedate: datetime 749 | birthdate: None 750 | accepted_answers: int 751 | featured_user_badge_ids: List[int] 752 | invited_by: None 753 | groups: List[Group] 754 | group_users: List[GroupUser] 755 | user_option: Optional[UserOption] 756 | 757 | @ staticmethod 758 | def from_dict(obj: Any) -> 'User': 759 | assert isinstance(obj, dict) 760 | id = from_int(obj.get("id")) 761 | username = from_str(obj.get("username")) 762 | name = from_str(obj.get("name")) 763 | avatar_template = from_str(obj.get("avatar_template")) 764 | email = from_str(obj.get("email")) 765 | secondary_emails = from_list(lambda x: x, obj.get("secondary_emails")) 766 | unconfirmed_emails = from_list(lambda x: x, obj.get("unconfirmed_emails")) 767 | last_posted_at = from_datetime(obj.get("last_posted_at")) 768 | last_seen_at = from_datetime(obj.get("last_seen_at")) 769 | created_at = from_datetime(obj.get("created_at")) 770 | ignored = from_bool(obj.get("ignored")) 771 | muted = from_bool(obj.get("muted")) 772 | can_ignore_user = from_bool(obj.get("can_ignore_user")) 773 | can_mute_user = from_bool(obj.get("can_mute_user")) 774 | can_send_private_messages = from_bool( 775 | obj.get("can_send_private_messages")) 776 | can_send_private_message_to_user = from_bool( 777 | obj.get("can_send_private_message_to_user")) 778 | trust_level = from_int(obj.get("trust_level")) 779 | moderator = from_bool(obj.get("moderator")) 780 | admin = from_bool(obj.get("admin")) 781 | title = from_str(obj.get("title")) 782 | badge_count = from_int(obj.get("badge_count")) 783 | custom_fields = CustomFields.from_dict(obj.get("custom_fields")) 784 | time_read = from_int(obj.get("time_read")) 785 | recent_time_read = from_int(obj.get("recent_time_read")) 786 | primary_group_id = from_none(obj.get("primary_group_id")) 787 | primary_group_name = from_none(obj.get("primary_group_name")) 788 | flair_group_id = from_none(obj.get("flair_group_id")) 789 | flair_name = from_none(obj.get("flair_name")) 790 | flair_url = from_none(obj.get("flair_url")) 791 | flair_bg_color = from_none(obj.get("flair_bg_color")) 792 | flair_color = from_none(obj.get("flair_color")) 793 | featured_topic = from_none(obj.get("featured_topic")) 794 | timezone = from_str(obj.get("timezone")) 795 | pending_posts_count = from_int(obj.get("pending_posts_count")) 796 | can_edit = from_bool(obj.get("can_edit")) 797 | can_edit_username = from_bool(obj.get("can_edit_username")) 798 | can_edit_email = from_bool(obj.get("can_edit_email")) 799 | can_edit_name = from_bool(obj.get("can_edit_name")) 800 | uploaded_avatar_id = from_int(obj.get("uploaded_avatar_id")) 801 | has_title_badges = from_bool(obj.get("has_title_badges")) 802 | pending_count = from_int(obj.get("pending_count")) 803 | profile_view_count = from_int(obj.get("profile_view_count")) 804 | second_factor_enabled = from_bool(obj.get("second_factor_enabled")) 805 | second_factor_backup_enabled = from_bool( 806 | obj.get("second_factor_backup_enabled")) 807 | associated_accounts = from_list( 808 | AssociatedAccount.from_dict, obj.get("associated_accounts")) 809 | can_upload_profile_header = from_bool( 810 | obj.get("can_upload_profile_header")) 811 | can_upload_user_card_background = from_bool( 812 | obj.get("can_upload_user_card_background")) 813 | locale = from_str(obj.get("locale")) 814 | muted_category_ids = from_list(from_int, obj.get("muted_category_ids")) 815 | regular_category_ids = from_list( 816 | lambda x: x, obj.get("regular_category_ids")) 817 | watched_tags = from_list(lambda x: x, obj.get("watched_tags")) 818 | watching_first_post_tags = from_list( 819 | lambda x: x, obj.get("watching_first_post_tags")) 820 | tracked_tags = from_list(lambda x: x, obj.get("tracked_tags")) 821 | muted_tags = from_list(lambda x: x, obj.get("muted_tags")) 822 | tracked_category_ids = from_list( 823 | lambda x: x, obj.get("tracked_category_ids")) 824 | watched_category_ids = from_list( 825 | lambda x: x, obj.get("watched_category_ids")) 826 | watched_first_post_category_ids = from_list( 827 | from_int, obj.get("watched_first_post_category_ids")) 828 | system_avatar_upload_id = from_none(obj.get("system_avatar_upload_id")) 829 | system_avatar_template = from_str(obj.get("system_avatar_template")) 830 | custom_avatar_upload_id = from_int(obj.get("custom_avatar_upload_id")) 831 | custom_avatar_template = from_str(obj.get("custom_avatar_template")) 832 | muted_usernames = from_list(lambda x: x, obj.get("muted_usernames")) 833 | ignored_usernames = from_list( 834 | lambda x: x, obj.get("ignored_usernames")) 835 | allowed_pm_usernames = from_list( 836 | lambda x: x, obj.get("allowed_pm_usernames")) 837 | mailing_list_posts_per_day = from_int( 838 | obj.get("mailing_list_posts_per_day")) 839 | can_change_bio = from_bool(obj.get("can_change_bio")) 840 | can_change_location = from_bool(obj.get("can_change_location")) 841 | can_change_website = from_bool(obj.get("can_change_website")) 842 | can_change_tracking_preferences = from_bool( 843 | obj.get("can_change_tracking_preferences")) 844 | user_api_keys = from_list( 845 | UserAPIKey.from_dict, obj.get("user_api_keys")) 846 | user_auth_tokens = from_list( 847 | UserAuthToken.from_dict, obj.get("user_auth_tokens")) 848 | user_notification_schedule = from_union( 849 | [from_none, UserNotificationSchedule.from_dict], obj.get("user_notification_schedule")) 850 | use_logo_small_as_avatar = from_bool( 851 | obj.get("use_logo_small_as_avatar")) 852 | sidebar_tags = from_list(SidebarTag.from_dict, obj.get("sidebar_tags")) 853 | sidebar_category_ids = from_list( 854 | from_int, obj.get("sidebar_category_ids")) 855 | display_sidebar_tags = from_bool(obj.get("display_sidebar_tags")) 856 | cakedate = from_datetime(obj.get("cakedate")) 857 | birthdate = from_none(obj.get("birthdate")) 858 | accepted_answers = from_int(obj.get("accepted_answers")) 859 | featured_user_badge_ids = from_list( 860 | from_int, obj.get("featured_user_badge_ids")) 861 | invited_by = from_none(obj.get("invited_by")) 862 | groups = from_list(Group.from_dict, obj.get("groups")) 863 | group_users = from_list(GroupUser.from_dict, obj.get("group_users")) 864 | user_option = from_union( 865 | [UserOption.from_dict, from_none], obj.get("user_option")) 866 | return User(id, username, name, avatar_template, email, secondary_emails, unconfirmed_emails, last_posted_at, last_seen_at, created_at, ignored, muted, can_ignore_user, can_mute_user, can_send_private_messages, can_send_private_message_to_user, trust_level, moderator, admin, title, badge_count, custom_fields, time_read, recent_time_read, primary_group_id, primary_group_name, flair_group_id, flair_name, flair_url, flair_bg_color, flair_color, featured_topic, timezone, pending_posts_count, can_edit, can_edit_username, can_edit_email, can_edit_name, uploaded_avatar_id, has_title_badges, pending_count, profile_view_count, second_factor_enabled, second_factor_backup_enabled, associated_accounts, can_upload_profile_header, can_upload_user_card_background, locale, muted_category_ids, regular_category_ids, watched_tags, watching_first_post_tags, tracked_tags, muted_tags, tracked_category_ids, watched_category_ids, watched_first_post_category_ids, system_avatar_upload_id, system_avatar_template, custom_avatar_upload_id, custom_avatar_template, muted_usernames, ignored_usernames, allowed_pm_usernames, mailing_list_posts_per_day, can_change_bio, can_change_location, can_change_website, can_change_tracking_preferences, user_api_keys, user_auth_tokens, user_notification_schedule, use_logo_small_as_avatar, sidebar_tags, sidebar_category_ids, display_sidebar_tags, cakedate, birthdate, accepted_answers, featured_user_badge_ids, invited_by, groups, group_users, user_option) 867 | 868 | def to_dict(self) -> dict: 869 | result: dict = {} 870 | result["id"] = from_int(self.id) 871 | result["username"] = from_str(self.username) 872 | result["name"] = from_str(self.name) 873 | result["avatar_template"] = from_str(self.avatar_template) 874 | result["email"] = from_str(self.email) 875 | result["secondary_emails"] = from_list(lambda x: x, self.secondary_emails) 876 | result["unconfirmed_emails"] = from_list(lambda x: x, self.unconfirmed_emails) 877 | result["last_posted_at"] = self.last_posted_at.isoformat() 878 | result["last_seen_at"] = self.last_seen_at.isoformat() 879 | result["created_at"] = self.created_at.isoformat() 880 | result["ignored"] = from_bool(self.ignored) 881 | result["muted"] = from_bool(self.muted) 882 | result["can_ignore_user"] = from_bool(self.can_ignore_user) 883 | result["can_mute_user"] = from_bool(self.can_mute_user) 884 | result["can_send_private_messages"] = from_bool( 885 | self.can_send_private_messages) 886 | result["can_send_private_message_to_user"] = from_bool( 887 | self.can_send_private_message_to_user) 888 | result["trust_level"] = from_int(self.trust_level) 889 | result["moderator"] = from_bool(self.moderator) 890 | result["admin"] = from_bool(self.admin) 891 | result["title"] = from_str(self.title) 892 | result["badge_count"] = from_int(self.badge_count) 893 | result["custom_fields"] = to_class(CustomFields, self.custom_fields) 894 | result["time_read"] = from_int(self.time_read) 895 | result["recent_time_read"] = from_int(self.recent_time_read) 896 | result["primary_group_id"] = from_none(self.primary_group_id) 897 | result["primary_group_name"] = from_none(self.primary_group_name) 898 | result["flair_group_id"] = from_none(self.flair_group_id) 899 | result["flair_name"] = from_none(self.flair_name) 900 | result["flair_url"] = from_none(self.flair_url) 901 | result["flair_bg_color"] = from_none(self.flair_bg_color) 902 | result["flair_color"] = from_none(self.flair_color) 903 | result["featured_topic"] = from_none(self.featured_topic) 904 | result["timezone"] = from_str(self.timezone) 905 | result["pending_posts_count"] = from_int(self.pending_posts_count) 906 | result["can_edit"] = from_bool(self.can_edit) 907 | result["can_edit_username"] = from_bool(self.can_edit_username) 908 | result["can_edit_email"] = from_bool(self.can_edit_email) 909 | result["can_edit_name"] = from_bool(self.can_edit_name) 910 | result["uploaded_avatar_id"] = from_int(self.uploaded_avatar_id) 911 | result["has_title_badges"] = from_bool(self.has_title_badges) 912 | result["pending_count"] = from_int(self.pending_count) 913 | result["profile_view_count"] = from_int(self.profile_view_count) 914 | result["second_factor_enabled"] = from_bool(self.second_factor_enabled) 915 | result["second_factor_backup_enabled"] = from_bool( 916 | self.second_factor_backup_enabled) 917 | result["associated_accounts"] = from_list( 918 | AssociatedAccount.from_dict, self.associated_accounts) 919 | result["can_upload_profile_header"] = from_bool( 920 | self.can_upload_profile_header) 921 | result["can_upload_user_card_background"] = from_bool( 922 | self.can_upload_user_card_background) 923 | result["locale"] = from_str(self.locale) 924 | result["muted_category_ids"] = from_list( 925 | from_int, self.muted_category_ids) 926 | result["regular_category_ids"] = from_list( 927 | lambda x: x, self.regular_category_ids) 928 | result["watched_tags"] = from_list(lambda x: x, self.watched_tags) 929 | result["watching_first_post_tags"] = from_list( 930 | lambda x: x, self.watching_first_post_tags) 931 | result["tracked_tags"] = from_list(lambda x: x, self.tracked_tags) 932 | result["muted_tags"] = from_list(lambda x: x, self.muted_tags) 933 | result["tracked_category_ids"] = from_list( 934 | lambda x: x, self.tracked_category_ids) 935 | result["watched_category_ids"] = from_list( 936 | lambda x: x, self.watched_category_ids) 937 | result["watched_first_post_category_ids"] = from_list( 938 | from_int, self.watched_first_post_category_ids) 939 | result["system_avatar_upload_id"] = from_none( 940 | self.system_avatar_upload_id) 941 | result["system_avatar_template"] = from_str( 942 | self.system_avatar_template) 943 | result["custom_avatar_upload_id"] = from_int( 944 | self.custom_avatar_upload_id) 945 | result["custom_avatar_template"] = from_str( 946 | self.custom_avatar_template) 947 | result["muted_usernames"] = from_list( 948 | lambda x: x, self.muted_usernames) 949 | result["ignored_usernames"] = from_list( 950 | lambda x: x, self.ignored_usernames) 951 | result["allowed_pm_usernames"] = from_list( 952 | lambda x: x, self.allowed_pm_usernames) 953 | result["mailing_list_posts_per_day"] = from_int( 954 | self.mailing_list_posts_per_day) 955 | result["can_change_bio"] = from_bool(self.can_change_bio) 956 | result["can_change_location"] = from_bool(self.can_change_location) 957 | result["can_change_website"] = from_bool(self.can_change_website) 958 | result["can_change_tracking_preferences"] = from_bool( 959 | self.can_change_tracking_preferences) 960 | result["user_api_keys"] = from_list( 961 | lambda x: to_class(UserAPIKey, x), self.user_api_keys) 962 | result["user_auth_tokens"] = from_list( 963 | lambda x: to_class(UserAuthToken, x), self.user_auth_tokens) 964 | result["user_notification_schedule"] = from_union( 965 | [from_none, UserNotificationSchedule.from_dict], self.user_notification_schedule) 966 | result["use_logo_small_as_avatar"] = from_bool( 967 | self.use_logo_small_as_avatar) 968 | result["sidebar_tags"] = from_list( 969 | lambda x: to_class(SidebarTag, x), self.sidebar_tags) 970 | result["sidebar_category_ids"] = from_list( 971 | from_int, self.sidebar_category_ids) 972 | result["display_sidebar_tags"] = from_bool(self.display_sidebar_tags) 973 | result["cakedate"] = self.cakedate.isoformat() 974 | result["birthdate"] = from_none(self.birthdate) 975 | result["accepted_answers"] = from_int(self.accepted_answers) 976 | result["featured_user_badge_ids"] = from_list( 977 | from_int, self.featured_user_badge_ids) 978 | result["invited_by"] = from_none(self.invited_by) 979 | result["groups"] = from_list(lambda x: to_class(Group, x), self.groups) 980 | result["group_users"] = from_list( 981 | lambda x: to_class(GroupUser, x), self.group_users) 982 | result["user_option"] = from_union( 983 | [UserOption.from_dict, from_none], self.user_option) 984 | return result 985 | 986 | 987 | def user_from_dict(s: Any) -> User: 988 | return User.from_dict(s) 989 | 990 | 991 | def user_to_dict(x: User) -> Any: 992 | return to_class(User, x) 993 | 994 | 995 | @ dataclass 996 | class UserInfo: 997 | user_badges: List[Any] 998 | user: User 999 | 1000 | @ staticmethod 1001 | def from_dict(obj: Any) -> 'UserInfo': 1002 | assert isinstance(obj, dict) 1003 | user_badges = obj.get("user_badges") 1004 | user = User.from_dict(obj.get("user")) 1005 | return UserInfo(user_badges, user) 1006 | 1007 | def to_dict(self) -> dict: 1008 | result: dict = {} 1009 | result["user_badges"] = self.user_badges 1010 | result["user"] = self.user.to_dict() 1011 | 1012 | 1013 | @ dataclass 1014 | class UserEmailInfo: 1015 | email: str 1016 | secondary_emails: List[Any] 1017 | unconfirmed_emails: List[Any] 1018 | associated_accounts: List[Any] 1019 | 1020 | @ staticmethod 1021 | def from_dict(obj: Any) -> 'UserEmailInfo': 1022 | assert isinstance(obj, dict) 1023 | email = from_str(obj.get("email")) 1024 | secondary_emails = obj.get("secondary_emails") 1025 | unconfirmed_emails = obj.get("unconfirmed_emails") 1026 | associated_accounts = obj.get("associated_accounts") 1027 | return UserEmailInfo(email, secondary_emails, unconfirmed_emails, associated_accounts) 1028 | 1029 | def to_dict(self) -> dict: 1030 | result: dict = {} 1031 | result["email"] = from_str(self.email) 1032 | result["secondary_emails"] = self.secondary_emails 1033 | result["unconfirmed_emails"] = self.unconfirmed_emails 1034 | result["associated_accounts"] = self.associated_accounts 1035 | return result 1036 | 1037 | 1038 | def user_email_info_from_dict(s: Any) -> UserEmailInfo: 1039 | return UserEmailInfo.from_dict(s) 1040 | 1041 | 1042 | def user_email_info_to_dict(x: UserEmailInfo) -> Any: 1043 | return to_class(UserEmailInfo, x) 1044 | 1045 | 1046 | @ dataclass 1047 | class UserAction: 1048 | excerpt: str 1049 | action_type: int 1050 | created_at: str 1051 | avatar_template: str 1052 | acting_avatar_template: str 1053 | slug: str 1054 | topic_id: int 1055 | target_user_id: int 1056 | target_name: str 1057 | target_username: str 1058 | post_number: int 1059 | post_id: int 1060 | username: str 1061 | name: str 1062 | user_id: int 1063 | acting_username: str 1064 | acting_name: str 1065 | acting_user_id: int 1066 | title: str 1067 | deleted: bool 1068 | hidden: bool 1069 | post_type: int 1070 | action_code: str 1071 | category_id: int 1072 | closed: bool 1073 | archived: bool 1074 | 1075 | @ staticmethod 1076 | def from_dict(obj: Any) -> 'UserAction': 1077 | assert isinstance(obj, dict) 1078 | excerpt = from_str(obj.get("excerpt")) 1079 | action_type = from_int(obj.get("action_type")) 1080 | created_at = from_str(obj.get("created_at")) 1081 | avatar_template = from_str(obj.get("avatar_template")) 1082 | acting_avatar_template = from_str(obj.get("acting_avatar_template")) 1083 | slug = from_str(obj.get("slug")) 1084 | topic_id = from_int(obj.get("topic_id")) 1085 | target_user_id = from_int(obj.get("target_user_id")) 1086 | target_name = from_str(obj.get("target_name")) 1087 | target_username = from_str(obj.get("target_username")) 1088 | post_number = from_int(obj.get("post_number")) 1089 | post_id = from_int(obj.get("post_id")) 1090 | username = from_str(obj.get("username")) 1091 | name = from_str(obj.get("name")) 1092 | user_id = from_int(obj.get("user_id")) 1093 | acting_username = from_str(obj.get("acting_username")) 1094 | acting_name = from_str(obj.get("acting_name")) 1095 | acting_user_id = from_int(obj.get("acting_user_id")) 1096 | title = from_str(obj.get("title")) 1097 | deleted = from_bool(obj.get("deleted")) 1098 | hidden = from_bool(obj.get("hidden")) 1099 | post_type = from_int(obj.get("post_type")) 1100 | action_code = from_str(obj.get("action_code")) 1101 | category_id = from_int(obj.get("category_id")) 1102 | closed = from_bool(obj.get("closed")) 1103 | archived = from_bool(obj.get("archived")) 1104 | return UserAction(excerpt, action_type, created_at, avatar_template, acting_avatar_template, slug, topic_id, target_user_id, target_name, target_username, post_number, post_id, username, name, user_id, acting_username, acting_name, acting_user_id, title, deleted, hidden, post_type, action_code, category_id, closed, archived) 1105 | 1106 | def to_dict(self) -> dict: 1107 | result: dict = {} 1108 | result["excerpt"] = from_str(self.excerpt) 1109 | result["action_type"] = from_int(self.action_type) 1110 | result["created_at"] = from_str(self.created_at) 1111 | result["avatar_template"] = from_str(self.avatar_template) 1112 | result["acting_avatar_template"] = from_str( 1113 | self.acting_avatar_template) 1114 | result["slug"] = from_str(self.slug) 1115 | result["topic_id"] = from_int(self.topic_id) 1116 | result["target_user_id"] = from_int(self.target_user_id) 1117 | result["target_name"] = from_str(self.target_name) 1118 | result["target_username"] = from_str(self.target_username) 1119 | result["post_number"] = from_int(self.post_number) 1120 | result["post_id"] = from_int(self.post_id) 1121 | result["username"] = from_str(self.username) 1122 | result["name"] = from_str(self.name) 1123 | result["user_id"] = from_int(self.user_id) 1124 | result["acting_username"] = from_str(self.acting_username) 1125 | result["acting_name"] = from_str(self.acting_name) 1126 | result["acting_user_id"] = from_int(self.acting_user_id) 1127 | result["title"] = from_str(self.title) 1128 | result["deleted"] = from_bool(self.deleted) 1129 | result["hidden"] = from_bool(self.hidden) 1130 | result["post_type"] = from_int(self.post_type) 1131 | result["action_code"] = from_str(self.action_code) 1132 | result["category_id"] = from_int(self.category_id) 1133 | result["closed"] = from_bool(self.closed) 1134 | result["archived"] = from_bool(self.archived) 1135 | return result 1136 | 1137 | 1138 | @ dataclass 1139 | class UserActionsInfo: 1140 | user_actions: List[UserAction] 1141 | 1142 | @ staticmethod 1143 | def from_dict(obj: Any) -> 'UserActionsInfo': 1144 | assert isinstance(obj, dict) 1145 | user_actions = from_list(UserAction.from_dict, obj.get("user_actions")) 1146 | return UserActionsInfo(user_actions) 1147 | 1148 | def to_dict(self) -> dict: 1149 | result: dict = {} 1150 | result["user_actions"] = from_list( 1151 | lambda x: to_class(UserAction, x), self.user_actions) 1152 | return result 1153 | 1154 | 1155 | def user_actions_from_dict(s: Any) -> UserActionsInfo: 1156 | return UserActionsInfo.from_dict(s) 1157 | 1158 | 1159 | def user_actions_to_dict(x: UserActionsInfo) -> Any: 1160 | return to_class(UserActionsInfo, x) 1161 | --------------------------------------------------------------------------------