diff --git a/.pylintrc b/.pylintrc index bc43bc7..8916dcb 100644 --- a/.pylintrc +++ b/.pylintrc @@ -2,13 +2,13 @@ # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. -extension-pkg-allow-list=lxml +extension-pkg-allow-list=lxml, pydantic # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. (This is an alternative name to extension-pkg-allow-list # for backward compatibility.) -extension-pkg-whitelist=lxml +extension-pkg-whitelist=lxml, pydantic # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime diff --git a/nonebot_plugin_tetris_stats/game_data_processor/io_data_processor.py b/nonebot_plugin_tetris_stats/game_data_processor/io_data_processor.py index d4b857c..572bd9d 100644 --- a/nonebot_plugin_tetris_stats/game_data_processor/io_data_processor.py +++ b/nonebot_plugin_tetris_stats/game_data_processor/io_data_processor.py @@ -1,19 +1,26 @@ -from typing import Any +import os from asyncio import gather from re import I -from playwright.async_api import Browser, async_playwright -from ujson import loads, JSONDecodeError +from typing import Any + import aiohttp - -from nonebot import on_regex, get_driver +from nonebot import get_driver, on_regex from nonebot.adapters.onebot.v11 import GROUP, MessageEvent -from nonebot.matcher import Matcher from nonebot.log import logger +from nonebot.matcher import Matcher +from ujson import JSONDecodeError, dumps, loads +from playwright.async_api import ( + Browser, + Response, + async_playwright +) -from ..utils.message_analyzer import handle_bind_message, handle_stats_query_message +from ..utils.config import Config from ..utils.sql import query_bind_info, write_bind_info - -_BROWSER: Browser | None = None +from ..utils.message_analyzer import ( + handle_bind_message, + handle_stats_query_message +) IOBind = on_regex(pattern=r'^io绑定|^iobind', flags=I, permission=GROUP) @@ -21,6 +28,8 @@ IOStats = on_regex(pattern=r'^io查|^iostats', flags=I, permission=GROUP) driver = get_driver() +config = Config.parse_obj(get_driver().config) + @IOBind.handle() async def _(event: MessageEvent, matcher: Matcher): @@ -28,7 +37,7 @@ async def _(event: MessageEvent, matcher: Matcher): if decoded_message[0] is None: await matcher.finish(decoded_message[1][0]) if decoded_message[0] == 'ID': - user_id_stats = await check_user_id(user_id=decoded_message[1][1]) + user_id_stats = await check_user_id(decoded_message[1][1]) if user_id_stats[0] is False: await matcher.finish(user_id_stats[1]) else: @@ -40,13 +49,17 @@ async def _(event: MessageEvent, matcher: Matcher): elif user_data[1] is False: await matcher.finish(f'用户信息请求错误:\n{user_data[2]["error"]}') else: - user_id = await get_user_id(user_data=user_data[2]) + user_id = await get_user_id(user_data[2]) if event.sender.user_id is None: # 理论上是不会有None出现的, ide快乐行属于是( logger.error('获取QQ号失败') await matcher.finish('获取QQ号失败') - await matcher.finish(await write_bind_info(qq_number=event.sender.user_id, - user=user_id, - game_type='IO')) + await matcher.finish( + await write_bind_info( + qq_number=event.sender.user_id, + user=user_id, + game_type='IO' + ) + ) @IOStats.handle() @@ -56,7 +69,7 @@ async def _(event: MessageEvent, matcher: Matcher): await matcher.finish(decoded_message[1][0]) elif decoded_message[0] == 'AT': if event.is_tome() is True: - await matcher.finish(message='不能查询bot的信息') + await matcher.finish('不能查询bot的信息') bind_info = await query_bind_info(qq_number=decoded_message[1][1], game_type='IO') if bind_info is None: message = '未查询到绑定信息' @@ -75,67 +88,24 @@ async def _(event: MessageEvent, matcher: Matcher): message = await generate_message(user_id=decoded_message[1][1]) elif decoded_message[0] == 'Name': message = await generate_message(user_name=decoded_message[1][1]) - await matcher.finish(message=message) + await matcher.finish(message) + + +@driver.on_startup +async def _(): + await Request.init_cache() + await Request.read_cache() @driver.on_shutdown async def _(): - if isinstance(_BROWSER, Browser): - await _BROWSER.close() + await Request.close_browser() -async def init_playwright() -> Browser: - '''初始化playwright''' - global _BROWSER - p = await async_playwright().start() - _BROWSER = await p.firefox.launch() - return _BROWSER - - -async def get_browser() -> Browser: - '''获取浏览器对象''' - return _BROWSER or await init_playwright() - - -async def request(url: str) -> tuple[bool, bool, dict[str, Any]]: - '''请求api''' - try: - async with aiohttp.ClientSession() as session: - async with session.get(url) as resp: - data = await resp.json() - return (True, data['success'], data) - except aiohttp.client_exceptions.ClientConnectorError as error: - logger.error(f'请求错误\n{error}') - return (False, False, {}) - except aiohttp.client_exceptions.ContentTypeError: - # 如果有五秒盾就用firefox硬穿 - browser = await get_browser() - page = await browser.new_page() - await page.goto(url) - attempts = 0 - while True: - text = await page.locator("body").text_content() - if text is None: - continue - attempts += 1 - if await page.title() == 'Please Wait... | Cloudflare': - break - try: - data = loads(text) - except JSONDecodeError: - await page.wait_for_timeout(1000) - else: - await page.close() - return (True, data['success'], data) - if attempts >= 60: - break - await page.close() - return (True, False, {'error': '绕过五秒盾失败'}) - - -async def get_user_data(user_name: str = None, - user_id: str = None - ) -> tuple[bool, bool, dict[str, Any]]: +async def get_user_data( + user_name: str = None, + user_id: str = None +) -> tuple[bool, bool, dict[str, Any]]: '''获取用户数据''' if user_name is not None and user_id is None: user_data_url = f'https://ch.tetr.io/api/users/{user_name}' @@ -143,12 +113,13 @@ async def get_user_data(user_name: str = None, user_data_url = f'https://ch.tetr.io/api/users/{user_id}' else: raise ValueError('预期外行为, 请上报GitHub') - return await request(url=user_data_url) + return await Request.request(user_data_url) -async def get_solo_data(user_name: str = None, - user_id: str = None - ) -> tuple[bool, bool, dict[str, Any]]: +async def get_solo_data( + user_name: str = None, + user_id: str = None +) -> tuple[bool, bool, dict[str, Any]]: '''获取Solo数据''' if user_name is not None and user_id is None: user_solo_url = f'https://ch.tetr.io/api/users/{user_name}/records' @@ -156,7 +127,7 @@ async def get_solo_data(user_name: str = None, user_solo_url = f'https://ch.tetr.io/api/users/{user_id}/records' else: raise ValueError('预期外行为, 请上报GitHub') - return await request(url=user_solo_url) + return await Request.request(user_solo_url) async def get_user_id(user_data: dict) -> str: @@ -168,11 +139,11 @@ async def check_user_id(user_id: str) -> tuple[bool, str]: '''检查用户ID是否有效''' user_data = await get_user_data(user_id=user_id) if user_data[0] is False: - return (False, '用户信息请求失败') + return False, '用户信息请求失败' if user_data[1] is False: - return (False, f'用户信息请求错误:\n{user_data[2]["error"]}') + return False, f'用户信息请求错误:\n{user_data[2]["error"]}' if user_id == user_data[2]['data']['user']['_id']: - return (True, '') + return True, '' raise ValueError('服务器返回的userID和用户提供的不一致, 这种情况理论上不应该发生, 以防万一还是写一下(x') @@ -226,8 +197,10 @@ async def get_blitz_stats(solo_data: dict) -> dict[str, Any]: async def generate_message(user_name: str = None, user_id: str = None) -> str: '''生成消息''' - user_data, solo_data = await gather(get_user_data(user_name=user_name, user_id=user_id), - get_solo_data(user_name=user_name, user_id=user_id)) + user_data, solo_data = await gather( + get_user_data(user_name=user_name, user_id=user_id), + get_solo_data(user_name=user_name, user_id=user_id) + ) if user_data[0] is False: return '用户信息请求失败' if user_data[1] is False: @@ -254,10 +227,138 @@ async def generate_message(user_name: str = None, user_id: str = None) -> str: return f'{message}\nSolo统计数据请求失败' if solo_data[1] is False: return f'{message}\nSolo统计数据请求错误:\n{solo_data[2]["error"]}' - sprint_stats, blitz_stats = await gather(get_sprint_stats(solo_data[2]), - get_blitz_stats(solo_data[2])) + sprint_stats, blitz_stats = await gather( + get_sprint_stats(solo_data[2]), + get_blitz_stats(solo_data[2]) + ) message += f'\n40L: {sprint_stats["Time"]}s' if 'Time' in sprint_stats else '' message += f' ( #{sprint_stats["Rank"]} )' if 'Rank' in sprint_stats else '' message += f'\nBlitz: {blitz_stats["Score"]}' if 'Score' in blitz_stats else '' message += f' ( #{blitz_stats["Rank"]} )' if 'Rank' in blitz_stats else '' return message + + +class Request: + _browser: Browser | None = None + _headers: dict | None = None + _cookies: dict | None = None + + @classmethod + async def _init_playwright(cls) -> Browser: + '''初始化playwright''' + playwright = await async_playwright().start() + cls._browser = await playwright.firefox.launch() + return cls._browser + + @classmethod + async def _get_browser(cls) -> Browser: + '''获取浏览器对象''' + return cls._browser or await cls._init_playwright() + + @classmethod + async def _anti_cloudflare(cls, url: str) -> tuple[bool, bool, dict[str, Any]]: + '''用firefox硬穿五秒盾''' + browser = await cls._get_browser() + context = await browser.new_context() + page = await context.new_page() + response = await page.goto(url) + attempts = 0 + while attempts < 60: + attempts += 1 + text = await page.locator("body").text_content() + if text is None: + await page.wait_for_timeout(1000) + continue + if await page.title() == 'Please Wait... | Cloudflare': + # TODO 有无人来做一个过验证码( + break + try: + data = loads(text) + except JSONDecodeError: + await page.wait_for_timeout(1000) + else: + assert isinstance(response, Response) + cls._headers = await response.request.all_headers() + cls._cookies = {i['name']: i['value'] for i in await context.cookies()} + await cls._write_cache() + await page.close() + await context.close() + return True, data['success'], data + await page.close() + await context.close() + return True, False, {'error': '绕过五秒盾失败'} + + @classmethod + async def init_cache(cls) -> None: + '''初始化缓存文件''' + if not os.path.exists(os.path.dirname(config.cache_path)): + os.makedirs(os.path.dirname(config.cache_path)) + if not os.path.exists(config.cache_path): + with open(file=config.cache_path, mode='w', encoding='UTF-8') as file: + file.write( + dumps( + { + 'headers': cls._browser, + 'cookies': cls._cookies + } + ) + ) + + @classmethod + async def read_cache(cls) -> None: + '''读取缓存文件''' + try: + with open(file=config.cache_path, mode='r', encoding='UTF-8') as file: + json = loads(file.read()) + cls._headers = json['headers'] + cls._cookies = json['cookies'] + except FileNotFoundError: + await cls.init_cache() + except PermissionError: + os.remove(config.cache_path) + await cls.init_cache() + except JSONDecodeError: + os.remove(config.cache_path) + await cls.init_cache() + + @classmethod + async def _write_cache(cls) -> None: + '''写入缓存文件''' + try: + with open(file=config.cache_path, mode='r+', encoding='UTF-8') as file: + file.write( + dumps( + { + 'headers': cls._browser, + 'cookies': cls._cookies + } + ) + ) + except FileNotFoundError: + await cls.init_cache() + except PermissionError: + os.remove(config.cache_path) + await cls.init_cache() + except JSONDecodeError: + os.remove(config.cache_path) + await cls.init_cache() + + @classmethod + async def request(cls, url: str) -> tuple[bool, bool, dict[str, Any]]: + '''请求api''' + try: + async with aiohttp.ClientSession(cookies=cls._cookies) as session: + async with session.get(url, headers=cls._headers) as resp: + data = await resp.json() + return True, data['success'], data + except aiohttp.client_exceptions.ClientConnectorError as error: + logger.error(f'请求错误\n{error}') + return False, False, {} + except aiohttp.client_exceptions.ContentTypeError: + return await cls._anti_cloudflare(url) + + @classmethod + async def close_browser(cls) -> None: + '''关闭浏览器对象''' + if isinstance(cls._browser, Browser): + await cls._browser.close() diff --git a/nonebot_plugin_tetris_stats/utils/config.py b/nonebot_plugin_tetris_stats/utils/config.py new file mode 100644 index 0000000..4cbca3a --- /dev/null +++ b/nonebot_plugin_tetris_stats/utils/config.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class Config(BaseModel): + cache_path: str = 'cache/nonebot_plugin_tetris_stats/cache' diff --git a/poetry.lock b/poetry.lock index b886e4e..5f39dcf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -100,6 +100,14 @@ python-versions = "*" pycodestyle = ">=2.8.0" toml = "*" +[[package]] +name = "brotli" +version = "1.0.9" +description = "Python bindings for the Brotli compression library" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "charset-normalizer" version = "2.1.0" @@ -562,7 +570,7 @@ python-versions = ">=3.7" [[package]] name = "tomlkit" -version = "0.11.1" +version = "0.11.2" description = "Style preserving TOML library" category = "main" optional = false @@ -688,7 +696,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.10,<3.11" -content-hash = "ee024f6b87812714d3b664289d4a47bf1228e49dc3f797f728a27d50fc169f9f" +content-hash = "acda543ec591c9d54e4a869521d9e2457fa441805c1172ca2f50205c4f587e38" [metadata.files] aiohttp = [ @@ -795,6 +803,70 @@ autopep8 = [ {file = "autopep8-1.6.0-py2.py3-none-any.whl", hash = "sha256:ed77137193bbac52d029a52c59bec1b0629b5a186c495f1eb21b126ac466083f"}, {file = "autopep8-1.6.0.tar.gz", hash = "sha256:44f0932855039d2c15c4510d6df665e4730f2b8582704fa48f9c55bd3e17d979"}, ] +brotli = [ + {file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"}, + {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"}, + {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6"}, + {file = "Brotli-1.0.9-cp27-cp27m-win32.whl", hash = "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa"}, + {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452"}, + {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7"}, + {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031"}, + {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43"}, + {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c"}, + {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c"}, + {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0"}, + {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91"}, + {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa"}, + {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb"}, + {file = "Brotli-1.0.9-cp310-cp310-win32.whl", hash = "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181"}, + {file = "Brotli-1.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2"}, + {file = "Brotli-1.0.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4"}, + {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296"}, + {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430"}, + {file = "Brotli-1.0.9-cp35-cp35m-win32.whl", hash = "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1"}, + {file = "Brotli-1.0.9-cp35-cp35m-win_amd64.whl", hash = "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea"}, + {file = "Brotli-1.0.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f"}, + {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4"}, + {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a"}, + {file = "Brotli-1.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b"}, + {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f"}, + {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6"}, + {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b"}, + {file = "Brotli-1.0.9-cp36-cp36m-win32.whl", hash = "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14"}, + {file = "Brotli-1.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c"}, + {file = "Brotli-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126"}, + {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d"}, + {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12"}, + {file = "Brotli-1.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130"}, + {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a"}, + {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3"}, + {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d"}, + {file = "Brotli-1.0.9-cp37-cp37m-win32.whl", hash = "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"}, + {file = "Brotli-1.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5"}, + {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb"}, + {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8"}, + {file = "Brotli-1.0.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb"}, + {file = "Brotli-1.0.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26"}, + {file = "Brotli-1.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c"}, + {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b"}, + {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17"}, + {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649"}, + {file = "Brotli-1.0.9-cp38-cp38-win32.whl", hash = "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429"}, + {file = "Brotli-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f"}, + {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19"}, + {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7"}, + {file = "Brotli-1.0.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b"}, + {file = "Brotli-1.0.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389"}, + {file = "Brotli-1.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7"}, + {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806"}, + {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1"}, + {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c"}, + {file = "Brotli-1.0.9-cp39-cp39-win32.whl", hash = "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3"}, + {file = "Brotli-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761"}, + {file = "Brotli-1.0.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267"}, + {file = "Brotli-1.0.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d"}, + {file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"}, +] charset-normalizer = [ {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, @@ -1440,8 +1512,8 @@ tomli = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tomlkit = [ - {file = "tomlkit-0.11.1-py3-none-any.whl", hash = "sha256:1c5bebdf19d5051e2e1de6cf70adfc5948d47221f097fcff7a3ffc91e953eaf5"}, - {file = "tomlkit-0.11.1.tar.gz", hash = "sha256:61901f81ff4017951119cd0d1ed9b7af31c821d6845c8c477587bbdcd5e5854e"}, + {file = "tomlkit-0.11.2-py3-none-any.whl", hash = "sha256:69e0675671a2eed1c08a53f342c955c4ead5d373a10f756219bf39f3d4f0018a"}, + {file = "tomlkit-0.11.2.tar.gz", hash = "sha256:d1b49c3e460f5910b22d799b13513504acb4f5fcaee01660ee66f07bd45a271c"}, ] types-pytz = [ {file = "types-pytz-2022.1.2.tar.gz", hash = "sha256:1a8b25c225c5e6bd8468aa9eb45ddd3b337f6716d4072ad0aa4ef1e41478eebc"}, diff --git a/pyproject.toml b/pyproject.toml index ee31dee..7072920 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ lxml = "^4.9.1" pandas = "^1.4.3" playwright = "^1.24.1" ujson = "^5.4.0" +Brotli = "^1.0.9" [tool.poetry.dev-dependencies] mypy = "^0.971"