├── main.py ├── .flake8 ├── .github └── workflows │ ├── python-publish.yml │ └── python-test.yml ├── pyproject.toml ├── LICENSE ├── .gitignore ├── repltalk ├── graphql.py ├── queries.py └── __init__.py ├── test_repltalk.py ├── README.md └── poetry.lock /main.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E402,E712,E501,E722,C901,ET121,F401,W191,PAI 3 | exclude = .git,__pycache__,.venv 4 | use-flake8-tabs = true 5 | inline-quotes = single 6 | multiline-quotes = single 7 | docstring-quotes = single 8 | select=E9,F63,F7,F82 9 | per-file-ignores = 10 | # imported but unused 11 | __init__.py: F401 -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Poetry when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Build and publish to pypi 16 | uses: JRubics/poetry-publish@v1.9 17 | with: 18 | pypi_token: ${{ secrets.PYPI_TOKEN }} 19 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool] 2 | [tool.poetry] 3 | name = "repltalk" 4 | version = "1.0.1" 5 | description = "Allows you to do various things with the kind of unofficial Replit Repl Talk API" 6 | authors = ["mat "] 7 | readme = "README.md" 8 | license = "MIT" 9 | homepage = "https://github.com/mat-1/repl-talk-api" 10 | repository = "https://github.com/mat-1/repl-talk-api" 11 | include = ["LICENSE"] 12 | 13 | [tool.poetry.dependencies] 14 | aiohttp = "^3.6" 15 | python = "^3.8" 16 | 17 | [tool.poetry.dev-dependencies] 18 | flake8 = "^4.0" 19 | 20 | [build-system] 21 | build-backend = "poetry.masonry.api" 22 | requires = ["poetry>=0.12"] 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 mat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/python-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python 3.8 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: 3.8 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install poetry 28 | if [ -f pyproject.toml ]; then poetry install; fi 29 | - name: Lint with flake8 30 | run: | 31 | # stop the build if there are Python syntax errors or undefined names 32 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 33 | poetry run flake8 . --count --max-complexity=10 --show-source --max-line-length=127 --statistics --config .flake8 34 | - name: Test 35 | run: | 36 | poetry run python -m unittest 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 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 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 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 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | -------------------------------------------------------------------------------- /repltalk/graphql.py: -------------------------------------------------------------------------------- 1 | def builtin_to_graphql(item): 2 | if isinstance(item, list) or isinstance(item, tuple) or isinstance(item, set): 3 | value = Field(*item) 4 | elif isinstance(item, dict): 5 | value = Field(item) 6 | else: 7 | value = str(item) 8 | return value 9 | 10 | 11 | class Alias(): 12 | def __init__(self, alias, field): 13 | self.alias = alias 14 | self.field = field 15 | 16 | def __repr__(self): 17 | return f'{self.alias}: {self.field}' 18 | 19 | 20 | def create_args_string(args): 21 | output = '' 22 | if args != {}: 23 | args_tmp = [] 24 | for arg_key in args: 25 | arg_value = args[arg_key] 26 | args_tmp.append(f'{arg_key}:{arg_value}') 27 | output += '(' 28 | output += ','.join(args_tmp) 29 | output += ')' 30 | return output 31 | 32 | 33 | class Field(): 34 | def __init__(self, *args, **kwargs): 35 | if 'name' in kwargs: 36 | self.data = ({kwargs['name']: kwargs['data']},) 37 | elif 'data' in kwargs and len(args) == 1: 38 | self.data = ({args[0]: kwargs.get('data')},) 39 | else: 40 | self.data = args or (kwargs.get('data'),) 41 | self.args = kwargs.get('args', {}) 42 | # if 'name' not in kwargs and self.args == {}: 43 | # self.args = kwargs 44 | 45 | def __str__(self): 46 | output = '' 47 | 48 | for item in self.data: 49 | if isinstance(item, str): 50 | output += item 51 | output += create_args_string(self.args) 52 | elif isinstance(item, Field): 53 | output += str(item) 54 | output += create_args_string(self.args) 55 | elif isinstance(item, Alias): 56 | output += str(item) 57 | output += create_args_string(self.args) 58 | elif isinstance(item, list) or isinstance(item, tuple): 59 | item_as_graphql = str(builtin_to_graphql(item)) 60 | output += item_as_graphql 61 | output += create_args_string(self.args) 62 | elif isinstance(item, Fragment): 63 | output += item.added_fragment() 64 | else: 65 | for field in item: 66 | value = item[field] 67 | value = builtin_to_graphql(value) 68 | output += str(field) 69 | output += create_args_string(self.args) 70 | 71 | output += '{' 72 | output += str(value) 73 | output += '}' 74 | output += ' ' 75 | if output.endswith(' '): 76 | output = output[:-1] 77 | return output 78 | 79 | def __repr__(self): 80 | return self.__str__() 81 | 82 | class Query(): 83 | def __init__(self, name, args, data, fragments=[]): 84 | self.field = Field({name: data}, args=args) 85 | self.frags = fragments 86 | 87 | def __str__(self): 88 | output = 'query ' + str(self.field) 89 | for frag in self.frags: 90 | str_frag = frag.fragment_string() 91 | output += str_frag 92 | return output 93 | 94 | 95 | class Mutation(): 96 | def __init__(self, name, args, data): 97 | self.field = Field({name: data}, args=args) 98 | 99 | def __str__(self): 100 | return 'mutation ' + str(self.field) 101 | 102 | 103 | class Fragment(): 104 | def __init__(self, fragment_name, adding_to_name, data): 105 | self.name = fragment_name 106 | self.adding_to_name = adding_to_name 107 | self.field = Field(data) 108 | 109 | def fragment_string(self): 110 | return f'fragment {self.name} on {self.adding_to_name} { {self.field} }' 111 | 112 | def added_fragment(self): 113 | return f'...{self.name}' 114 | 115 | def __str__(self): 116 | return self.added_fragment() 117 | -------------------------------------------------------------------------------- /test_repltalk.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import repltalk 3 | import asyncio 4 | import datetime 5 | 6 | # The following code is for unit tests, 7 | # please read README.md for documentation 8 | 9 | 10 | class TestReplTalk(unittest.TestCase): 11 | def setUp(self): 12 | self.client = repltalk.Client() 13 | self.loop = asyncio.get_event_loop() 14 | self.run_async = self.loop.run_until_complete 15 | 16 | async def async_test_board_posts(self): 17 | async for post in self.client.boards.all.get_posts(sort='new', limit=1): 18 | self.assertIsInstance(post.author, repltalk.User) 19 | 20 | def test_board_posts(self): 21 | self.run_async(self.async_test_board_posts()) 22 | 23 | def test_post(self): 24 | post = self.run_async(self.client.get_post(5599)) 25 | self.assertIsNotNone(post, 'Post should not be None') 26 | self.assertIsNotNone(post.board, 'Post board should not be None') 27 | self.assertIsInstance( 28 | post.timestamp, datetime.datetime, 'Post time is a datetime object' 29 | ) 30 | self.assertIsNotNone(post.author, 'Author is not None') 31 | if post.repl is not None: 32 | self.assertIsNotNone(post.repl.language) 33 | self.assertTrue(post.board.name.istitle(), 'Board name is a title') 34 | self.assertTrue(post.board.slug.islower(), 'Board slug is lowercase') 35 | 36 | def test_deleted_post(self): 37 | try: 38 | self.run_async(self.client.get_post(0)) 39 | except repltalk.PostNotFound: 40 | return 41 | raise Exception('Deleted post did not raise PostNotFound') 42 | 43 | async def async_test_comments(self): 44 | post = await self.client.get_post(5599) 45 | for c in await post.get_comments(): 46 | self.assertIsInstance(c.id, int) 47 | 48 | def test_comments(self): 49 | self.run_async(self.async_test_comments()) 50 | 51 | def test_user_comments(self): 52 | user = self.run_async(self.client.get_user('mat1')) 53 | comments = self.run_async(user.get_comments()) 54 | self.assertIsInstance(comments, list, 'Not a list of comments?') 55 | 56 | def test_post_exists(self): 57 | exists = self.run_async(self.client.post_exists(1)) 58 | self.assertFalse(exists, 'Invalid post should not exist') 59 | 60 | exists = self.run_async(self.client.post_exists(5599)) 61 | self.assertTrue(exists, 'Real post should exist') 62 | 63 | def test_get_user(self): 64 | user = self.run_async(self.client.get_user('a')) 65 | self.assertIsNone(user) 66 | 67 | user = self.run_async(self.client.get_user('mat1')) 68 | self.assertIsInstance(user.id, int, 'User ID is not an integer') 69 | self.assertIsNotNone(user) 70 | 71 | async def async_test_leaderboards(self): 72 | users = await self.client.get_leaderboard() 73 | for user in users: 74 | self.assertIsInstance(user.id, int) 75 | 76 | def test_leaderboards(self): 77 | self.run_async(self.async_test_leaderboards()) 78 | 79 | async def async_test_get_new_posts(self): 80 | async for post in self.client.boards.all.get_posts(sort='new'): 81 | pass 82 | 83 | def test_get_new_posts(self): 84 | self.run_async(self.async_test_get_new_posts()) 85 | 86 | def test_answered(self): 87 | post = self.run_async(self.client.get_post(12578)) 88 | self.assertTrue(post.answered) 89 | self.assertTrue(post.can_answer) 90 | 91 | def test_language(self): 92 | post = self.run_async(self.client.get_post(13315)) 93 | self.assertEqual(post.language.id, 'python3') 94 | self.assertEqual(str(post.language), 'Python') 95 | 96 | async def async_test_async_for_posts(self): 97 | async for post in self.client.boards.all.get_posts(sort='new', limit=64): 98 | self.assertIsInstance(post, repltalk.Post) 99 | 100 | def test_async_for_posts(self): 101 | self.run_async(self.async_test_async_for_posts()) 102 | 103 | def make_example_user(self, override={}): 104 | data = { 105 | 'id': '747811', 106 | 'username': 'mat1', 107 | 'image': None, 108 | 'url': 'https://repl.it/@mat1', 109 | 'karma': 1000, 110 | 'roles': [], 111 | 'fullName': 'mat', 112 | 'firstName': 'mat', 113 | 'lastName': ' ', 114 | 'timeCreated': '2000-1-01T01:01:00.000Z', 115 | 'organization': None, 116 | 'isLoggedIn': False, 117 | 'bio': 'I do dev. https://matdoes.dev', 118 | 'isHacker': True, 119 | 'languages': [] 120 | } 121 | data.update(override) 122 | return repltalk.User(self.client, data) 123 | 124 | def make_example_board(self, rich=True): 125 | return repltalk.RichBoard(self.client, { 126 | 'id': 14, 127 | 'url': '/talk/announcements', 128 | 'name': 'Announcements', 129 | 'slug': 'announcements', 130 | 'body_cta': None, 131 | 'title_cta': None, 132 | 'button_cta': 'Post an update' 133 | }) 134 | 135 | def make_example_post(self, lazy=False): 136 | if lazy: 137 | return repltalk.LazyPost(self.client, { 138 | 'id': '135633', 139 | 'title': 'Repl Talk Rules and Guidelines [README]', 140 | 'url': '/talk/announcements/Repl-Talk-Rules-and-Guidelines-README/135633', 141 | 'body': None, 142 | 'user': self.make_example_user().data, 143 | }) 144 | else: 145 | return repltalk.Post(self.client, { 146 | 'id': '135633', 147 | 'title': 'Repl Talk Rules and Guidelines [README]', 148 | 'body': 'Rules', 149 | 'isAnnouncement': True, 150 | 'url': '/talk/announcements/Repl-Talk-Rules-and-Guidelines-README/135633', 151 | 'board': self.make_example_board().data, 152 | 'timeCreated': '2000-1-01T01:01:00.000Z', 153 | 'canEdit': False, 154 | 'canComment': True, 155 | 'canPin': False, 156 | 'canSetType': False, 157 | 'canReport': True, 158 | 'hasReported': False, 159 | 'isLocked': False, 160 | 'showHosted': False, 161 | 'voteCount': 100, 162 | 'votes': { 163 | 'items': [] 164 | }, 165 | 'canVote': True, 166 | 'hasVoted': False, 167 | 'user': self.make_example_user().data, 168 | 'repl': None, 169 | 'isAnswered': False, 170 | 'isAnswerable': False, 171 | 'isPinned': True, 172 | 'commentCount': 100 173 | }) 174 | 175 | def make_example_report(self): 176 | return repltalk.Report(self.client, { 177 | 'id': 1, 178 | 'type': 'post', 179 | 'reason': 'Report reason', 180 | 'resolved': False, 181 | 'timeCreated': None, 182 | 'creator': self.make_example_user().data, 183 | 'post': self.make_example_post(lazy=True).data 184 | }) 185 | 186 | 187 | async def async_test_report_get_attached(self): 188 | print('making example report') 189 | report = self.make_example_report() 190 | assert isinstance(report.post, repltalk.LazyPost) 191 | await report.get_attached() 192 | assert isinstance(report.post, repltalk.Post) 193 | await report.get_attached() 194 | assert isinstance(report.post, repltalk.Post) 195 | 196 | def test_report_get_attached(self): 197 | self.run_async(self.async_test_report_get_attached()) 198 | 199 | 200 | if __name__ == '__main__': 201 | unittest.main() 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | ```py 3 | # Getting the newest posts on Repl Talk and printing their titles 4 | async for post in client.boards.all.get_posts(): 5 | print(post.title) 6 | ``` 7 | 8 | # API Reference 9 | How to use the `repltalk` lib for Python. The functions are pretty self explanatory but I've added a short description for each of them. 10 | *** 11 | > *The following functions are all coroutines unless specifically specified because asyncio is cool* 12 | 13 | ## Client 14 | `class repltalk.Client()` 15 | + `await login(username, password)` 16 | Logs in to Repl.it with your username and password. Your bot must be verified in order to use this function. 17 | + `await get_post(post_id)` 18 | Gets the post with that id. 19 | *returns Post* 20 | + `await get_comment(comment_id)` 21 | Gets the comment with that id. 22 | *returns Comment* 23 | + `await post_exists(post_id)` 24 | Returns whether or not the post exists. 25 | + `await get_leaderboard(limit=30)` 26 | Gets the top users from the Repl Talk leaderboard. 27 | *returns list of `User`s* 28 | + `await get_all_comments()` 29 | Gets all the recent comments from Repl Talk. 30 | *returns list of `Comment`s 31 | + `await get_user(username)` 32 | Gets the user with that username. 33 | *returns User* 34 | + `await get_reports(resolved=False)` 35 | Gets a list of reports. Only works for moderators or admins. See *Report List* 36 | + `boards` 37 | See *Board*. 38 | 39 | *** 40 | ## Board 41 | `class client.boards` 42 | *** 43 | + `all` 44 | The *All* board on Repl Talk. 45 | + `share` 46 | The *Share* board on Repl Talk. 47 | + `ask` 48 | The *Ask* board on Repl Talk. 49 | + `announcements` 50 | The *Announcements* board on Repl Talk. 51 | + `challenge` 52 | The *Challenge* board on Repl Talk. 53 | + `learn` 54 | The *Learn* board on Repl Talk. 55 | *** 56 | + `async for post in get_posts(sort='top', search='')` 57 | Gets the most recent posts from that board. 58 | Sort is the sorting order (top|hot|new) and search is the search query. 59 | *returns AsyncPostList* 60 | ### RichBoard 61 | A board that contains all the information from *Board*, and more. 62 | You can get this by doing `await client.boards.get(board_name)` (NOT YET ADDED) 63 | + `name` 64 | The name of the board. 65 | + `title_cta` 66 | Title call to action 67 | + `body_cta` 68 | Body call to action 69 | + `button_cta` 70 | Button call to action 71 | + `repl_required` 72 | Whether a Repl is required to be submitted. 73 | + 74 | 75 | *** 76 | ## Post 77 | + `id` 78 | The post ID. 79 | + `title` 80 | The post title. 81 | + `content` 82 | The post content. 83 | + `board` 84 | The board the post was made on. 85 | + `votes` 86 | The amount of upvotes the post has. 87 | + `author` 88 | The post author. Will be a User object. 89 | + `timestamp` 90 | The time the post was created at. (datetime.datetime object) 91 | + `url` 92 | The post url in Repl Talk. 93 | + `repl` 94 | The repl attached to the post. 95 | + `language` 96 | The *Language* that the Repl attached to the post uses. 97 | + `show_hosted` 98 | Indicates whether the post has a hosted repl linked to it. 99 | + `is_announcement` 100 | Whether the post is marked as an announcement. 101 | + `pinned` 102 | Whether the post has been pinned to the top of the board. 103 | + `can_edit` 104 | Indicates if the user can edit the post. This will be *False* unless you created the post. 105 | + `can_comment` 106 | If the user can comment on the post. 107 | + `can_vote` 108 | Indicates if the user can upvote the post. 109 | + `has_voted` 110 | Indicates if the user has already voted on the post. 111 | + `is_locked` 112 | Indicates if the post is locked. 113 | + `can_answer` 114 | Whether or not the user can answer the post. 115 | + `answered` 116 | If the post has been answered (will always be False if it's not a question). 117 | + `comment_count` 118 | The amount of comments the post has 119 | + `await get_comments()` 120 | Gets the comments on the post. 121 | + `await post_comment(content)` 122 | Posts a comment on the post. 123 | + `await report(reason)` 124 | Report the post 125 | + `await delete()` 126 | Delete the Post 127 | 128 | 129 | *** 130 | ## Comment 131 | + `id` 132 | The comment ID. 133 | + `content` 134 | The comment body. 135 | + `timestamp` 136 | The time the comment was created at. (datetime.datetime object) 137 | + `can_edit` 138 | Indicates if the user can edit the comment. 139 | + `can_comment` 140 | Whether or not the user can post a comment. 141 | + `url` 142 | The comment's url. 143 | + `votes` 144 | Gets the amount of upvotes the comment has. 145 | + `can_vote` 146 | Indicates if the user can vote on the comment. 147 | + `has_voted` 148 | Indicates if the user has already upvoted the post. 149 | + `author` 150 | The *User* for the author of the post. 151 | + `post` 152 | The post that the comment was made on. 153 | + `replies` 154 | A list of replies that the comment received. 155 | + `parent` 156 | The parent comment, if any. 157 | + `await reply(content)` 158 | Replies to the comment with the content. 159 | + `await report(reason)` 160 | Report the comment 161 | + `await delete()` 162 | Delete the comment 163 | 164 | *** 165 | ## User 166 | + `id` 167 | The user ID. Pretty useless since you can't get the user from their id. 168 | + `name` 169 | The user's username. 170 | + `avatar` 171 | The user's avatar url. 172 | + `url` 173 | The user's profile link. 174 | + `cycles` 175 | The amount of cycles/karma that user has. 176 | + `roles` 177 | The roles the user has set on their profile. 178 | + `bio` 179 | The short description written by a user on their profile. 180 | + `first_name` 181 | What the user set as their first name in their profile 182 | + `last_name` 183 | What the user set as their last name in their profile 184 | + `languages` 185 | The *Language*s that the user uses most often. 186 | + `timestamp` 187 | The time when the user account was created. (datetime.datetime object) 188 | + `is_hacker` 189 | Whether the user has the hacker plan 190 | + `await get_comments(limit=30, order='new')` 191 | Get a list of up to 1100 of the users comments. See *Comment* 192 | + `await get_posts(limit=30, order='new')` 193 | Get a list of up to 100 of the user's posts. See *Post* 194 | + `await ban(reason)` 195 | Ban the user 196 | 197 | 198 | *** 199 | ## PostList/AsyncPostList 200 | Acts like a normal list, except you can iterate over it 201 | + `await next()` 202 | Gets the next page of posts. Not present in *AsyncPostList* because it's done automatically. 203 | + `board` 204 | Gets the board of the repls it's getting from 205 | 206 | *** 207 | ## Repl 208 | + `id` 209 | The Repl ID. 210 | + `embed_url` 211 | The url for embedding the Repl on a web page. 212 | + `url` 213 | The url of the Repl. 214 | + `title` 215 | The title of the Repl. 216 | + `language` 217 | The *Language* of the Repl. 218 | 219 | *** 220 | ## Language 221 | Represents a programming language on Repl.it. 222 | + `id` 223 | Gets the ID of the language (ie python3). 224 | + `display_name` 225 | Gets the display name of the language (ie Python). 226 | + `icon` 227 | Gets the url for the language icon. 228 | + `category` 229 | Gets the category that the language is listed as. 230 | + `is_new` 231 | Whether the language was recently added to Repl.it. 232 | + `tagline` 233 | A short description of the language. 234 | 235 | *** 236 | ## Report List 237 | List of reports. *see Report* If linked post/comment is deleted is lazy report, *See lazyReport* 238 | + `for report in get_reports` 239 | Cycles through the reports, with lazy posts/comments. 240 | + `async for report in get_reports` 241 | Cycles through the reports with full posts, if there is a post. 242 | 243 | *** 244 | ## Report 245 | A report on a comment or post 246 | + `id` 247 | The report id 248 | + `type` 249 | The type of the report. (`'post'` or `'comment'`) 250 | + `reason` 251 | Why the report was made 252 | + `timestamp` 253 | When the report was created 254 | + `creator` 255 | Who created the report 256 | + `await get_attached()` 257 | Get the attached post/comment 258 | 259 | *** 260 | ## Lazy Report 261 | A less complete report 262 | + `id` 263 | The report id 264 | + `reason` 265 | Why the report was made 266 | + `creator` 267 | Who created the report 268 | 269 | *** 270 | ## Lazy Post 271 | A less complete post 272 | + `url` 273 | The url to the post 274 | + `id` 275 | The post's id 276 | + `author` 277 | The post's author 278 | + `content` 279 | The post's content 280 | + `title` 281 | The post's title 282 | + `await delete()` 283 | Delete the post 284 | + `await get_full_post()` 285 | Returns the full post 286 | 287 | *** 288 | ## Lazy Comment 289 | A less complete comment 290 | + `url` 291 | The url to the comment 292 | + `id` 293 | The comment's id 294 | + `author` 295 | The comment's author 296 | + `content` 297 | The comment's content 298 | + `await delete()` 299 | Delete the comment 300 | + `await get_full_comment()` 301 | Returns the full comment 302 | 303 | 304 | -------------------------------------------------------------------------------- /repltalk/queries.py: -------------------------------------------------------------------------------- 1 | from repltalk import graphql 2 | 3 | # query: query post($id: Int!, $after: String) { 4 | # post(id: $id) { 5 | # ...PostVotesVotes 6 | # __typename 7 | # } 8 | # } 9 | 10 | # fragment PostVotesVotes on Post { 11 | # id 12 | # voteCount 13 | # votes(after: $after) { 14 | # items { 15 | # id 16 | # user { 17 | # ...DepreciatedUserLabelWithImageUser 18 | # __typename 19 | # } 20 | # __typename 21 | # } 22 | # pageInfo { 23 | # hasNextPage 24 | # nextCursor 25 | # __typename 26 | # } 27 | # __typename 28 | # } 29 | # __typename 30 | # } 31 | 32 | # fragment DepreciatedUserLabelWithImageUser on User { 33 | # id 34 | # image 35 | # ...DepreciatedUserLabelUser 36 | # __typename 37 | # } 38 | 39 | # fragment DepreciatedUserLabelUser on User { 40 | # id 41 | # image 42 | # username 43 | # url 44 | # karma 45 | # __typename 46 | # } 47 | 48 | class Queries: 49 | 'There are all the graphql strings used' 50 | language_attributes = graphql.Field(( 51 | 'id', 52 | 'displayName', 53 | 'key', 54 | 'category', 55 | 'tagline', 56 | 'icon', 57 | 'isNew' 58 | )) 59 | language_field = graphql.Field({ 60 | 'lang': language_attributes 61 | }) 62 | languages_field = graphql.Field({ 63 | 'languages': language_attributes 64 | }) 65 | 66 | user_attributes = graphql.Field(( 67 | 'id', 68 | 'username', 69 | 'url', 70 | 'image', 71 | 'karma', 72 | 'firstName', 73 | 'lastName', 74 | 'fullName', 75 | 'displayName', 76 | 'isLoggedIn', 77 | 'bio', 78 | 'timeCreated', 79 | 'isHacker', 80 | languages_field, 81 | graphql.Field({'roles': ('id', 'name', 'key', 'tagline')}), 82 | )) 83 | user_field = graphql.Field({'user': user_attributes}) 84 | repl_attributes = ( 85 | 'id', 86 | graphql.Alias( 87 | 'embedUrl', 88 | graphql.Field('url', args={'lite': 'true'}) 89 | ), 90 | 'hostedUrl', 91 | 'title', 92 | language_field, 93 | 'language', 94 | 'timeCreated' 95 | ) 96 | repl_field = graphql.Field({ 97 | 'repl': repl_attributes 98 | }) 99 | board_field = graphql.Field({ 100 | 'board': ( 101 | 'id', 102 | 'url', 103 | 'slug', 104 | 'cta', 105 | 'titleCta', 106 | 'bodyCta', 107 | 'buttonCta', 108 | 'description', 109 | 'name', 110 | 'replRequired', 111 | 'isLocked', 112 | 'isPrivate' 113 | ) 114 | }) 115 | 116 | def connection_generator(attributes): 117 | return graphql.Field({ 118 | 'pageInfo': ( 119 | 'hasNextPage', 120 | # 'hasPreviousPage', 121 | 'nextCursor', 122 | # 'previousCursor' 123 | ), 124 | 'items': attributes 125 | }) 126 | 127 | comment_attributes = graphql.Field(( 128 | 'id', 129 | 'body', 130 | 'voteCount', 131 | 'timeCreated', 132 | 'timeUpdated', 133 | user_field, 134 | 'url', 135 | {'post': 'id'}, 136 | {'parentComment': 'id'}, 137 | {'comments': 'id'}, 138 | 'isAuthor', 139 | 'canEdit', 140 | 'canVote', 141 | 'canComment', 142 | 'hasVoted', 143 | 'canReport', 144 | 'hasReported', 145 | 'isAnswer', 146 | 'canSelectAsAnswer', 147 | 'canUnselectAsAnswer', 148 | graphql.Field( 149 | 'preview', 150 | args={ 151 | 'removeMarkdown': 'true', 152 | 'length': 150 153 | } 154 | ) 155 | )) 156 | 157 | comment_connection_field = graphql.Field( 158 | 'comments', 159 | args={ 160 | 'after': '$after', 161 | 'count': '$count', 162 | 'order': '$order' 163 | }, 164 | data=connection_generator(comment_attributes) 165 | ) 166 | 167 | post_vote_connection = graphql.Field( 168 | graphql.Field( 169 | 'votes', 170 | args={ 171 | 'before': '$votesBefore', 172 | 'after': '$votesAfter', 173 | 'count': '$votesCount', 174 | 'order': '$votesOrder', 175 | 'direction': '$votesDirection' 176 | }, 177 | data=connection_generator(( 178 | 'id', 179 | {'user': 'id'}, 180 | {'post': 'id'} 181 | )) 182 | ) 183 | ) 184 | 185 | post_attributes = graphql.Field(( 186 | 'id', 187 | 'title', 188 | 'body', 189 | 'showHosted', 190 | 'voteCount', 191 | 'commentCount', 192 | 'isPinned', 193 | 'isLocked', 194 | 'timeCreated', # datetime 195 | 'timeUpdated', # datetime 196 | 'url', 197 | user_field, # User 198 | board_field, # Board 199 | repl_field, # Repl 200 | comment_connection_field, 201 | post_vote_connection, 202 | 'isAnnouncement', 203 | 'isAuthor', 204 | 'canEdit', 205 | 'canComment', 206 | 'canVote', 207 | 'canPin', 208 | 'canSetType', 209 | 'canChangeBoard', 210 | 'canLock', 211 | 'hasVoted', 212 | 'canReport', 213 | 'hasReported', 214 | 'isAnswered', 215 | 'isAnswerable', 216 | {'answeredBy': user_attributes}, 217 | {'answer': comment_attributes}, 218 | graphql.Field( 219 | 'preview', 220 | args={ 221 | 'removeMarkdown': 'true', 222 | 'length': 150 223 | } 224 | ) 225 | 226 | )) 227 | 228 | comment_connection_field = graphql.Field(( 229 | connection_generator(comment_attributes) 230 | )) 231 | 232 | comment_detail_comment_fragment = graphql.Fragment( 233 | 'CommentDetailComment', 234 | 'Comment', 235 | comment_attributes 236 | ) 237 | get_post = graphql.Query( 238 | 'post', 239 | { 240 | '$id': 'Int!', 241 | '$count': 'Int', 242 | '$order': 'String', 243 | '$after': 'String', 244 | '$votesBefore': 'String', 245 | '$votesAfter': 'String', 246 | '$votesCount': 'Int', 247 | '$votesOrder': 'String', 248 | '$votesDirection': 'String', 249 | }, 250 | graphql.Field( 251 | args={'id': '$id'}, 252 | data={ 253 | 'post': post_attributes 254 | } 255 | ) 256 | ) 257 | get_comment = graphql.Query( 258 | 'comment', 259 | { 260 | '$id': 'Int!' 261 | }, 262 | graphql.Field( 263 | args={'id': '$id'}, 264 | data={ 265 | 'comment': comment_attributes 266 | } 267 | ) 268 | ) 269 | get_leaderboard = graphql.Query( 270 | 'leaderboard', {'$after': 'String'}, 271 | { 272 | graphql.Field(args={'after': '$after'}, data={ 273 | 'leaderboard': connection_generator(user_attributes) 274 | }) 275 | } 276 | ) 277 | 278 | delete_post = ''' 279 | mutation deletePost($id: Int!) { 280 | deletePost(id: $id) { 281 | id 282 | __typename 283 | } 284 | } 285 | ''' 286 | 287 | delete_comment = ''' 288 | mutation deleteComment($id: Int!) { 289 | deleteComment(id: $id) { 290 | id 291 | __typename 292 | } 293 | } 294 | ''' 295 | 296 | # get_all_posts = f''' 297 | # query posts($order: String, $after: String, $searchQuery: String) {{ 298 | # posts(order: $order, after: $after, searchQuery: $searchQuery) {{ 299 | # pageInfo {{ 300 | # nextCursor 301 | # }} 302 | # items {{ 303 | # {post_field} 304 | # }} 305 | # }} 306 | # }} 307 | # ''' 308 | posts_feed = graphql.Query( 309 | 'ReplPostsFeed', 310 | { 311 | "$options": "ReplPostsQueryOptions", 312 | '$count': 'Int', 313 | '$order': 'String', 314 | '$after': 'String', 315 | '$votesBefore': 'String', 316 | '$votesAfter': 'String', 317 | '$votesCount': 'Int', 318 | '$votesOrder': 'String', 319 | '$votesDirection': 'String', 320 | }, 321 | graphql.Field( 322 | name='replPosts', 323 | args={ 324 | 'options': '$options' 325 | }, 326 | data=connection_generator(post_attributes) 327 | ) 328 | ) 329 | get_comments = graphql.Query( 330 | 'post', 331 | {'$id': 'Int!', '$commentsOrder': 'String', '$commentsAfter': 'String'}, 332 | { 333 | graphql.Field('post', args={'id': '$id'}, data={ 334 | graphql.Field( 335 | 'comments', 336 | args={'order': '$commentsOrder', 'after': '$commentsAfter'}, 337 | data={ 338 | 'pageInfo': 'nextCursor', 339 | 'items': ( 340 | comment_detail_comment_fragment, 341 | { 342 | 'comments': comment_detail_comment_fragment 343 | } 344 | ) 345 | } 346 | ) 347 | }) 348 | }, 349 | fragments=[comment_detail_comment_fragment] 350 | ) 351 | 352 | get_user = graphql.Query( 353 | 'userByUsername', 354 | {'$username': 'String!'}, 355 | graphql.Alias( 356 | 'user', 357 | graphql.Field( 358 | {'userByUsername': user_attributes}, 359 | args={'username': '$username'} 360 | ) 361 | ) 362 | ) 363 | get_user_by_id = graphql.Query( 364 | 'user', 365 | {'$user_id': 'Int!'}, 366 | graphql.Alias( 367 | 'user', 368 | graphql.Field( 369 | {'user': user_attributes}, 370 | args={'id': '$user_id'} 371 | ) 372 | ) 373 | 374 | ) 375 | 376 | # query: query userByUsername($username: String!, $pinnedReplsFirst: Boolean, $count: Int, $after: String, $before: String, $direction: String, $order: String) { 377 | # user: userByUsername(username: $username) { 378 | # id 379 | # username 380 | # firstName 381 | # displayName 382 | # isLoggedIn 383 | # repls: publicRepls(pinnedReplsFirst: $pinnedReplsFirst, count: $count, after: $after, before: $before, direction: $direction, order: $order) { 384 | # items { 385 | # id 386 | # timeCreated 387 | # pinnedToProfile 388 | # ...ProfileReplItemRepl 389 | # __typename 390 | # } 391 | # pageInfo { 392 | # hasNextPage 393 | # nextCursor 394 | # } 395 | # } 396 | # } 397 | # } 398 | 399 | 400 | 401 | get_user_repls = graphql.Query( 402 | 'user', 403 | { 404 | '$user_id': 'Int!', 405 | '$pinnedFirst': 'Boolean', 406 | '$showUnnamed': 'Boolean', 407 | '$before': 'String', 408 | '$after': 'String', 409 | '$count': 'Int', 410 | '$order': 'String', 411 | '$direction': 'String' 412 | }, 413 | graphql.Field('user', args={'id': '$user_id'}, 414 | data={ 415 | graphql.Field( 416 | 'publicRepls', 417 | args={ 418 | 'pinnedReplsFirst': '$pinnedFirst', 419 | 'showUnnamed': '$showUnnamed', 420 | 'before': '$before', 421 | 'after': '$after', 422 | 'count': '$count', 423 | 'order': '$order', 424 | 'direction': '$direction' 425 | }, 426 | data={ 427 | 'pageInfo': ( 428 | 'nextCursor', 429 | 'hasNextPage' 430 | ), 431 | 'items': ( 432 | repl_attributes 433 | ) 434 | } 435 | ) 436 | }) 437 | ) 438 | 439 | # query ProfileComments($username: String!, $after: String, $order: String) { 440 | # user: userByUsername(username: $username) { 441 | # id 442 | # displayName 443 | # comments(after: $after, order: $order) { 444 | # items { 445 | # id 446 | # ...ProfileCommentsComment 447 | # __typename 448 | # } 449 | # pageInfo { 450 | # nextCursor 451 | # __typename 452 | # } 453 | # __typename 454 | # } 455 | # __typename 456 | # } 457 | # } 458 | 459 | get_user_comments = graphql.Query( 460 | 'ProfileComments', 461 | {'$user_id': 'Int!', '$order': 'String', '$after': 'String', '$count': 'Int'}, 462 | graphql.Field( 463 | 'user', 464 | args={'id': '$user_id'}, 465 | data={ 466 | graphql.Field( 467 | 'comments', 468 | args={'order': '$order', 'after': '$after', 'count': '$count'}, 469 | data=connection_generator(comment_attributes) 470 | ) 471 | } 472 | ) 473 | ) 474 | 475 | get_user_posts = graphql.Query( 476 | 'user', 477 | { 478 | '$user_id': 'Int!', 479 | '$order': 'String', 480 | '$after': 'String', 481 | '$count': 'Int', 482 | '$votesBefore': 'String', 483 | '$votesAfter': 'String', 484 | '$votesCount': 'Int', 485 | '$votesOrder': 'String', 486 | '$votesDirection': 'String' 487 | }, 488 | graphql.Field( 489 | 'user', 490 | args={'id': '$user_id'}, 491 | data={ 492 | graphql.Field( 493 | 'posts', 494 | args={ 495 | 'order': '$order', 496 | 'after': '$after', 497 | 'count': '$count' 498 | }, 499 | data={ 500 | 'pageInfo': 'nextCursor', 501 | 'items': ( 502 | post_attributes 503 | ) 504 | } 505 | ) 506 | } 507 | ) 508 | ) 509 | 510 | ban_user = ''' 511 | mutation Mutation($user: String!, $reason: String) { 512 | clui { 513 | moderator { 514 | user { 515 | ban(user: $user, reason: $reason) { 516 | ...CluiOutput 517 | __typename 518 | } 519 | __typename 520 | } 521 | __typename 522 | } 523 | __typename 524 | } 525 | } 526 | 527 | fragment CluiOutput on CluiOutput { 528 | ... on CluiSuccessOutput { 529 | message 530 | json 531 | __typename 532 | } 533 | ... on CluiErrorOutput { 534 | error 535 | json 536 | __typename 537 | } 538 | ... on CluiMarkdownOutput { 539 | markdown 540 | __typename 541 | } 542 | ... on CluiTableOutput { 543 | columns { 544 | label 545 | key 546 | __typename 547 | } 548 | rows 549 | __typename 550 | } 551 | __typename 552 | } 553 | ''' 554 | 555 | 556 | report_attributes = graphql.Field( 557 | {'creator': user_attributes}, 558 | 'id', 559 | 'reason', 560 | 'resolved', 561 | 'timeCreated', 562 | 'type', 563 | {'post': graphql.Field('url', 'body', 'id', {'user': user_attributes}, 'title')}, 564 | {'comment': graphql.Field( 565 | 'url', 566 | 'body', 567 | {'post': 'id'}, 568 | {'user': user_attributes}, 569 | 'id' 570 | )} 571 | ) 572 | resolve_report = graphql.Mutation( 573 | 'resolveBoardReport', 574 | { 575 | '$id': 'Int!' 576 | }, 577 | graphql.Field( 578 | 'resolveBoardReport', 579 | args={'id': '$id'}, 580 | data=report_attributes 581 | ) 582 | ) 583 | get_reports = graphql.Query( 584 | 'boardReports', 585 | { 586 | '$unresolvedOnly': 'Boolean!' 587 | }, 588 | 589 | graphql.Field( 590 | 'boardReports', 591 | args={'unresolvedOnly': '$unresolvedOnly'}, 592 | data=report_attributes 593 | ) 594 | ) 595 | get_lazy_reports = graphql.Query( 596 | 'boardReports', 597 | { 598 | '$unresolvedOnly': 'Boolean!' 599 | }, 600 | 601 | graphql.Field( 602 | 'boardReports', 603 | args={'unresolvedOnly': '$unresolvedOnly'}, 604 | data=graphql.Field( 605 | 'id', 606 | 'reason', 607 | {'creator': user_attributes} 608 | ) 609 | ) 610 | ) 611 | # get_comments = f''' 612 | # query post( 613 | # $id: Int!, $commentsOrder: String, $commentsAfter: String 614 | # ) {{ 615 | # post(id: $id) {{ 616 | # comments(order: $commentsOrder, after: $commentsAfter) {{ 617 | # pageInfo {{ 618 | # nextCursor 619 | # }} 620 | # items {{ 621 | # ...CommentDetailComment 622 | # comments {{ 623 | # ...CommentDetailComment 624 | # }} 625 | # }} 626 | # }} 627 | # }} 628 | # }} 629 | 630 | # fragment CommentDetailComment on Comment {{ 631 | # id 632 | # body 633 | # timeCreated 634 | # canEdit 635 | # canComment 636 | # canReport 637 | # hasReported 638 | # url 639 | # voteCount 640 | # canVote 641 | # hasVoted 642 | # {user_field} 643 | # }} 644 | 645 | # ''' 646 | # query comments($after: String, $order: String) { 647 | # comments(after: $after, order: $order) { 648 | # items { 649 | # id 650 | # } 651 | # } 652 | # } 653 | get_all_comments = ''' 654 | query comments($after: String, $order: String) {{ 655 | comments(after: $after, order: $order) {{ 656 | items {{ 657 | id 658 | body 659 | user { 660 | username 661 | id 662 | } 663 | comments {{ 664 | id 665 | body 666 | user { 667 | username 668 | id 669 | } 670 | }} 671 | }} 672 | pageInfo {{ 673 | hasNextPage 674 | nextCursor 675 | }} 676 | }} 677 | }} 678 | ''' 679 | 680 | post_exists = graphql.Query('post', {'$id': 'Int!'}, { 681 | graphql.Field('post', args={'id': '$id'}, data='id') 682 | }) 683 | 684 | 685 | # query ProfilePosts($username: String!, $after: String, $order: String, $count: Int) { user: userByUsername(username: $username) { id displayName posts(after: $after, order: $order, count: $count) { items { id ...PostsFeedItemPost board { id name url slug color __typename } __typename } pageInfo { nextCursor __typename } __typename } __typename }}fragment PostsFeedItemPost on Post { id title preview(removeMarkdown: true, length: 150) url commentCount isPinned isLocked isAnnouncement timeCreated isAnswered isAnswerable ...PostVoteControlPost ...PostLinkPost user { id username isHacker image isModerator: hasRole(role: MODERATOR) isAdmin: hasRole(role: ADMIN) ...UserLabelUser ...UserLinkUser __typename } repl { id lang { id icon key displayName tagline __typename } __typename } board { id name slug url color __typename } recentComments(count: 3) { id ...SimpleCommentComment __typename } __typename}fragment PostVoteControlPost on Post { id voteCount canVote hasVoted __typename}fragment PostLinkPost on Post { id url __typename}fragment UserLabelUser on User { id username karma ...UserLinkUser __typename}fragment UserLinkUser on User { id url username __typename}fragment SimpleCommentComment on Comment { id user { id isModerator: hasRole(role: MODERATOR) isAdmin: hasRole(role: ADMIN) ...UserLabelUser ...UserLinkUser __typename } preview(removeMarkdown: true, length: 500) timeCreated __typename} 686 | profile_posts = graphql.Query('ProfilePosts', { 687 | '$username': 'String!', 688 | '$after': 'String', 689 | '$order': 'String', 690 | '$count': 'Int', 691 | }, { 692 | }) 693 | create_report = graphql.Mutation( 694 | 'createBoardReport', 695 | { 696 | '$postId': 'Int', 697 | '$commentId': 'Int', 698 | '$reason': 'String!' 699 | }, 700 | graphql.Field( 701 | { 702 | 'createBoardReport': 703 | ( 704 | 'id', 705 | ) 706 | }, 707 | args={ 708 | 'postId': '$postId', 709 | 'commentId': '$commentId', 710 | 'reason': '$reason' 711 | }, 712 | ) 713 | ) 714 | # create_report_2 = ''' 715 | # mutation createBoardReport() { 716 | # createBoardReport(postId: $postId, $commentId: Int, $reason: string!) { 717 | # id 718 | # } 719 | # } 720 | # ''' 721 | create_post = ''' 722 | mutation createPost($input: CreatePostInput!) { 723 | createPost(input: $input) { 724 | post { 725 | id 726 | url 727 | showHosted 728 | board { 729 | id 730 | name 731 | slug 732 | url 733 | replRequired 734 | template 735 | } 736 | } 737 | } 738 | } 739 | ''' 740 | 741 | create_comment = ''' 742 | mutation createComment($input: CreateCommentInput!) 743 | { createComment(input: $input) 744 | { 745 | comment 746 | { id ...CommentDetailComment comments 747 | { id ...CommentDetailComment __typename } 748 | parentComment { id __typename } __typename 749 | } __typename 750 | } 751 | } 752 | fragment CommentDetailComment on Comment { 753 | id 754 | body 755 | timeCreated 756 | canEdit 757 | canComment 758 | canReport 759 | hasReported 760 | url 761 | canSelectAsAnswer 762 | canUnselectAsAnswer 763 | isAnswer 764 | ...CommentVoteControlComment 765 | user { 766 | id 767 | username 768 | ...DepreciatedUserLabelWithImageUser 769 | __typename 770 | } 771 | post { id isAnswerable __typename } 772 | ...EditCommentComment __typename 773 | } 774 | fragment DepreciatedUserLabelWithImageUser on User { 775 | id 776 | image 777 | ...DepreciatedUserLabelUser 778 | __typename 779 | } 780 | fragment DepreciatedUserLabelUser on User { 781 | id 782 | image 783 | username 784 | url 785 | karma __typename 786 | } 787 | fragment CommentVoteControlComment on Comment { 788 | id 789 | voteCount 790 | canVote 791 | hasVoted 792 | __typename 793 | } 794 | fragment EditCommentComment on Comment { 795 | id 796 | parentComment 797 | { id __typename } 798 | post { id __typename } 799 | __typename } 800 | ''' 801 | -------------------------------------------------------------------------------- /repltalk/__init__.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | from datetime import datetime 3 | from repltalk.queries import Queries 4 | import warnings 5 | 6 | # Bots approved by the Repl.it Team (or me) that are allowed to log in 7 | # (100% impossible to hack yes definitely) 8 | whitelisted_bots = { 9 | 'repltalk', 10 | 'admin@allseeingbot.com', 11 | 'r4idtheweb', 12 | 'replpedia', 13 | 'codingcactus' 14 | } 15 | 16 | base_url = 'https://replit.com' 17 | 18 | 19 | class ReplTalkException(Exception): pass 20 | 21 | 22 | class NotWhitelisted(ReplTalkException): pass 23 | 24 | 25 | class AlreadyReported(ReplTalkException): pass 26 | 27 | 28 | class BoardDoesntExist(ReplTalkException): pass 29 | 30 | 31 | class GraphqlError(ReplTalkException): pass 32 | 33 | 34 | class DeletedError(ReplTalkException): pass 35 | 36 | 37 | class InvalidLogin(ReplTalkException): pass 38 | 39 | 40 | class PostNotFound(ReplTalkException): pass 41 | 42 | 43 | class Repl(): 44 | __slots__ = ('id', 'embed_url', 'url', 'title', 'language') 45 | 46 | def __init__( 47 | self, data 48 | ): 49 | # Grab the attributes from the data object 50 | self.id = data['id'] 51 | self.embed_url = data['embedUrl'] 52 | self.url = data['hostedUrl'] 53 | self.title = data['title'] 54 | self.language = Language( 55 | data['lang'] 56 | ) 57 | 58 | def __repr__(self): 59 | return f'<{self.title}>' 60 | 61 | def __eq__(self, repl2): 62 | return self.id == repl2.id 63 | 64 | def __hash__(self): 65 | return hash((self.id, self.url, self.title)) 66 | 67 | 68 | # A LazyPost contains more limited information about the post 69 | # so it doesn't include things like comments 70 | class LazyPost(): 71 | __slots__ = ('client', 'data', 'url', 'id', 'content', 'author', 'title') 72 | 73 | def __init__(self, client, data): 74 | self.client = client 75 | self.data = data 76 | 77 | self.url = data['url'] 78 | self.id = data['id'] 79 | self.content = data['body'] 80 | self.author = User(client, data['user']) 81 | self.title = data['title'] 82 | 83 | async def delete(self): 84 | client = self.client 85 | r = await client.perform_graphql( 86 | 'deletePost', 87 | Queries.delete_post, 88 | id=self.id, 89 | ) 90 | return r 91 | 92 | async def get_full_post(self): 93 | return await self.client.get_post(self.id) 94 | 95 | 96 | # A LazyComment contains limited information about the comment, 97 | # so it doesn't include things like replies 98 | class LazyComment(): 99 | def __init__(self, client, data): 100 | self.client = client 101 | self.url = data['url'] 102 | self.id = data['id'] 103 | self.content = data['body'] 104 | self.author = User(client, data['user']) 105 | 106 | async def delete(self): 107 | client = self.client 108 | r = await client.perform_graphql( 109 | 'deleteComment', 110 | Queries.delete_comment, 111 | id=self.id, 112 | ) 113 | return r 114 | 115 | async def get_full_comment(self): 116 | return await self.client.get_comment(self.id) 117 | 118 | 119 | class Report: 120 | __slots__ = ( 121 | 'id', 'reason', 'resolved', 'timestamp', 122 | 'creator', 'post', 'type', 'client', 'deleted' 123 | ) 124 | 125 | def __init__(self, client, data): 126 | 127 | self.id = data['id'] 128 | self.type = data['type'] 129 | self.reason = data['reason'] 130 | self.resolved = data['resolved'] 131 | self.timestamp = data['timeCreated'] # Should this be parsed? if so, fix test_report_get_attached test 132 | self.creator = User(client, data['creator']) 133 | self.client = client 134 | self.deleted = False 135 | # There are two types of reports, post reports and comment reports. 136 | if data['post']: 137 | self.type = 'post' 138 | self.post = LazyPost(client, data['post']) 139 | elif data['comment']: 140 | self.type = 'comment' 141 | 142 | self.post = LazyComment(client, data['comment']) 143 | else: 144 | raise DeletedError('Post is already deleted') 145 | 146 | def __str__(self): 147 | url = self.post.url 148 | if 'https://repl.it' in url: 149 | url = url.replace('https://repl.it', '') 150 | return f'' 151 | 152 | async def get_attached(self): 153 | if isinstance(self.post, (LazyPost, LazyComment)): 154 | if self.type == 'post': 155 | self.post = await self.post.get_full_post() 156 | else: 157 | self.post = await self.post.get_full_comment() 158 | return self.post 159 | 160 | async def resolve(self): 161 | await self.client._resolve_report(self.id) 162 | 163 | 164 | class LazyReport: 165 | __slots__ = ('client', 'id', 'reason', 'deleted', 'creator') 166 | 167 | def __init__(self, client, data): 168 | self.client = client 169 | self.id = data['id'] 170 | self.reason = data['reason'] 171 | self.creator = User(client, data['creator']) 172 | self.deleted = True 173 | 174 | def __str__(self): 175 | url = 'DELETED' 176 | return f'' 177 | 178 | async def get_attached(self): 179 | return self 180 | 181 | async def resolve(self): 182 | await self.client._resolve_report(self.id) 183 | 184 | async def get_full_comment(self): 185 | return await self.client._get_repor 186 | 187 | 188 | class ReportList(): 189 | __slots__ = ( 190 | 'client', 'refresh', 'resolved', 'reports', 'i' 191 | ) 192 | 193 | def __init__(self, client, data): 194 | self.reports = [] 195 | for r in data: 196 | try: 197 | self.reports.append(Report(client, r)) 198 | except DeletedError: 199 | self.reports.append(LazyReport(client, r)) 200 | 201 | def __iter__(self): 202 | self.i = 0 203 | return self 204 | 205 | def __next__(self): 206 | if self.i >= len(self.reports): 207 | raise StopIteration 208 | self.i += 1 209 | return self.reports[self.i - 1] 210 | 211 | def __aiter__(self): 212 | self.i = 0 213 | return self 214 | 215 | async def __anext__(self): 216 | if self.i >= len(self.reports): 217 | raise StopAsyncIteration 218 | self.i += 1 219 | await self.reports[self.i - 1].get_attached() 220 | return self.reports[self.i - 1] 221 | 222 | 223 | class AsyncPostList(): 224 | __slots__ = ( 225 | 'i', 'client', 'sort', 'search', 'after', 'limit', 'posts_queue', 'board' 226 | ) 227 | 228 | def __init__( 229 | self, client, board, limit=32, sort='new', search='', after=None 230 | ): 231 | self.i = 0 232 | self.client = client 233 | 234 | self.sort = sort 235 | self.search = search 236 | self.after = after 237 | 238 | self.limit = limit 239 | self.posts_queue = [] 240 | 241 | self.board = board 242 | 243 | def __aiter__(self): 244 | return self 245 | 246 | async def __anext__(self): 247 | if self.i >= self.limit: 248 | raise StopAsyncIteration 249 | if len(self.posts_queue) == 0: 250 | new_posts = await self.board._get_posts( 251 | sort=self.sort, 252 | search=self.search, 253 | after=self.after 254 | ) 255 | self.posts_queue.extend(new_posts['items']) 256 | self.after = new_posts['pageInfo']['nextCursor'] 257 | current_post_raw = self.posts_queue.pop(0) 258 | current_post = get_post_object(self.client, current_post_raw) 259 | 260 | self.i += 1 261 | 262 | return current_post 263 | 264 | def __await__(self): 265 | post_list = PostList( 266 | client=self.client, 267 | posts=[], 268 | board=self.board, 269 | after=self.after, 270 | sort=self.sort, 271 | search=self.search 272 | ) 273 | return post_list.next().__await__() 274 | 275 | 276 | class PostList(list): 277 | __slots__ = ('posts', 'after', 'board', 'sort', 'search', 'i', 'client') 278 | 279 | def __init__(self, client, posts, board, after, sort, search): 280 | self.posts = posts 281 | self.after = after 282 | self.board = board 283 | self.sort = sort 284 | self.search = search 285 | self.client = client 286 | warnings.warn( 287 | 'Doing await get_posts is deprecated, ' 288 | 'use async for post in get_posts instead.', 289 | DeprecationWarning 290 | ) 291 | 292 | def __iter__(self): 293 | self.i = 0 294 | return self 295 | 296 | def __next__(self): 297 | self.i += 1 298 | if self.i >= len(self.posts): 299 | raise StopIteration 300 | return self.posts[self.i] 301 | 302 | def __str__(self): 303 | if len(self.posts) > 30: 304 | return f'<{len(self.posts)} posts>' 305 | return str(self.posts) 306 | 307 | def __getitem__(self, indices): 308 | return self.posts[indices] 309 | 310 | async def next(self): 311 | new_posts = await self.board._get_posts( 312 | sort=self.sort, 313 | search=self.search, 314 | after=self.after 315 | ) 316 | posts = [ 317 | get_post_object(self.client, post_raw) for post_raw in new_posts['items'] 318 | ] 319 | self.after = new_posts['pageInfo']['nextCursor'] 320 | self.posts = posts 321 | return self 322 | 323 | def __eq__(self, postlist2): 324 | return self.board == postlist2.board 325 | 326 | def __ne__(self, postlist2): 327 | return self.board != postlist2.board 328 | 329 | 330 | class CommentList(list): 331 | __slots__ = ('post', 'comments', 'after', 'board', 'sort', 'search', 'i') 332 | 333 | def __init__(self, post, comments, board, after, sort): 334 | self.post = post 335 | self.comments = comments 336 | self.after = after 337 | self.board = board 338 | self.sort = sort 339 | 340 | def __iter__(self): 341 | self.i = 0 342 | return self 343 | 344 | def __next__(self): 345 | self.i += 1 346 | if self.i >= len(self.posts): 347 | raise StopIteration 348 | return self.comments[self.i] 349 | 350 | def __str__(self): 351 | if len(self.comments) > 30: 352 | return f'<{len(self.comments)} comments>' 353 | return str(self.comments) 354 | 355 | async def next(self): 356 | post_list = await self.board.comments( 357 | sort=self.sort, 358 | search=self.search, 359 | after=self.after 360 | ) 361 | self.comments = post_list.comments 362 | self.board = post_list.board 363 | self.after = post_list.after 364 | self.sort = post_list.sort 365 | self.search = post_list.search 366 | 367 | def __eq__(self, commentlist2): 368 | return self.post == commentlist2.post 369 | 370 | def __ne__(self, commentlist2): 371 | return self.post != commentlist2.post 372 | 373 | 374 | class Comment(): 375 | __slots__ = ( 376 | 'client', 'id', 'content', 'timestamp', 'can_edit', 'can_comment', 377 | 'can_report', 'has_reported', 'path', 'url', 'votes', 'can_vote', 378 | 'has_voted', 'author', 'post', 'replies', 'parent' 379 | ) 380 | 381 | def __init__( 382 | self, client, data, post, parent=None 383 | ): 384 | self.client = client 385 | self.id = data['id'] 386 | self.content = data.get('body') 387 | if self.content is None: 388 | return 389 | timestamp = data['timeCreated'] 390 | self.timestamp = datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%fZ') 391 | self.can_edit = data['canEdit'] 392 | self.can_comment = data['canComment'] 393 | self.can_report = data['canReport'] 394 | self.has_reported = data['hasReported'] 395 | self.path = data['url'] 396 | self.url = base_url + data['url'] 397 | self.votes = data['voteCount'] 398 | self.can_vote = data['canVote'] 399 | self.has_voted = data['hasVoted'] 400 | self.parent = parent 401 | user = data['user'] 402 | 403 | if user is not None: 404 | user = User( 405 | client, 406 | user=user 407 | ) 408 | self.author = user 409 | self.post = post # Should already be a post object 410 | 411 | raw_replies = data.get('comments', []) 412 | replies = [] 413 | 414 | for inner_reply in raw_replies: 415 | replies.append(Comment( 416 | self.client, 417 | data=inner_reply, 418 | post=self.post, 419 | parent=self 420 | )) 421 | self.replies = replies 422 | 423 | def __repr__(self): 424 | if len(self.content) > 100: 425 | return repr(self.content[:100] + '...') 426 | else: 427 | return repr(self.content) 428 | 429 | def __eq__(self, post2): 430 | return self.id == post2.id 431 | 432 | def __ne__(self, post2): 433 | return self.id != post2.id 434 | 435 | async def reply(self, content): 436 | c = await self.client.perform_graphql( 437 | 'createComment', 438 | Queries.create_comment, 439 | input={ 440 | 'body': content, 441 | 'commentId': self.id, 442 | 'postId': self.post.id 443 | } 444 | ) 445 | c = c['comment'] 446 | return Comment( 447 | self.client, 448 | data=c, 449 | post=self.post, 450 | parent=self 451 | ) 452 | 453 | async def report(self, reason): 454 | client = self.client 455 | r = await client.perform_graphql( 456 | 'createBoardReport', 457 | Queries.create_report, 458 | commentId=self.id, 459 | reason=reason, 460 | show_errors=False 461 | ) 462 | if not r: 463 | raise AlreadyReported('This comment has already been reported by this account.') 464 | return r 465 | 466 | async def delete(self): 467 | client = self.client 468 | r = await client.perform_graphql( 469 | 'deleteComment', 470 | Queries.delete_comment, 471 | id=self.id, 472 | ) 473 | return r 474 | 475 | def __hash__(self): 476 | return hash((self.id, self.content, self.votes, self.replies)) 477 | 478 | 479 | class Board(): 480 | __slots__ = ('client', 'name', ) 481 | 482 | def __init__(self, client): 483 | self.client = client 484 | 485 | async def _get_posts(self, sort, search, after): 486 | return await self.client._posts_in_board( 487 | board_slugs=[self.name], 488 | order=sort, 489 | search_query=search, 490 | after=after 491 | ) 492 | 493 | def get_posts(self, sort='top', search='', limit=32, after=None): 494 | if sort == 'top': 495 | sort = 'votes' 496 | return AsyncPostList( 497 | self.client, 498 | limit=limit, 499 | sort=sort, 500 | search=search, 501 | after=after, 502 | board=self 503 | ) 504 | 505 | async def create_post( # TODO 506 | self, title: str, content: str, repl: Repl = None, show_hosted=False 507 | ): 508 | pass 509 | 510 | def __repr__(self): 511 | return f'<{self.name} board>' 512 | 513 | def __hash__(self): 514 | return hash((self.name,)) 515 | 516 | 517 | class RichBoard(Board): # a board with more stuff than usual 518 | __slots__ = ( 519 | 'client', 'id', 'url', 'name', 'slug', 'title_cta', 'body_cta', 520 | 'button_cta', 'name', 'repl_required', 'data' 521 | ) 522 | 523 | def __init__( 524 | self, client, data 525 | ): 526 | self.client = client 527 | self.data = data 528 | 529 | self.id = data['id'] 530 | self.url = base_url + data['url'] 531 | self.name = data['name'] 532 | self.slug = data['slug'] 533 | self.body_cta = data['bodyCta'] 534 | self.title_cta = data['titleCta'] 535 | self.button_cta = data['buttonCta'] 536 | 537 | def __eq__(self, board2): 538 | return self.id == board2.id and self.name == board2.name 539 | 540 | def __ne__(self, board2): 541 | return self.id != board2.id or self.name != board2.name 542 | 543 | def __hash__(self): 544 | return hash(( 545 | self.id, 546 | self.name, 547 | self.slug, 548 | self.body_cta, 549 | self.title_cta, 550 | self.button_cta 551 | )) 552 | 553 | 554 | class Language(): 555 | __slots__ = ( 556 | 'id', 'display_name', 'key', 'category', 'is_new', 'icon', 'icon_path', 557 | 'tagline' 558 | ) 559 | 560 | def __init__( 561 | self, data 562 | ): 563 | self.id = data['id'] 564 | self.display_name = data['displayName'] 565 | self.key = data['key'] # identical to id??? 566 | self.category = data['category'] 567 | self.tagline = data['tagline'] 568 | self.is_new = data['isNew'] 569 | icon = data['icon'] 570 | self.icon_path = icon 571 | if icon and icon[0] == '/': 572 | icon = 'https://repl.it' + icon 573 | self.icon = icon 574 | 575 | def __str__(self): 576 | return self.display_name 577 | 578 | def __repr__(self): 579 | return f'<{self.id}>' 580 | 581 | def __eq__(self, lang2): 582 | return self.key == lang2.key 583 | 584 | def __ne__(self, lang2): 585 | return self.key != lang2.key 586 | 587 | def __hash__(self): 588 | return hash(( 589 | self.id, 590 | self.display_name, 591 | self.key, 592 | self.category, 593 | self.tagline, 594 | self.icon_path 595 | )) 596 | 597 | 598 | def get_post_object(client, post): 599 | return Post( 600 | client, data=post 601 | ) 602 | 603 | 604 | class Post(): 605 | __slots__ = ( 606 | 'client', 'id', 'title', 'content', 'is_announcement', 'path', 'url', 607 | 'board', 'timestamp', 'can_edit', 'can_comment', 'can_pin', 'can_set_type', 608 | 'can_report', 'has_reported', 'is_locked', 'show_hosted', 'votes', 609 | 'can_vote', 'has_voted', 'author', 'repl', 'answered', 'can_answer', 610 | 'pinned', 'comment_count', 'language', 'vote_list', 'data' 611 | ) 612 | 613 | def __init__( 614 | self, client, data 615 | ): 616 | self.client = client 617 | self.data = data 618 | 619 | self.id = data['id'] 620 | self.title = data['title'] 621 | self.content = data['body'] 622 | self.is_announcement = data['isAnnouncement'] 623 | self.path = data['url'] 624 | self.url = base_url + data['url'] 625 | board = RichBoard( 626 | client=client, 627 | data=data['board'] 628 | ) 629 | self.board = board 630 | timestamp = data['timeCreated'] 631 | self.timestamp = datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%fZ') 632 | self.can_edit = data['canEdit'] 633 | self.can_comment = data['canComment'] 634 | self.can_pin = data['canPin'] 635 | self.can_set_type = data['canSetType'] 636 | self.can_report = data['canReport'] 637 | self.has_reported = data['hasReported'] 638 | self.is_locked = data['isLocked'] 639 | self.show_hosted = data['showHosted'] 640 | self.votes = data['voteCount'] 641 | self.vote_list = data['votes']['items'] 642 | 643 | self.can_vote = data['canVote'] 644 | self.has_voted = data['hasVoted'] 645 | 646 | user = data['user'] 647 | if user is not None: 648 | user = User( 649 | client, 650 | user=user 651 | ) 652 | self.author = user 653 | 654 | repl = data['repl'] 655 | if repl is None: 656 | self.repl = None 657 | self.language = None 658 | else: 659 | self.repl = Repl(repl) 660 | self.language = self.repl.language 661 | self.answered = data['isAnswered'] 662 | self.can_answer = data['isAnswerable'] 663 | self.pinned = data['isPinned'] 664 | self.comment_count = data['commentCount'] 665 | 666 | def __repr__(self): 667 | return f'<{self.title}>' 668 | 669 | def __eq__(self, post2): 670 | return self.id == post2.id 671 | 672 | def __ne__(self, post2): 673 | return self.id != post2.id 674 | 675 | async def report(self, reason): 676 | client = self.client 677 | r = await client.perform_graphql( 678 | 'createBoardReport', 679 | Queries.create_report, 680 | postId=self.id, 681 | reason=reason, 682 | show_errors=False 683 | ) 684 | if not r: 685 | raise AlreadyReported('This post has already been reported by this account.') 686 | return r 687 | 688 | async def delete(self): 689 | client = self.client 690 | r = await client.perform_graphql( 691 | 'deletePost', 692 | Queries.delete_post, 693 | id=self.id, 694 | ) 695 | return r 696 | 697 | async def get_comments(self, order='new'): 698 | _comments = await self.client._get_comments( 699 | self.id, 700 | order 701 | ) 702 | comments = [] 703 | for c in _comments['comments']['items']: 704 | comments.append(Comment( 705 | self.client, 706 | data=c, 707 | post=self 708 | )) 709 | return comments 710 | 711 | async def post_comment(self, content): 712 | c = await self.client.perform_graphql( 713 | 'createComment', 714 | Queries.create_comment, 715 | input={ 716 | 'body': content, 717 | 'postId': self.id 718 | } 719 | ) 720 | c = c['comment'] 721 | return Comment( 722 | client=self.client, 723 | data=c, 724 | post=self, 725 | ) 726 | 727 | def __hash__(self): 728 | return hash(( 729 | self.id, 730 | self.title, 731 | self.content 732 | )) 733 | 734 | 735 | class User(): 736 | __slots__ = ( 737 | 'client', 'data', 'id', 'name', 'avatar', 'url', 'cycles', 'roles', 738 | 'full_name', 'first_name', 'last_name', 'is_logged_in', 739 | 'bio', 'is_hacker', 'languages', 'timestamp', 'data' 740 | ) 741 | 742 | def __init__( 743 | self, client, user 744 | ): 745 | self.client = client 746 | self.data = user 747 | 748 | self.id = user['id'] 749 | self.name = user['username'] 750 | self.avatar = user['image'] 751 | self.url = user['url'] 752 | self.cycles = user['karma'] 753 | self.roles = user['roles'] 754 | self.is_hacker = user['isHacker'] 755 | 756 | self.full_name = user['fullName'] 757 | self.first_name = user['firstName'] 758 | self.last_name = user['lastName'] 759 | 760 | time_created = user['timeCreated'] 761 | self.timestamp = datetime.strptime(time_created, '%Y-%m-%dT%H:%M:%S.%fZ') 762 | 763 | self.is_logged_in = user['isLoggedIn'] 764 | self.bio = user['bio'] 765 | # Convert all of the user's frequently used 766 | # languages into Language objects 767 | self.languages = [ 768 | Language(language) for language in user['languages'] 769 | ] 770 | 771 | async def get_comments(self, limit=30, order='new'): 772 | client = self.client 773 | _comments = await client._get_user_comments( 774 | self.id, 775 | limit, 776 | order 777 | ) 778 | comments = [] 779 | for c in _comments['comments']['items']: 780 | 781 | comments.append(Comment( 782 | client, 783 | data=c, 784 | post=c['post']['id'] 785 | )) 786 | 787 | return comments 788 | 789 | async def get_posts(self, limit=30, order='new'): 790 | client = self.client 791 | _posts = await client._get_user_posts( 792 | self.id, 793 | limit, 794 | order 795 | ) 796 | 797 | posts = [] 798 | for p in _posts['posts']['items']: 799 | 800 | posts.append(Post( 801 | client, 802 | data=p 803 | )) 804 | 805 | return posts 806 | 807 | async def get_repls( 808 | self, limit=30, 809 | pinned_first=False, 810 | before=None, 811 | after=None, 812 | direction=None 813 | ): 814 | repls = await self.client._get_user_repls( 815 | self, 816 | limit, 817 | pinned_first, before, 818 | after, direction, 819 | ) 820 | public_repls_data = repls['publicRepls'] 821 | 822 | repl_list_raw = public_repls_data['items'] 823 | 824 | repl_list = [Repl(r) for r in repl_list_raw] 825 | return repl_list 826 | 827 | async def ban(self, reason): 828 | client = self.client 829 | r = await client.perform_graphql( 830 | 'Mutation', 831 | Queries.ban_user, 832 | user=self.name, 833 | reason=reason, 834 | ) 835 | return r 836 | 837 | def __repr__(self): 838 | return f'<{self.name} ({self.cycles})>' 839 | 840 | def __eq__(self, user2): 841 | return self.id == user2.id 842 | 843 | def __ne__(self, user2): 844 | return self.id != user2.id 845 | 846 | def __hash__(self): 847 | return hash(( 848 | self.id, 849 | self.name, 850 | self.full_name, 851 | self.bio 852 | )) 853 | 854 | 855 | class Leaderboards(): 856 | __slots__ = ( 857 | 'limit', 'iterated', 'users', 'raw_users', 'next_cursor', 'client' 858 | ) 859 | 860 | def __init__(self, client, limit): 861 | self.limit = limit 862 | self.iterated = 0 863 | self.users = [] 864 | self.raw_users = [] 865 | self.next_cursor = None 866 | self.client = client 867 | 868 | def __await__(self): 869 | return self.load_all().__await__() 870 | 871 | def __aiter__(self): 872 | return self 873 | 874 | def __next__(self): 875 | raise NotImplementedError 876 | 877 | async def load_all(self): 878 | async for _ in self: _ 879 | return self.users 880 | 881 | async def __anext__(self): 882 | ended = len(self.users) + 1 > self.limit 883 | if self.iterated <= len(self.users) and not ended: 884 | self.iterated += 30 885 | leaderboard = await self.client._get_leaderboard( 886 | self.next_cursor 887 | ) 888 | self.next_cursor = leaderboard['pageInfo']['nextCursor'] 889 | self.raw_users.extend(leaderboard['items']) 890 | 891 | if ended: 892 | raise StopAsyncIteration 893 | user = self.raw_users[len(self.users)] 894 | user = User( 895 | self, 896 | user=user 897 | ) 898 | 899 | self.users.append(user) 900 | return user 901 | 902 | def __repr__(self): 903 | if self.iterated >= self.limit: 904 | return f'' 905 | return f'' 906 | 907 | def __str__(self): 908 | return self.__repr__() 909 | 910 | 911 | class Client(): 912 | __slots__ = ('default_ref', 'default_requested_with', 'sid', 'boards') 913 | 914 | def __init__(self): 915 | self.default_ref = base_url + '/@mat1/repl-talk-api' 916 | self.default_requested_with = 'ReplTalk' 917 | self.sid = None 918 | self.boards = self._boards(self) 919 | 920 | async def perform_graphql( 921 | self, 922 | operation_name, 923 | query, 924 | ignore_none=False, 925 | show_errors=True, 926 | **variables, 927 | ): 928 | payload = { 929 | 'operationName': operation_name, 930 | 'query': str(query) 931 | } 932 | if ignore_none: 933 | payload['variables'] = {q: variables[q] for q in variables if q is not None} 934 | else: 935 | payload['variables'] = variables 936 | 937 | async with aiohttp.ClientSession( 938 | cookies={'connect.sid': self.sid}, 939 | headers={ 940 | 'referer': self.default_ref, 941 | 'X-Requested-With': self.default_requested_with 942 | } 943 | ) as s: 944 | async with s.post( 945 | base_url + '/graphql', 946 | json=payload 947 | ) as r: 948 | data = await r.json() 949 | if 'data' in data: 950 | data = data['data'] 951 | if data is None: 952 | if show_errors: 953 | print('ERROR:', await r.json()) 954 | return None 955 | keys = data.keys() 956 | if len(keys) == 1: 957 | data = data[next(iter(keys))] 958 | if isinstance(data, list): 959 | 960 | try: 961 | loc = data[0]['locations'][0]['column'] 962 | print(str(query)[:loc-1] + '!!!!!' + str(query)[loc-1:]) 963 | except KeyError: 964 | pass 965 | return data 966 | 967 | async def login(self, username, password): 968 | if username.lower() not in whitelisted_bots: 969 | raise NotWhitelisted( 970 | f'{username} is not whitelisted and therefore is not allowed to log in.\n' 971 | 'Please ask mat#6207 if you would like to be added to the whitelist.' 972 | ) 973 | 974 | async with aiohttp.ClientSession( 975 | headers={'referer': self.default_ref} 976 | ) as s: 977 | async with s.post( 978 | base_url + '/login', 979 | json={ 980 | 'username': username, 981 | 'password': password, 982 | 'teacher': False 983 | }, 984 | headers={ 985 | 'X-Requested-With': username 986 | } 987 | ) as r: 988 | if await r.text() == '{"message":"Invalid username or password."}': 989 | raise InvalidLogin('Invalid username or password.') 990 | # Gets the connect.sid cookie 991 | connectsid = str(dict(r.cookies)['connect.sid'].value) 992 | self.sid = connectsid 993 | return self 994 | 995 | async def _get_reports(self, resolved): 996 | reports = await self.perform_graphql( 997 | 'boardReports', 998 | Queries.get_reports, 999 | unresolvedOnly=not resolved 1000 | ) 1001 | ids = [r['id'] for r in reports if 'id' in r] 1002 | lazy_reports = await self.perform_graphql( 1003 | 'boardReports', 1004 | Queries.get_lazy_reports, 1005 | unresolvedOnly=not resolved 1006 | ) 1007 | for lr in lazy_reports: 1008 | if lr['id'] not in ids: 1009 | reports.append(lr) 1010 | return reports 1011 | 1012 | async def get_reports(self, resolved=False): 1013 | raw_data = await self._get_reports(resolved) 1014 | return ReportList(self, raw_data) 1015 | 1016 | async def _get_post(self, post_id): 1017 | post = await self.perform_graphql( 1018 | 'post', Queries.get_post, id=int(post_id), votesCount=100 1019 | ) 1020 | if post is None: 1021 | raise PostNotFound(f'Post id {post_id} is invalid') 1022 | return post 1023 | 1024 | async def get_post(self, post_id): 1025 | post = await self._get_post(post_id) 1026 | return get_post_object(self, post) 1027 | 1028 | async def post_exists(self, post_id): 1029 | if isinstance(post_id, Post): 1030 | post_id = post_id.id 1031 | post = await self.perform_graphql( 1032 | 'post', Queries.post_exists, id=post_id 1033 | ) 1034 | return post is not None 1035 | 1036 | async def _get_leaderboard(self, cursor=None): 1037 | if cursor is None: 1038 | leaderboard = await self.perform_graphql( 1039 | 'leaderboard', 1040 | Queries.get_leaderboard 1041 | ) 1042 | else: 1043 | leaderboard = await self.perform_graphql( 1044 | 'leaderboard', 1045 | Queries.get_leaderboard, 1046 | after=cursor 1047 | ) 1048 | return leaderboard 1049 | 1050 | def get_leaderboard(self, limit=30): 1051 | return Leaderboards(self, limit) 1052 | 1053 | async def _resolve_report(self, id): 1054 | return await self.perform_graphql( 1055 | 'resolveBoardReport', 1056 | Queries.resolve_report, 1057 | id=id 1058 | ) 1059 | 1060 | async def _get_all_posts( 1061 | self, order='new', search_query=None, after=None 1062 | ): 1063 | posts = await self.perform_graphql( 1064 | 'ReplPostsFeed', 1065 | Queries.posts_feed, 1066 | options={ 1067 | 'order': order.title(), 1068 | 'searchQuery': search_query, 1069 | 'after': after 1070 | } 1071 | ) 1072 | return posts 1073 | 1074 | async def get_user_by_id(self, user_id): 1075 | return User( 1076 | self, 1077 | await self.perform_graphql( 1078 | 'user', 1079 | Queries.get_user_by_id, 1080 | user_id=user_id 1081 | ) 1082 | ) 1083 | 1084 | async def _posts_in_board( 1085 | self, 1086 | board_slugs=None, 1087 | order='new', 1088 | search_query=None, 1089 | after=None 1090 | ): 1091 | posts = await self.perform_graphql( 1092 | 'ReplPostsFeed', 1093 | Queries.posts_feed, 1094 | options={ 1095 | 'boardSlugs': board_slugs, 1096 | 'order': order.title(), 1097 | 'searchQuery': search_query, 1098 | 'after': after 1099 | } 1100 | ) 1101 | return posts 1102 | 1103 | class _boards: 1104 | board_names = ['all', 'announcements', 'challenge', 'ask', 'learn', 'share', 'templates', 'tutorials'] 1105 | __slots__ = ['client', ] + board_names 1106 | for board in board_names: 1107 | locals()['_' + board] = type( 1108 | '_' + board, 1109 | (Board,), 1110 | {'name': board} 1111 | ) 1112 | # Creates classes for each of the boards 1113 | del board # Don't want that extra class var 1114 | 1115 | def __init__(self, client): 1116 | self.client = client 1117 | 1118 | self.all = self._all(client) 1119 | self.announcements = self._announcements(client) 1120 | self.challenge = self._challenge(client) 1121 | self.ask = self._ask(client) 1122 | self.learn = self._learn(client) 1123 | self.share = self._share(client) 1124 | self.templates = self._templates(client) 1125 | self.tutorials = self._tutorials(client) 1126 | 1127 | async def _get_comments(self, post_id, order='new'): 1128 | return await self.perform_graphql( 1129 | 'post', 1130 | Queries.get_comments, 1131 | id=post_id, 1132 | commentsOrder=order 1133 | ) 1134 | 1135 | async def _get_user_comments(self, user_id, limit, order): 1136 | 1137 | return await self.perform_graphql( 1138 | 'ProfileComments', 1139 | Queries.get_user_comments, 1140 | user_id=user_id, 1141 | limit=limit, 1142 | commentsOrder=order 1143 | ) 1144 | 1145 | async def _get_user_posts(self, user_id, limit, order): 1146 | return await self.perform_graphql( 1147 | 'user', 1148 | Queries.get_user_posts, 1149 | user_id=user_id, 1150 | limit=limit, 1151 | order=order 1152 | ) 1153 | 1154 | async def _get_user_repls( 1155 | self, user, limit, 1156 | pinned_first, before, 1157 | after, direction 1158 | ): 1159 | return await self.perform_graphql( 1160 | 'user', 1161 | Queries.get_user_repls, 1162 | user_id=user.id, 1163 | limit=limit, 1164 | pinnedFirst=pinned_first, 1165 | before=before, 1166 | after=after, 1167 | direction=direction 1168 | ) 1169 | 1170 | async def _get_all_comments(self, order='new'): 1171 | return await self.perform_graphql( 1172 | 'comments', 1173 | Queries.get_all_comments, 1174 | order=order 1175 | ) 1176 | 1177 | async def _get_comment(self, id): 1178 | return await self.perform_graphql( 1179 | 'comment', 1180 | Queries.get_comment, 1181 | id=id 1182 | ) 1183 | 1184 | async def get_comment(self, id): 1185 | data = await self._get_comment(id) 1186 | post = await self.get_post(data['post']['id']) 1187 | return Comment(self, data, post) 1188 | 1189 | async def get_all_comments(self, order='new'): 1190 | _comments = await self._get_all_comments(order=order) 1191 | comments = [] 1192 | for c in _comments['items']: 1193 | comments.append(Comment( 1194 | self, 1195 | id=c['id'], 1196 | body=c['body'], 1197 | time_created=c['timeCreated'], 1198 | can_edit=c['canEdit'], 1199 | can_comment=c['canComment'], 1200 | can_report=c['canReport'], 1201 | has_reported=c['hasReported'], 1202 | url=c['url'], 1203 | votes=c['voteCount'], 1204 | can_vote=c['canVote'], 1205 | has_voted=c['hasVoted'], 1206 | user=c['user'], 1207 | post=c['post'], 1208 | comments=c['comments'] 1209 | )) 1210 | return comments 1211 | 1212 | async def _get_user(self, name): 1213 | user = await self.perform_graphql( 1214 | 'userByUsername', 1215 | Queries.get_user, 1216 | username=name, 1217 | ) 1218 | return user 1219 | 1220 | async def get_user(self, name): 1221 | user = await self._get_user(name) 1222 | if user is None: 1223 | return None 1224 | u = User( 1225 | self, 1226 | user=user 1227 | ) 1228 | return u 1229 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "aiohttp" 3 | version = "3.8.1" 4 | description = "Async http client/server framework (asyncio)" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [package.dependencies] 10 | aiosignal = ">=1.1.2" 11 | async-timeout = ">=4.0.0a3,<5.0" 12 | attrs = ">=17.3.0" 13 | charset-normalizer = ">=2.0,<3.0" 14 | frozenlist = ">=1.1.1" 15 | multidict = ">=4.5,<7.0" 16 | yarl = ">=1.0,<2.0" 17 | 18 | [package.extras] 19 | speedups = ["aiodns", "brotli", "cchardet"] 20 | 21 | [[package]] 22 | name = "aiosignal" 23 | version = "1.2.0" 24 | description = "aiosignal: a list of registered asynchronous callbacks" 25 | category = "main" 26 | optional = false 27 | python-versions = ">=3.6" 28 | 29 | [package.dependencies] 30 | frozenlist = ">=1.1.0" 31 | 32 | [[package]] 33 | name = "async-timeout" 34 | version = "4.0.2" 35 | description = "Timeout context manager for asyncio programs" 36 | category = "main" 37 | optional = false 38 | python-versions = ">=3.6" 39 | 40 | [[package]] 41 | name = "attrs" 42 | version = "21.4.0" 43 | description = "Classes Without Boilerplate" 44 | category = "main" 45 | optional = false 46 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 47 | 48 | [package.extras] 49 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 50 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 51 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 52 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] 53 | 54 | [[package]] 55 | name = "charset-normalizer" 56 | version = "2.0.9" 57 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 58 | category = "main" 59 | optional = false 60 | python-versions = ">=3.5.0" 61 | 62 | [package.extras] 63 | unicode_backport = ["unicodedata2"] 64 | 65 | [[package]] 66 | name = "flake8" 67 | version = "4.0.1" 68 | description = "the modular source code checker: pep8 pyflakes and co" 69 | category = "dev" 70 | optional = false 71 | python-versions = ">=3.6" 72 | 73 | [package.dependencies] 74 | mccabe = ">=0.6.0,<0.7.0" 75 | pycodestyle = ">=2.8.0,<2.9.0" 76 | pyflakes = ">=2.4.0,<2.5.0" 77 | 78 | [[package]] 79 | name = "frozenlist" 80 | version = "1.2.0" 81 | description = "A list-like structure which implements collections.abc.MutableSequence" 82 | category = "main" 83 | optional = false 84 | python-versions = ">=3.6" 85 | 86 | [[package]] 87 | name = "idna" 88 | version = "3.3" 89 | description = "Internationalized Domain Names in Applications (IDNA)" 90 | category = "main" 91 | optional = false 92 | python-versions = ">=3.5" 93 | 94 | [[package]] 95 | name = "mccabe" 96 | version = "0.6.1" 97 | description = "McCabe checker, plugin for flake8" 98 | category = "dev" 99 | optional = false 100 | python-versions = "*" 101 | 102 | [[package]] 103 | name = "multidict" 104 | version = "5.2.0" 105 | description = "multidict implementation" 106 | category = "main" 107 | optional = false 108 | python-versions = ">=3.6" 109 | 110 | [[package]] 111 | name = "pycodestyle" 112 | version = "2.8.0" 113 | description = "Python style guide checker" 114 | category = "dev" 115 | optional = false 116 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 117 | 118 | [[package]] 119 | name = "pyflakes" 120 | version = "2.4.0" 121 | description = "passive checker of Python programs" 122 | category = "dev" 123 | optional = false 124 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 125 | 126 | [[package]] 127 | name = "yarl" 128 | version = "1.7.2" 129 | description = "Yet another URL library" 130 | category = "main" 131 | optional = false 132 | python-versions = ">=3.6" 133 | 134 | [package.dependencies] 135 | idna = ">=2.0" 136 | multidict = ">=4.0" 137 | 138 | [metadata] 139 | lock-version = "1.1" 140 | python-versions = "^3.8" 141 | content-hash = "6d3b8514fee7ec651cae65d0139e7907e2684621935315d607afd5504d692f0c" 142 | 143 | [metadata.files] 144 | aiohttp = [ 145 | {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, 146 | {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, 147 | {file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"}, 148 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"}, 149 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"}, 150 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"}, 151 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"}, 152 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"}, 153 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"}, 154 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"}, 155 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"}, 156 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"}, 157 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"}, 158 | {file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"}, 159 | {file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"}, 160 | {file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"}, 161 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"}, 162 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"}, 163 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"}, 164 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"}, 165 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"}, 166 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"}, 167 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"}, 168 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"}, 169 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"}, 170 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"}, 171 | {file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"}, 172 | {file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"}, 173 | {file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"}, 174 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"}, 175 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"}, 176 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"}, 177 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"}, 178 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"}, 179 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"}, 180 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"}, 181 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"}, 182 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"}, 183 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"}, 184 | {file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"}, 185 | {file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"}, 186 | {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"}, 187 | {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"}, 188 | {file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"}, 189 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"}, 190 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"}, 191 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"}, 192 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"}, 193 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"}, 194 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"}, 195 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"}, 196 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"}, 197 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"}, 198 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"}, 199 | {file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"}, 200 | {file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"}, 201 | {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"}, 202 | {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"}, 203 | {file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"}, 204 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"}, 205 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"}, 206 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"}, 207 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"}, 208 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"}, 209 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"}, 210 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"}, 211 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"}, 212 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"}, 213 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"}, 214 | {file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"}, 215 | {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"}, 216 | {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"}, 217 | ] 218 | aiosignal = [ 219 | {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, 220 | {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, 221 | ] 222 | async-timeout = [ 223 | {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, 224 | {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, 225 | ] 226 | attrs = [ 227 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 228 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 229 | ] 230 | charset-normalizer = [ 231 | {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, 232 | {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, 233 | ] 234 | flake8 = [ 235 | {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, 236 | {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, 237 | ] 238 | frozenlist = [ 239 | {file = "frozenlist-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:977a1438d0e0d96573fd679d291a1542097ea9f4918a8b6494b06610dfeefbf9"}, 240 | {file = "frozenlist-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8d86547a5e98d9edd47c432f7a14b0c5592624b496ae9880fb6332f34af1edc"}, 241 | {file = "frozenlist-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:181754275d5d32487431a0a29add4f897968b7157204bc1eaaf0a0ce80c5ba7d"}, 242 | {file = "frozenlist-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5df31bb2b974f379d230a25943d9bf0d3bc666b4b0807394b131a28fca2b0e5f"}, 243 | {file = "frozenlist-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4766632cd8a68e4f10f156a12c9acd7b1609941525569dd3636d859d79279ed3"}, 244 | {file = "frozenlist-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16eef427c51cb1203a7c0ab59d1b8abccaba9a4f58c4bfca6ed278fc896dc193"}, 245 | {file = "frozenlist-1.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:01d79515ed5aa3d699b05f6bdcf1fe9087d61d6b53882aa599a10853f0479c6c"}, 246 | {file = "frozenlist-1.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:28e164722ea0df0cf6d48c4d5bdf3d19e87aaa6dfb39b0ba91153f224b912020"}, 247 | {file = "frozenlist-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e63ad0beef6ece06475d29f47d1f2f29727805376e09850ebf64f90777962792"}, 248 | {file = "frozenlist-1.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:41de4db9b9501679cf7cddc16d07ac0f10ef7eb58c525a1c8cbff43022bddca4"}, 249 | {file = "frozenlist-1.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c6a9d84ee6427b65a81fc24e6ef589cb794009f5ca4150151251c062773e7ed2"}, 250 | {file = "frozenlist-1.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:f5f3b2942c3b8b9bfe76b408bbaba3d3bb305ee3693e8b1d631fe0a0d4f93673"}, 251 | {file = "frozenlist-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c98d3c04701773ad60d9545cd96df94d955329efc7743fdb96422c4b669c633b"}, 252 | {file = "frozenlist-1.2.0-cp310-cp310-win32.whl", hash = "sha256:72cfbeab7a920ea9e74b19aa0afe3b4ad9c89471e3badc985d08756efa9b813b"}, 253 | {file = "frozenlist-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:11ff401951b5ac8c0701a804f503d72c048173208490c54ebb8d7bb7c07a6d00"}, 254 | {file = "frozenlist-1.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b46f997d5ed6d222a863b02cdc9c299101ee27974d9bbb2fd1b3c8441311c408"}, 255 | {file = "frozenlist-1.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351686ca020d1bcd238596b1fa5c8efcbc21bffda9d0efe237aaa60348421e2a"}, 256 | {file = "frozenlist-1.2.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfbaa08cf1452acad9cb1c1d7b89394a41e712f88df522cea1a0f296b57782a0"}, 257 | {file = "frozenlist-1.2.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2ae2f5e9fa10805fb1c9adbfefaaecedd9e31849434be462c3960a0139ed729"}, 258 | {file = "frozenlist-1.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6790b8d96bbb74b7a6f4594b6f131bd23056c25f2aa5d816bd177d95245a30e3"}, 259 | {file = "frozenlist-1.2.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:41f62468af1bd4e4b42b5508a3fe8cc46a693f0cdd0ca2f443f51f207893d837"}, 260 | {file = "frozenlist-1.2.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:ec6cf345771cdb00791d271af9a0a6fbfc2b6dd44cb753f1eeaa256e21622adb"}, 261 | {file = "frozenlist-1.2.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:14a5cef795ae3e28fb504b73e797c1800e9249f950e1c964bb6bdc8d77871161"}, 262 | {file = "frozenlist-1.2.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8b54cdd2fda15467b9b0bfa78cee2ddf6dbb4585ef23a16e14926f4b076dfae4"}, 263 | {file = "frozenlist-1.2.0-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f025f1d6825725b09c0038775acab9ae94264453a696cc797ce20c0769a7b367"}, 264 | {file = "frozenlist-1.2.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:84e97f59211b5b9083a2e7a45abf91cfb441369e8bb6d1f5287382c1c526def3"}, 265 | {file = "frozenlist-1.2.0-cp36-cp36m-win32.whl", hash = "sha256:c5328ed53fdb0a73c8a50105306a3bc013e5ca36cca714ec4f7bd31d38d8a97f"}, 266 | {file = "frozenlist-1.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:9ade70aea559ca98f4b1b1e5650c45678052e76a8ab2f76d90f2ac64180215a2"}, 267 | {file = "frozenlist-1.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0d3ffa8772464441b52489b985d46001e2853a3b082c655ec5fad9fb6a3d618"}, 268 | {file = "frozenlist-1.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3457f8cf86deb6ce1ba67e120f1b0128fcba1332a180722756597253c465fc1d"}, 269 | {file = "frozenlist-1.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a72eecf37eface331636951249d878750db84034927c997d47f7f78a573b72b"}, 270 | {file = "frozenlist-1.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:acc4614e8d1feb9f46dd829a8e771b8f5c4b1051365d02efb27a3229048ade8a"}, 271 | {file = "frozenlist-1.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:87521e32e18a2223311afc2492ef2d99946337da0779ddcda77b82ee7319df59"}, 272 | {file = "frozenlist-1.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b4c7665a17c3a5430edb663e4ad4e1ad457614d1b2f2b7f87052e2ef4fa45ca"}, 273 | {file = "frozenlist-1.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ed58803563a8c87cf4c0771366cf0ad1aa265b6b0ae54cbbb53013480c7ad74d"}, 274 | {file = "frozenlist-1.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:aa44c4740b4e23fcfa259e9dd52315d2b1770064cde9507457e4c4a65a04c397"}, 275 | {file = "frozenlist-1.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:2de5b931701257d50771a032bba4e448ff958076380b049fd36ed8738fdb375b"}, 276 | {file = "frozenlist-1.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6e105013fa84623c057a4381dc8ea0361f4d682c11f3816cc80f49a1f3bc17c6"}, 277 | {file = "frozenlist-1.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:705c184b77565955a99dc360f359e8249580c6b7eaa4dc0227caa861ef46b27a"}, 278 | {file = "frozenlist-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:a37594ad6356e50073fe4f60aa4187b97d15329f2138124d252a5a19c8553ea4"}, 279 | {file = "frozenlist-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:25b358aaa7dba5891b05968dd539f5856d69f522b6de0bf34e61f133e077c1a4"}, 280 | {file = "frozenlist-1.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af2a51c8a381d76eabb76f228f565ed4c3701441ecec101dd18be70ebd483cfd"}, 281 | {file = "frozenlist-1.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:82d22f6e6f2916e837c91c860140ef9947e31194c82aaeda843d6551cec92f19"}, 282 | {file = "frozenlist-1.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cfe6fef507f8bac40f009c85c7eddfed88c1c0d38c75e72fe10476cef94e10f"}, 283 | {file = "frozenlist-1.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f602e380a5132880fa245c92030abb0fc6ff34e0c5500600366cedc6adb06a"}, 284 | {file = "frozenlist-1.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ad065b2ebd09f32511ff2be35c5dfafee6192978b5a1e9d279a5c6e121e3b03"}, 285 | {file = "frozenlist-1.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc93f5f62df3bdc1f677066327fc81f92b83644852a31c6aa9b32c2dde86ea7d"}, 286 | {file = "frozenlist-1.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:89fdfc84c6bf0bff2ff3170bb34ecba8a6911b260d318d377171429c4be18c73"}, 287 | {file = "frozenlist-1.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:47b2848e464883d0bbdcd9493c67443e5e695a84694efff0476f9059b4cb6257"}, 288 | {file = "frozenlist-1.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4f52d0732e56906f8ddea4bd856192984650282424049c956857fed43697ea43"}, 289 | {file = "frozenlist-1.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:16ef7dd5b7d17495404a2e7a49bac1bc13d6d20c16d11f4133c757dd94c4144c"}, 290 | {file = "frozenlist-1.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1cf63243bc5f5c19762943b0aa9e0d3fb3723d0c514d820a18a9b9a5ef864315"}, 291 | {file = "frozenlist-1.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:54a1e09ab7a69f843cd28fefd2bcaf23edb9e3a8d7680032c8968b8ac934587d"}, 292 | {file = "frozenlist-1.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:954b154a4533ef28bd3e83ffdf4eadf39deeda9e38fb8feaf066d6069885e034"}, 293 | {file = "frozenlist-1.2.0-cp38-cp38-win32.whl", hash = "sha256:cb3957c39668d10e2b486acc85f94153520a23263b6401e8f59422ef65b9520d"}, 294 | {file = "frozenlist-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0a7c7cce70e41bc13d7d50f0e5dd175f14a4f1837a8549b0936ed0cbe6170bf9"}, 295 | {file = "frozenlist-1.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4c457220468d734e3077580a3642b7f682f5fd9507f17ddf1029452450912cdc"}, 296 | {file = "frozenlist-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e74f8b4d8677ebb4015ac01fcaf05f34e8a1f22775db1f304f497f2f88fdc697"}, 297 | {file = "frozenlist-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fbd4844ff111449f3bbe20ba24fbb906b5b1c2384d0f3287c9f7da2354ce6d23"}, 298 | {file = "frozenlist-1.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0081a623c886197ff8de9e635528fd7e6a387dccef432149e25c13946cb0cd0"}, 299 | {file = "frozenlist-1.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9b6e21e5770df2dea06cb7b6323fbc008b13c4a4e3b52cb54685276479ee7676"}, 300 | {file = "frozenlist-1.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:406aeb340613b4b559db78d86864485f68919b7141dec82aba24d1477fd2976f"}, 301 | {file = "frozenlist-1.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:878ebe074839d649a1cdb03a61077d05760624f36d196884a5cafb12290e187b"}, 302 | {file = "frozenlist-1.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1fef737fd1388f9b93bba8808c5f63058113c10f4e3c0763ced68431773f72f9"}, 303 | {file = "frozenlist-1.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a495c3d513573b0b3f935bfa887a85d9ae09f0627cf47cad17d0cc9b9ba5c38"}, 304 | {file = "frozenlist-1.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e7d0dd3e727c70c2680f5f09a0775525229809f1a35d8552b92ff10b2b14f2c2"}, 305 | {file = "frozenlist-1.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:66a518731a21a55b7d3e087b430f1956a36793acc15912e2878431c7aec54210"}, 306 | {file = "frozenlist-1.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:94728f97ddf603d23c8c3dd5cae2644fa12d33116e69f49b1644a71bb77b89ae"}, 307 | {file = "frozenlist-1.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c1e8e9033d34c2c9e186e58279879d78c94dd365068a3607af33f2bc99357a53"}, 308 | {file = "frozenlist-1.2.0-cp39-cp39-win32.whl", hash = "sha256:83334e84a290a158c0c4cc4d22e8c7cfe0bba5b76d37f1c2509dabd22acafe15"}, 309 | {file = "frozenlist-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:735f386ec522e384f511614c01d2ef9cf799f051353876b4c6fb93ef67a6d1ee"}, 310 | {file = "frozenlist-1.2.0.tar.gz", hash = "sha256:68201be60ac56aff972dc18085800b6ee07973c49103a8aba669dee3d71079de"}, 311 | ] 312 | idna = [ 313 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 314 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 315 | ] 316 | mccabe = [ 317 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 318 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 319 | ] 320 | multidict = [ 321 | {file = "multidict-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55"}, 322 | {file = "multidict-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:28e6d883acd8674887d7edc896b91751dc2d8e87fbdca8359591a13872799e4e"}, 323 | {file = "multidict-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b61f85101ef08cbbc37846ac0e43f027f7844f3fade9b7f6dd087178caedeee7"}, 324 | {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9b668c065968c5979fe6b6fa6760bb6ab9aeb94b75b73c0a9c1acf6393ac3bf"}, 325 | {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:517d75522b7b18a3385726b54a081afd425d4f41144a5399e5abd97ccafdf36b"}, 326 | {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b4ac3ba7a97b35a5ccf34f41b5a8642a01d1e55454b699e5e8e7a99b5a3acf5"}, 327 | {file = "multidict-5.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:df23c83398715b26ab09574217ca21e14694917a0c857e356fd39e1c64f8283f"}, 328 | {file = "multidict-5.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e58a9b5cc96e014ddf93c2227cbdeca94b56a7eb77300205d6e4001805391747"}, 329 | {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f76440e480c3b2ca7f843ff8a48dc82446b86ed4930552d736c0bac507498a52"}, 330 | {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cfde464ca4af42a629648c0b0d79b8f295cf5b695412451716531d6916461628"}, 331 | {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0fed465af2e0eb6357ba95795d003ac0bdb546305cc2366b1fc8f0ad67cc3fda"}, 332 | {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:b70913cbf2e14275013be98a06ef4b412329fe7b4f83d64eb70dce8269ed1e1a"}, 333 | {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5635bcf1b75f0f6ef3c8a1ad07b500104a971e38d3683167b9454cb6465ac86"}, 334 | {file = "multidict-5.2.0-cp310-cp310-win32.whl", hash = "sha256:77f0fb7200cc7dedda7a60912f2059086e29ff67cefbc58d2506638c1a9132d7"}, 335 | {file = "multidict-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:9416cf11bcd73c861267e88aea71e9fcc35302b3943e45e1dbb4317f91a4b34f"}, 336 | {file = "multidict-5.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fd77c8f3cba815aa69cb97ee2b2ef385c7c12ada9c734b0f3b32e26bb88bbf1d"}, 337 | {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ec9aea6223adf46999f22e2c0ab6cf33f5914be604a404f658386a8f1fba37"}, 338 | {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5283c0a00f48e8cafcecadebfa0ed1dac8b39e295c7248c44c665c16dc1138b"}, 339 | {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f79c19c6420962eb17c7e48878a03053b7ccd7b69f389d5831c0a4a7f1ac0a1"}, 340 | {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e4a67f1080123de76e4e97a18d10350df6a7182e243312426d508712e99988d4"}, 341 | {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:94b117e27efd8e08b4046c57461d5a114d26b40824995a2eb58372b94f9fca02"}, 342 | {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2e77282fd1d677c313ffcaddfec236bf23f273c4fba7cdf198108f5940ae10f5"}, 343 | {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:116347c63ba049c1ea56e157fa8aa6edaf5e92925c9b64f3da7769bdfa012858"}, 344 | {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:dc3a866cf6c13d59a01878cd806f219340f3e82eed514485e094321f24900677"}, 345 | {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac42181292099d91217a82e3fa3ce0e0ddf3a74fd891b7c2b347a7f5aa0edded"}, 346 | {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:f0bb0973f42ffcb5e3537548e0767079420aefd94ba990b61cf7bb8d47f4916d"}, 347 | {file = "multidict-5.2.0-cp36-cp36m-win32.whl", hash = "sha256:ea21d4d5104b4f840b91d9dc8cbc832aba9612121eaba503e54eaab1ad140eb9"}, 348 | {file = "multidict-5.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:e6453f3cbeb78440747096f239d282cc57a2997a16b5197c9bc839099e1633d0"}, 349 | {file = "multidict-5.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3def943bfd5f1c47d51fd324df1e806d8da1f8e105cc7f1c76a1daf0f7e17b0"}, 350 | {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35591729668a303a02b06e8dba0eb8140c4a1bfd4c4b3209a436a02a5ac1de11"}, 351 | {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8cacda0b679ebc25624d5de66c705bc53dcc7c6f02a7fb0f3ca5e227d80422"}, 352 | {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:baf1856fab8212bf35230c019cde7c641887e3fc08cadd39d32a421a30151ea3"}, 353 | {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a43616aec0f0d53c411582c451f5d3e1123a68cc7b3475d6f7d97a626f8ff90d"}, 354 | {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25cbd39a9029b409167aa0a20d8a17f502d43f2efebfe9e3ac019fe6796c59ac"}, 355 | {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a2cbcfbea6dc776782a444db819c8b78afe4db597211298dd8b2222f73e9cd0"}, 356 | {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3d2d7d1fff8e09d99354c04c3fd5b560fb04639fd45926b34e27cfdec678a704"}, 357 | {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a37e9a68349f6abe24130846e2f1d2e38f7ddab30b81b754e5a1fde32f782b23"}, 358 | {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:637c1896497ff19e1ee27c1c2c2ddaa9f2d134bbb5e0c52254361ea20486418d"}, 359 | {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9815765f9dcda04921ba467957be543423e5ec6a1136135d84f2ae092c50d87b"}, 360 | {file = "multidict-5.2.0-cp37-cp37m-win32.whl", hash = "sha256:8b911d74acdc1fe2941e59b4f1a278a330e9c34c6c8ca1ee21264c51ec9b67ef"}, 361 | {file = "multidict-5.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:380b868f55f63d048a25931a1632818f90e4be71d2081c2338fcf656d299949a"}, 362 | {file = "multidict-5.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e7d81ce5744757d2f05fc41896e3b2ae0458464b14b5a2c1e87a6a9d69aefaa8"}, 363 | {file = "multidict-5.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d1d55cdf706ddc62822d394d1df53573d32a7a07d4f099470d3cb9323b721b6"}, 364 | {file = "multidict-5.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4771d0d0ac9d9fe9e24e33bed482a13dfc1256d008d101485fe460359476065"}, 365 | {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da7d57ea65744d249427793c042094c4016789eb2562576fb831870f9c878d9e"}, 366 | {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdd68778f96216596218b4e8882944d24a634d984ee1a5a049b300377878fa7c"}, 367 | {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecc99bce8ee42dcad15848c7885197d26841cb24fa2ee6e89d23b8993c871c64"}, 368 | {file = "multidict-5.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:067150fad08e6f2dd91a650c7a49ba65085303fcc3decbd64a57dc13a2733031"}, 369 | {file = "multidict-5.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:78c106b2b506b4d895ddc801ff509f941119394b89c9115580014127414e6c2d"}, 370 | {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6c4fa1ec16e01e292315ba76eb1d012c025b99d22896bd14a66628b245e3e01"}, 371 | {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b227345e4186809d31f22087d0265655114af7cda442ecaf72246275865bebe4"}, 372 | {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:06560fbdcf22c9387100979e65b26fba0816c162b888cb65b845d3def7a54c9b"}, 373 | {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7878b61c867fb2df7a95e44b316f88d5a3742390c99dfba6c557a21b30180cac"}, 374 | {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:246145bff76cc4b19310f0ad28bd0769b940c2a49fc601b86bfd150cbd72bb22"}, 375 | {file = "multidict-5.2.0-cp38-cp38-win32.whl", hash = "sha256:c30ac9f562106cd9e8071c23949a067b10211917fdcb75b4718cf5775356a940"}, 376 | {file = "multidict-5.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:f19001e790013ed580abfde2a4465388950728861b52f0da73e8e8a9418533c0"}, 377 | {file = "multidict-5.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c1ff762e2ee126e6f1258650ac641e2b8e1f3d927a925aafcfde943b77a36d24"}, 378 | {file = "multidict-5.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bd6c9c50bf2ad3f0448edaa1a3b55b2e6866ef8feca5d8dbec10ec7c94371d21"}, 379 | {file = "multidict-5.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc66d4016f6e50ed36fb39cd287a3878ffcebfa90008535c62e0e90a7ab713ae"}, 380 | {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9acb76d5f3dd9421874923da2ed1e76041cb51b9337fd7f507edde1d86535d6"}, 381 | {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dfc924a7e946dd3c6360e50e8f750d51e3ef5395c95dc054bc9eab0f70df4f9c"}, 382 | {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fdba7333eb2351fee2596b756d730d62b5827d5e1ab2f84e6cbb287cc67fe0"}, 383 | {file = "multidict-5.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b9aad49466b8d828b96b9e3630006234879c8d3e2b0a9d99219b3121bc5cdb17"}, 384 | {file = "multidict-5.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:93de39267c4c676c9ebb2057e98a8138bade0d806aad4d864322eee0803140a0"}, 385 | {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9bef5cff994ca3026fcc90680e326d1a19df9841c5e3d224076407cc21471a1"}, 386 | {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5f841c4f14331fd1e36cbf3336ed7be2cb2a8f110ce40ea253e5573387db7621"}, 387 | {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:38ba256ee9b310da6a1a0f013ef4e422fca30a685bcbec86a969bd520504e341"}, 388 | {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3bc3b1621b979621cee9f7b09f024ec76ec03cc365e638126a056317470bde1b"}, 389 | {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6ee908c070020d682e9b42c8f621e8bb10c767d04416e2ebe44e37d0f44d9ad5"}, 390 | {file = "multidict-5.2.0-cp39-cp39-win32.whl", hash = "sha256:1c7976cd1c157fa7ba5456ae5d31ccdf1479680dc9b8d8aa28afabc370df42b8"}, 391 | {file = "multidict-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac"}, 392 | {file = "multidict-5.2.0.tar.gz", hash = "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"}, 393 | ] 394 | pycodestyle = [ 395 | {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, 396 | {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, 397 | ] 398 | pyflakes = [ 399 | {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, 400 | {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, 401 | ] 402 | yarl = [ 403 | {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, 404 | {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, 405 | {file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"}, 406 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"}, 407 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"}, 408 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"}, 409 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"}, 410 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"}, 411 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"}, 412 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"}, 413 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"}, 414 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"}, 415 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"}, 416 | {file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"}, 417 | {file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"}, 418 | {file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"}, 419 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"}, 420 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"}, 421 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"}, 422 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"}, 423 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"}, 424 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"}, 425 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"}, 426 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"}, 427 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"}, 428 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"}, 429 | {file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"}, 430 | {file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"}, 431 | {file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"}, 432 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"}, 433 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"}, 434 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"}, 435 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"}, 436 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"}, 437 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"}, 438 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"}, 439 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"}, 440 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"}, 441 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"}, 442 | {file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"}, 443 | {file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"}, 444 | {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"}, 445 | {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"}, 446 | {file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"}, 447 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"}, 448 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"}, 449 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"}, 450 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"}, 451 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"}, 452 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"}, 453 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"}, 454 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"}, 455 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"}, 456 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"}, 457 | {file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"}, 458 | {file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"}, 459 | {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"}, 460 | {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"}, 461 | {file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"}, 462 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"}, 463 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"}, 464 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"}, 465 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"}, 466 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"}, 467 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"}, 468 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"}, 469 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"}, 470 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"}, 471 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"}, 472 | {file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"}, 473 | {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"}, 474 | {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"}, 475 | ] 476 | --------------------------------------------------------------------------------