├── tests └── __init__.py ├── pypartpicker ├── errors.py ├── __init__.py ├── regex.py ├── urls.py ├── types.py ├── client.py └── scraper.py ├── pyproject.toml ├── LICENSE ├── .gitignore ├── README.md └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pypartpicker/errors.py: -------------------------------------------------------------------------------- 1 | class CloudflareException(Exception): 2 | pass 3 | 4 | 5 | class RateLimitException(Exception): 6 | pass 7 | -------------------------------------------------------------------------------- /pypartpicker/__init__.py: -------------------------------------------------------------------------------- 1 | from .types import * 2 | from .client import * 3 | from .urls import * 4 | 5 | 6 | class Scraper: 7 | def __init__(self): 8 | raise Exception( 9 | "Initialising the library via Scraper is deprecated, use Client or AsyncClient instead.\nRead 2.0 changes here: https://github.com/thefakequake/pypartpicker/tree/2.0" 10 | ) 11 | -------------------------------------------------------------------------------- /pypartpicker/regex.py: -------------------------------------------------------------------------------- 1 | import re 2 | from .urls import * 3 | 4 | PCPP_BASE_RE = re.compile("(http|https)://([a-zA-Z]{2}\.)?pcpartpicker\.com") 5 | ID_RE = re.compile("(\w{6})") 6 | PRODUCT_URL_RE = re.compile(PCPP_BASE_RE.pattern + PRODUCT_PATH + ID_RE.pattern) 7 | PRODUCT_RATINGS_RE = re.compile("\(([0-9]+) Ratings?, ([0-9]\.[0-9]) Average\)") 8 | DECIMAL_RE = re.compile("[0-9]+\.[0-9]+") 9 | PART_LIST_URL_RE = re.compile( 10 | PCPP_BASE_RE.pattern + PART_LIST_PATH + ID_RE.pattern 11 | ) 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pypartpicker" 3 | version = "2.0.5" 4 | description = "A PCPartPicker data extractor for Python." 5 | authors = ["QuaKe"] 6 | readme = "README.md" 7 | license = "LICENSE" 8 | homepage = "https://github.com/thefakequake/pypartpicker" 9 | repository = "https://github.com/thefakequake/pypartpicker" 10 | keywords = ["pcpartpicker", "pcpp", "scraping"] 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.10" 14 | requests-html = "^0.10.0" 15 | lxml = {extras = ["html-clean"], version = "^5.3.0"} 16 | 17 | 18 | [build-system] 19 | requires = ["poetry-core"] 20 | build-backend = "poetry.core.masonry.api" 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021-2025 QuaKe 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /pypartpicker/urls.py: -------------------------------------------------------------------------------- 1 | PRODUCT_PATH = "/product/" 2 | PART_LIST_PATH = "/list/" 3 | SEARCH_PATH = "/search/" 4 | PART_REVIEWS_PATH = "/reviews/" 5 | PRODUCTS_PATH = "/products/" 6 | 7 | 8 | PRODUCT_KEYBOARD_PATH = "keyboard" 9 | PRODUCT_SPEAKERS_PATH = "speakers" 10 | PRODUCT_MONITOR_PATH = "monitor" 11 | PRODUCT_THERMAL_PASTE_PATH = "thermal-paste" 12 | PRODUCT_VIDEO_CARD_PATH = "video-card" 13 | PRODUCT_CASE_FAN_PATH = "case-fan" 14 | PRODUCT_OS_PATH = "os" 15 | PRODUCT_CPU_COOLER_PATH = "cpu-cooler" 16 | PRODUCT_FAN_CONTROLLER_PATH = "fan-controller" 17 | PRODUCT_UPS_PATH = "ups" 18 | PRODUCT_WIRED_NETWORK_CARD_PATH = "wired-network-card" 19 | PRODUCT_MEMORY_PATH = "memory" 20 | PRODUCT_HEADPHONES_PATH = "headphones" 21 | PRODUCT_SOUND_CARD_PATH = "sound-card" 22 | PRODUCT_INTERNAL_HARD_DRIVE_PATH = "internal-hard-drive" 23 | PRODUCT_MOUSE_PATH = "mouse" 24 | PRODUCT_WIRELESS_NETWORK_CARD_PATH = "wireless-network-card" 25 | PRODUCT_POWER_SUPPLY_PATH = "power-supply" 26 | PRODUCT_WEBCAM_PATH = "webcam" 27 | PRODUCT_MOTHERBOARD_PATH = "motherboard" 28 | PRODUCT_EXTERNAL_HARD_DRIVE_PATH = "external-hard-drive" 29 | PRODUCT_OPTICAL_DRIVE_PATH = "optical-drive" 30 | PRODUCT_CASE_PATH = "case" 31 | PRODUCT_CPU_PATH = "cpu" 32 | 33 | PRODUCT_PATHS = ( 34 | PRODUCT_KEYBOARD_PATH, 35 | PRODUCT_SPEAKERS_PATH, 36 | PRODUCT_MONITOR_PATH, 37 | PRODUCT_THERMAL_PASTE_PATH, 38 | PRODUCT_VIDEO_CARD_PATH, 39 | PRODUCT_CASE_FAN_PATH, 40 | PRODUCT_OS_PATH, 41 | PRODUCT_CPU_COOLER_PATH, 42 | PRODUCT_FAN_CONTROLLER_PATH, 43 | PRODUCT_UPS_PATH, 44 | PRODUCT_WIRED_NETWORK_CARD_PATH, 45 | PRODUCT_MEMORY_PATH, 46 | PRODUCT_HEADPHONES_PATH, 47 | PRODUCT_SOUND_CARD_PATH, 48 | PRODUCT_INTERNAL_HARD_DRIVE_PATH, 49 | PRODUCT_MOUSE_PATH, 50 | PRODUCT_WIRELESS_NETWORK_CARD_PATH, 51 | PRODUCT_POWER_SUPPLY_PATH, 52 | PRODUCT_WEBCAM_PATH, 53 | PRODUCT_MOTHERBOARD_PATH, 54 | PRODUCT_EXTERNAL_HARD_DRIVE_PATH, 55 | PRODUCT_OPTICAL_DRIVE_PATH, 56 | PRODUCT_CASE_PATH, 57 | PRODUCT_CPU_PATH, 58 | ) 59 | -------------------------------------------------------------------------------- /pypartpicker/types.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | 4 | class Price: 5 | def __init__( 6 | self, 7 | base: Optional[float] = None, 8 | discounts: Optional[float] = None, 9 | shipping: Optional[float] = None, 10 | tax: Optional[float] = None, 11 | total: Optional[float] = None, 12 | currency: Optional[str] = None, 13 | ): 14 | self.base = base 15 | self.discounts = discounts 16 | self.shipping = shipping 17 | self.tax = tax 18 | self.total = total 19 | self.currency = currency 20 | 21 | def __repr__(self): 22 | return f"" 23 | 24 | def __str__(self): 25 | if self.currency is None: 26 | return "" 27 | return f"{self.currency}{self.total:.02f}" 28 | 29 | 30 | class Vendor: 31 | def __init__( 32 | self, name: str, logo_url: str, in_stock: bool, price: Price, buy_url: str 33 | ): 34 | self.name = name 35 | self.logo_url = logo_url 36 | self.in_stock = in_stock 37 | self.price = price 38 | self.buy_url = buy_url 39 | 40 | 41 | class Rating: 42 | def __init__(self, stars: int, count: int, average: float): 43 | self.stars = stars 44 | self.count = count 45 | self.average = average 46 | 47 | def __repr__(self): 48 | return f"" 49 | 50 | 51 | class User: 52 | def __init__( 53 | self, 54 | username: str, 55 | avatar_url: str, 56 | profile_url: str, 57 | ): 58 | self.username = username 59 | self.avatar_url = avatar_url 60 | self.profile_url = profile_url 61 | 62 | 63 | class Review: 64 | def __init__( 65 | self, 66 | author: User, 67 | points: int, 68 | stars: int, 69 | created_at: str, 70 | content: str, 71 | build_name: Optional[str] = None, 72 | build_url: Optional[str] = None, 73 | ): 74 | self.author = author 75 | self.points = points 76 | self.stars = stars 77 | self.created_at = created_at 78 | self.content = content 79 | self.build_name = build_name 80 | self.build_url = build_url 81 | 82 | 83 | class PartReviewsResult: 84 | def __init__(self, reviews: list[Review], page: int, total_pages: int): 85 | self.reviews = reviews 86 | self.page = page 87 | self.total_pages = total_pages 88 | 89 | 90 | class Part: 91 | def __init__( 92 | self, 93 | name: str, 94 | type: str, 95 | image_urls: Optional[list[str]], 96 | url: Optional[str], 97 | cheapest_price: Optional[Price], 98 | in_stock: Optional[bool], 99 | vendors: Optional[list[Vendor]] = None, 100 | rating: Optional[Rating] = None, 101 | specs: Optional[dict[str, str]] = None, 102 | reviews: Optional[list[Review]] = None, 103 | ): 104 | self.name = name 105 | self.type = type 106 | self.image_urls = image_urls 107 | self.url = url 108 | self.cheapest_price = cheapest_price 109 | self.in_stock = in_stock 110 | self.vendors = vendors 111 | self.rating = rating 112 | self.specs = specs 113 | self.reviews = reviews 114 | 115 | def __repr__(self): 116 | return f"" 117 | 118 | 119 | class PartList: 120 | def __init__( 121 | self, 122 | parts: list[Part], 123 | url: str, 124 | estimated_wattage: float, 125 | total_price: float, 126 | currency: str, 127 | ): 128 | self.parts = parts 129 | self.url = url 130 | self.estimated_wattage = estimated_wattage 131 | self.total_price = total_price 132 | self.currency = currency 133 | 134 | 135 | class PartSearchResult: 136 | def __init__(self, parts: list[Part], page: int, total_pages: int): 137 | self.parts = parts 138 | self.page = page 139 | self.total_pages = total_pages 140 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | *__pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | -------------------------------------------------------------------------------- /pypartpicker/client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from .scraper import Scraper 3 | from .types import Part, PartList, PartSearchResult, PartReviewsResult 4 | from .errors import CloudflareException, RateLimitException 5 | from requests import Response 6 | from requests_html import HTMLSession, AsyncHTMLSession 7 | from typing import Coroutine, Optional 8 | import time 9 | 10 | 11 | class Client: 12 | def __init__( 13 | self, 14 | max_retries=3, 15 | retry_delay=0, 16 | response_retriever=None, 17 | no_js=False, 18 | cookies=None, 19 | ): 20 | self.__scraper = Scraper() 21 | self.__session = HTMLSession() 22 | self.max_retries = max_retries 23 | self.retry_delay = retry_delay 24 | self.cookies = cookies 25 | self.no_js = no_js 26 | 27 | self.__get_response = ( 28 | response_retriever 29 | if response_retriever is not None 30 | else self.__default_response_retriever 31 | ) 32 | if not callable(self.__get_response): 33 | raise ValueError("response_retriever must be callable.") 34 | 35 | def __default_response_retriever(self, url: str, retries=0) -> Response: 36 | if retries >= self.max_retries: 37 | raise CloudflareException(f"Request to {url} failed, max retries exceeded.") 38 | 39 | res = self.__session.get(url, cookies=self.cookies) 40 | 41 | # Check if we are being Cloudflare checked 42 | if self.__scraper.is_cloudflare(res): 43 | if self.no_js: 44 | return self.__default_response_retriever(url, self.max_retries) 45 | 46 | res.html.render() 47 | 48 | if self.__scraper.is_cloudflare(res): 49 | time.sleep(self.retry_delay) 50 | return self.__default_response_retriever(url, retries + 1) 51 | elif self.__scraper.is_rate_limit(res): 52 | raise RateLimitException(f"PCPP rate limit encountered: {url}") 53 | 54 | return res 55 | 56 | def get_part(self, id_url: str, region: str = None) -> Part: 57 | url = self.__scraper.prepare_part_url(id_url, region) 58 | res = self.__get_response(url) 59 | return self.__scraper.parse_part(res) 60 | 61 | def get_part_list(self, id_url: str, region: str = None) -> PartList: 62 | url = self.__scraper.prepare_part_list_url(id_url, region) 63 | res = self.__get_response(url) 64 | return self.__scraper.parse_part_list(res) 65 | 66 | def get_part_search( 67 | self, query: str, page: int = 1, region: Optional[str] = None 68 | ) -> PartSearchResult: 69 | url = self.__scraper.prepare_search_url(query, page, region) 70 | res = self.__get_response(url) 71 | return self.__scraper.parse_part_search(res) 72 | 73 | def get_part_reviews( 74 | self, id_url: str, page: int = 1, rating: Optional[int] = None 75 | ) -> PartReviewsResult: 76 | url = self.__scraper.prepare_part_reviews_url(id_url, page, rating) 77 | res = self.__get_response(url) 78 | return self.__scraper.parse_reviews(res) 79 | 80 | # def get_parts( 81 | # self, 82 | # product_path: str, 83 | # page: int = 1, 84 | # region: Optional[str] = None, 85 | # compatible_with: Optional[str] = None, 86 | # ) -> PartSearchResult: 87 | # url = self.__scraper.prepare_parts_url( 88 | # product_path, page, region, compatible_with 89 | # ) 90 | # res = self.__get_response(url) 91 | # res.html.render() 92 | # return self.__scraper.parse_parts(res) 93 | 94 | 95 | class AsyncClient: 96 | def __init__( 97 | self, 98 | max_retries=3, 99 | retry_delay=0, 100 | response_retriever=None, 101 | cookies=None, 102 | no_js=False, 103 | ): 104 | self.__scraper = Scraper() 105 | self.__session = None 106 | self.max_retries = max_retries 107 | self.retry_delay = retry_delay 108 | self.cookies = cookies 109 | self.no_js = no_js 110 | 111 | self.__get_response = ( 112 | response_retriever 113 | if response_retriever is not None 114 | else self.__default_response_retriever 115 | ) 116 | if not callable(self.__get_response): 117 | raise ValueError("response_retriever must be callable.") 118 | 119 | async def __aenter__(self): 120 | self.__session = AsyncHTMLSession() 121 | return self 122 | 123 | async def __aexit__(self, exc_type, exc_value, traceback): 124 | await self.__session.close() 125 | 126 | async def __default_response_retriever( 127 | self, url: str, retries=0 128 | ) -> Coroutine[None, None, Response]: 129 | if retries >= self.max_retries: 130 | raise CloudflareException(f"Request to {url} failed, max retries exceeded.") 131 | 132 | res = await self.__session.get(url, cookies=self.cookies) 133 | 134 | # Check if we are being Cloudflare checked 135 | if self.__scraper.is_cloudflare(res): 136 | if self.no_js: 137 | return await self.__default_response_retriever(url, self.max_retries) 138 | 139 | await res.html.arender() 140 | 141 | if self.__scraper.is_cloudflare(res): 142 | asyncio.sleep(self.retry_delay) 143 | return await self.__default_response_retriever(url, retries + 1) 144 | elif self.__scraper.is_rate_limit(res): 145 | raise RateLimitException(f"PCPP rate limit encountered: {url}") 146 | 147 | return res 148 | 149 | async def get_part( 150 | self, id_url: str, region: str = None 151 | ) -> Coroutine[None, None, Part]: 152 | url = self.__scraper.prepare_part_url(id_url, region) 153 | res = await self.__get_response(url) 154 | return self.__scraper.parse_part(res) 155 | 156 | async def get_part_list( 157 | self, id_url: str, region: str = None 158 | ) -> Coroutine[None, None, PartList]: 159 | url = self.__scraper.prepare_part_list_url(id_url, region) 160 | res = await self.__get_response(url) 161 | return self.__scraper.parse_part_list(res) 162 | 163 | async def get_part_search( 164 | self, query: str, page: int = 1, region: Optional[str] = None 165 | ) -> Coroutine[None, None, PartSearchResult]: 166 | url = self.__scraper.prepare_search_url(query, page, region) 167 | res = await self.__get_response(url) 168 | return self.__scraper.parse_part_search(res) 169 | 170 | async def get_part_reviews( 171 | self, id_url: str, page: int = 1, rating: Optional[int] = None 172 | ) -> Coroutine[None, None, PartReviewsResult]: 173 | url = self.__scraper.prepare_part_reviews_url(id_url, page, rating) 174 | res = await self.__get_response(url) 175 | return self.__scraper.parse_reviews(res) 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pypartpicker 2 | 3 | A PCPartPicker data extractor for Python. 4 | 5 | ### Features: 6 | 7 | - Fetch product information, specs, pricing and reviews 8 | - Fetch part lists 9 | - Utilise PCPartPicker's built in search functionality 10 | - Scraping countermeasures out of the box via [requests-html](https://github.com/psf/requests-html>) 11 | - Support for all regions 12 | - Customisable scraping 13 | 14 | # Table of Contents 15 | 16 | - [Installation](#installation) 17 | - [Examples](#examples) 18 | - [Documentation](#documentation) 19 | - [Client](#client) 20 | - [Part](#part) 21 | - [PartList](#part-list) 22 | - [PartSearchResult](#part-search-result) 23 | - [PartReviewsResult](#part-reviews-result) 24 | - [Price](#price) 25 | - [Vendor](#vendor) 26 | - [Rating](#rating) 27 | - [Review](#review) 28 | - [User](#user) 29 | - [Supported Regions](#regions) 30 | - [Supported Product Types](#types) 31 | - [FAQs](#faqs) 32 | 33 | # Installation 34 | 35 | ```bash 36 | $ pip install pypartpicker 37 | ``` 38 | 39 | # Note 40 | 41 | Due to [pyppeteer](https://github.com/pyppeteer/pyppeteer) your first use of the library may install a chromium browser for JS rendering. 42 | 43 | This is only done once. If you would like to disable this feature entirely, use the `no_js=True` option in the Client constructor. 44 | 45 | # Examples 46 | 47 | Fetch a product: 48 | 49 | ```py 50 | import pypartpicker 51 | 52 | pcpp = pypartpicker.Client() 53 | part = pcpp.get_part("https://pcpartpicker.com/product/fN88TW") 54 | 55 | for spec, value in part.specs.items(): 56 | print(f"{spec}: {value}") 57 | 58 | print(part.cheapest_price) 59 | ``` 60 | 61 | Search parts with pagination: 62 | 63 | ```py 64 | import pypartpicker 65 | 66 | pcpp = pypartpicker.Client() 67 | page = 1 68 | 69 | while True: 70 | result = pcpp.get_part_search("ryzen 5", region="uk", page=page) 71 | 72 | for part in result.parts: 73 | print(part.name) 74 | 75 | page += 1 76 | if page > result.total_pages: 77 | break 78 | ``` 79 | 80 | Fetch a product (async): 81 | 82 | ```py 83 | import pypartpicker 84 | import asyncio 85 | 86 | 87 | async def get_parts(): 88 | async with pypartpicker.AsyncClient() as pcpp: 89 | part = await pcpp.get_part("https://pcpartpicker.com/product/fN88TW") 90 | 91 | for spec, value in part.specs.items(): 92 | print(f"{spec}: {value}") 93 | 94 | 95 | asyncio.run(get_parts()) 96 | ``` 97 | 98 | Proxy rotation w/ response_retriever override: 99 | 100 | ```py 101 | import pypartpicker 102 | import requests_html # requires requests-html and pysocks to be installed 103 | from itertools import cycle 104 | 105 | # replace with own list of proxies 106 | list_proxy = [ 107 | "socks5://Username:Password@IP1:20000", 108 | "socks5://Username:Password@IP2:20000", 109 | "socks5://Username:Password@IP3:20000", 110 | "socks5://Username:Password@IP4:20000", 111 | ] 112 | 113 | proxy_cycle = cycle(list_proxy) 114 | session = requests_html.HTMLSession() 115 | 116 | 117 | def response_retriever(url): 118 | proxy = next(proxy_cycle) 119 | return session.get(url, proxies={"http": proxy, "https": proxy}) 120 | 121 | 122 | client = pypartpicker.Client(response_retriever=response_retriever) 123 | 124 | res = client.get_part_search("cpu") 125 | for result in res.parts: 126 | part = client.get_part(result.url) 127 | print(part.specs) 128 | ``` 129 | 130 | # Documentation 131 | 132 |

Client

133 | 134 | Represents a client for interacting with parts-related data and making HTTP requests. 135 | 136 | ### Options 137 | 138 | - **`max_retries`**: `int` – The maximum number of retries for requests. Default is `3`. 139 | - **`retry_delay`**: `int` – The delay between retries in seconds. Default is `0`. 140 | - **`cookies`**: `Optional[dict]` – Cookies to include in requests. 141 | - **`response_retriever`**: `Optional[Callable]` – A custom function to perform a request, overriding the default one. 142 | Can be used to implement proxy rotation and custom scraping measures. 143 | - **`no_js`**: `bool` – Disables pyppeteer JS rendering. Default is `False`. 144 | 145 | --- 146 | 147 | ### Methods 148 | 149 | #### `get_part(id_url: str, region: str = None) -> Part` 150 | 151 | Fetches a single part by its URL/ID and region. 152 | 153 | - **Parameters**: 154 | 155 | - **`id_url`**: `str` – The part ID or URL of the part to retrieve. 156 | - **`region`**: `Optional[str]` – The region for the part data. 157 | 158 | - **Returns**: [`Part`](#part) – The part details. 159 | 160 | --- 161 | 162 | #### `get_part_list(id_url: str, region: str = None) -> PartList` 163 | 164 | Fetches a part list by its URL/ID and region. 165 | 166 | - **Parameters**: 167 | 168 | - **`id_url`**: `str` – The part list ID or URL of the part list to retrieve. 169 | - **`region`**: `Optional[str]` – The region for the part list data. 170 | 171 | - **Returns**: [`PartList`](#part-list) – The part list details. 172 | 173 | --- 174 | 175 | #### `get_part_search(query: str, page: int = 1, region: Optional[str] = None) -> PartSearchResult` 176 | 177 | Searches for parts using PCPartPicker's search functionality. 178 | 179 | - **Parameters**: 180 | 181 | - **`query`**: `str` – The search query string. 182 | - **`page`**: `int` – The page number to fetch. Default is `1`. 183 | - **`region`**: `Optional[str]` – The region for the search results. 184 | 185 | - **Returns**: [`PartSearchResult`](#part-search-result) – The search results for parts. 186 | 187 | --- 188 | 189 | #### `get_part_reviews(id_url: str, page: int = 1, rating: Optional[int] = None) -> PartReviewsResult` 190 | 191 | Fetches reviews for a specific part. 192 | 193 | - **Parameters**: 194 | 195 | - **`id_url`**: `str` – The part ID or URL of the part to retrieve reviews for. 196 | - **`page`**: `int` – The page number to fetch. Default is `1`. 197 | - **`rating`**: `Optional[int]` – Filter reviews by a specific star rating. 198 | 199 | - **Returns**: [`PartReviewsResult`](#part-reviews-result) – The reviews for the specified part. 200 | 201 | --- 202 | 203 | 216 | 217 | ### Exceptions 218 | 219 | - **`CloudflareException`** – Raised when the request fails due to Cloudflare protection after the maximum retries. 220 | - **`RateLimitException`** – Raised when the request encounters a PCPartPicker rate limit issue. 221 | 222 | --- 223 | 224 |

AsyncClient

225 | 226 | Same methods and options as Client except called with `await`. 227 | 228 | ## Types 229 | 230 |

Price

231 | 232 | Represents the pricing details of a product. 233 | 234 | - **`base`**: `Optional[float]` – The base price of the item. 235 | - **`discounts`**: `Optional[float]` – Any discounts applied to the item. 236 | - **`shipping`**: `Optional[float]` – Shipping costs associated with the item. 237 | - **`tax`**: `Optional[float]` – Taxes applied to the item. 238 | - **`total`**: `Optional[float]` – The total price after applying all factors. 239 | - **`currency`**: `Optional[str]` – The currency of the price. 240 | 241 | --- 242 | 243 |

Vendor

244 | 245 | Represents a vendor offering a product. 246 | 247 | - **`name`**: `str` – The name of the vendor. 248 | - **`logo_url`**: `str` – The URL to the vendor's logo image. 249 | - **`in_stock`**: `bool` – Whether the product is in stock. 250 | - **`price`**: [`Price`](#price) – The price details for the product. 251 | - **`buy_url`**: `str` – The vendor URL to purchase the product. 252 | 253 | --- 254 | 255 |

Rating

256 | 257 | Represents the rating of a product. 258 | 259 | - **`stars`**: `int` – The number of stars given by reviewers. 260 | - **`count`**: `int` – The total number of ratings received. 261 | - **`average`**: `float` – The average rating value. 262 | 263 | --- 264 | 265 |

User

266 | 267 | Represents a user who interacts with reviews. 268 | 269 | - **`username`**: `str` – The username of the user. 270 | - **`avatar_url`**: `str` – The URL to the user's avatar image. 271 | - **`profile_url`**: `str` – The URL to the user's profile. 272 | 273 | --- 274 | 275 |

Review

276 | 277 | Represents a review for a product. 278 | 279 | - **`author`**: [`User`](#user) – The user who wrote the review. 280 | - **`points`**: `int` – The number of points given to the review. 281 | - **`stars`**: `int` – The star rating given in the review. 282 | - **`created_at`**: `str` – The timestamp when the review was created. 283 | - **`content`**: `str` – The textual content of the review. 284 | - **`build_name`**: `Optional[str]` – The name of the build associated with the review. 285 | - **`build_url`**: `Optional[str]` – The URL to the build associated with the review. 286 | 287 | --- 288 | 289 |

PartReviewsResult

290 | 291 | Represents the result of a paginated query for part reviews. 292 | 293 | - **`reviews`**: `list` of [`Review`](#review) – A list of reviews for a product. 294 | - **`page`**: `int` – The current page of results. 295 | - **`total_pages`**: `int` – The total number of pages available. 296 | 297 | --- 298 | 299 |

Part

300 | 301 | Represents a part from a product page, part list or search page. 302 | 303 | - **`name`**: `str` – The name of the part. 304 | - **`type`**: `str` – The type or category of the part. 305 | - **`image_urls`**: `Optional[list[str]]` – Image URLs of the part. 306 | - **`url`**: `Optional[str]` – The part's main product URL. 307 | - **`cheapest_price`**: `Optional` of [`Price`](#price) – The cheapest price for the part. 308 | - **`in_stock`**: `Optional[bool]` – Whether the part is currently in stock. 309 | - **`vendors`**: `Optional[list` of [`Vendor`](#vendor)`]` – A list of vendors offering the part. 310 | - **`rating`**: `Optional` of [`Rating`](#rating) – The rating details for the part. 311 | - **`specs`**: `Optional[dict[str, str]]` – A dictionary of specifications for the part. 312 | - **`reviews`**: `Optional[list` of [`Review`](#review)`]` – A list of reviews for the part. 313 | 314 | --- 315 | 316 |

PartList

317 | 318 | Represents a list of parts for a system or build. 319 | 320 | - **`parts`**: `list` of [`Part`](#part) – A list of parts in the build (only partial data). 321 | - **`url`**: `str` – The URL for the part list. 322 | - **`estimated_wattage`**: `float` – The power consumption of the build, measured in watts. 323 | - **`total_price`**: `float` – The total price of the build. 324 | - **`currency`**: `str` – The currency used for pricing. 325 | 326 | --- 327 | 328 |

PartSearchResult

329 | 330 | Represents the result of a paginated query for parts. 331 | 332 | - **`parts`**: `list` of [`Part`](#part) – A list of parts matching the search query (only partial data). 333 | - **`page`**: `int` – The current page of results. 334 | - **`total_pages`**: `int` – The total number of pages available. 335 | 336 |

Supported Regions

337 | 338 | - **Australia**: `au` 339 | - **Austria**: `at` 340 | - **Belgium**: `be` 341 | - **Canada**: `ca` 342 | - **Czech Republic**: `cz` 343 | - **Denmark**: `dk` 344 | - **Finland**: `fi` 345 | - **France**: `fr` 346 | - **Germany**: `de` 347 | - **Hungary**: `hu` 348 | - **Ireland**: `ie` 349 | - **Italy**: `it` 350 | - **Netherlands**: `nl` 351 | - **New Zealand**: `nz` 352 | - **Norway**: `no` 353 | - **Portugal**: `pt` 354 | - **Romania**: `ro` 355 | - **Saudi Arabia**: `sa` 356 | - **Slovakia**: `sk` 357 | - **Spain**: `es` 358 | - **Sweden**: `se` 359 | - **United Kingdom**: `uk` 360 | - **United States**: `us` 361 | 362 |

Supported Product Types

363 | 364 | ```py 365 | PRODUCT_KEYBOARD_PATH = "keyboard" 366 | PRODUCT_SPEAKERS_PATH = "speakers" 367 | PRODUCT_MONITOR_PATH = "monitor" 368 | PRODUCT_THERMAL_PASTE_PATH = "thermal-paste" 369 | PRODUCT_VIDEO_CARD_PATH = "video-card" 370 | PRODUCT_CASE_FAN_PATH = "case-fan" 371 | PRODUCT_OS_PATH = "os" 372 | PRODUCT_CPU_COOLER_PATH = "cpu-cooler" 373 | PRODUCT_FAN_CONTROLLER_PATH = "fan-controller" 374 | PRODUCT_UPS_PATH = "ups" 375 | PRODUCT_WIRED_NETWORK_CARD_PATH = "wired-network-card" 376 | PRODUCT_MEMORY_PATH = "memory" 377 | PRODUCT_HEADPHONES_PATH = "headphones" 378 | PRODUCT_SOUND_CARD_PATH = "sound-card" 379 | PRODUCT_INTERNAL_HARD_DRIVE_PATH = "internal-hard-drive" 380 | PRODUCT_MOUSE_PATH = "mouse" 381 | PRODUCT_WIRELESS_NETWORK_CARD_PATH = "wireless-network-card" 382 | PRODUCT_POWER_SUPPLY_PATH = "power-supply" 383 | PRODUCT_WEBCAM_PATH = "webcam" 384 | PRODUCT_MOTHERBOARD_PATH = "motherboard" 385 | PRODUCT_EXTERNAL_HARD_DRIVE_PATH = "external-hard-drive" 386 | PRODUCT_OPTICAL_DRIVE_PATH = "optical-drive" 387 | PRODUCT_CASE_PATH = "case" 388 | PRODUCT_CPU_PATH = "cpu" 389 | ``` 390 |

FAQs

391 | 392 | **Chromium Errors** 393 | 394 | If `[INFO]: Downloading Chromium` errors are encountered, find your `__init__.py` file located in `C:\Users\yourusername\AppData\Local\Programs\Python\Python3XX\Lib\site-packages\pyppeteer`, and edit line 20 from `__chromium_revision__ = '1181205'` to `__chromium_revision__ = '1263111'` 395 | -------------------------------------------------------------------------------- /pypartpicker/scraper.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from requests_html import HTML 3 | import urllib.parse 4 | from .types import ( 5 | Part, 6 | Rating, 7 | Vendor, 8 | Price, 9 | PartList, 10 | PartSearchResult, 11 | Review, 12 | User, 13 | PartReviewsResult, 14 | ) 15 | from .urls import * 16 | from .regex import * 17 | from requests import Response 18 | 19 | 20 | class Scraper: 21 | def __init__(self): 22 | pass 23 | 24 | def __get_base_url(self, region: str) -> str: 25 | if region == "us": 26 | return "https://pcpartpicker.com" 27 | 28 | return f"https://{region}.pcpartpicker.com" 29 | 30 | def is_cloudflare(self, res: Response) -> bool: 31 | return res.html.find("title", first=True).text == "Just a moment..." 32 | 33 | def is_rate_limit(self, res: Response) -> bool: 34 | title = res.html.find(".pageTitle", first=True) 35 | if title is None: 36 | return res.html.find("title", first=True).text == "Unavailable" 37 | return title.text == "Verification" 38 | 39 | def prepare_part_url(self, id_url: str, region: str = None) -> str: 40 | match = PRODUCT_URL_RE.match(id_url) 41 | if match is None: 42 | url = ID_RE.match(id_url) 43 | if url is None: 44 | raise ValueError("Invalid pcpartpicker product URL or ID.") 45 | 46 | id_url = url.group(1) 47 | region = "us" 48 | else: 49 | region = "us" if match.group(2) is None else match.group(2)[:-1] 50 | id_url = match.group(3) 51 | 52 | if id_url is None: 53 | raise ValueError("Invalid pcpartpicker product URL or ID.") 54 | 55 | return self.__get_base_url(region) + PRODUCT_PATH + id_url 56 | 57 | def parse_part(self, res: Response) -> Part: 58 | html: HTML = res.html 59 | title_container = html.find(".wrapper__pageTitle", first=True) 60 | sidebar = html.find(".sidebar-content", first=True) 61 | 62 | # Part name and type 63 | type = title_container.find(".breadcrumb", first=True).text 64 | name = title_container.find(".pageTitle", first=True).text 65 | 66 | # Rating 67 | rating = None 68 | star_container = title_container.find(".product--rating", first=True) 69 | if star_container is not None: 70 | stars = ( 71 | len(star_container.find(".shape-star-full")) 72 | + len(star_container.find(".shape-star-half")) * 0.5 73 | ) 74 | rating_info = PRODUCT_RATINGS_RE.match( 75 | title_container.find("section div:has(ul)", first=True).text 76 | ) 77 | count = rating_info.group(1) 78 | average = rating_info.group(2) 79 | rating = Rating(stars, int(count), float(average)) 80 | 81 | # Specs 82 | specs = {} 83 | specs_block = sidebar.find(".specs", first=True) 84 | for spec in specs_block.find(".group--spec"): 85 | spec_title = spec.find(".group__title", first=True).text 86 | spec_value = spec.find(".group__content", first=True).text 87 | specs[spec_title] = spec_value 88 | 89 | # Images 90 | image_urls = [] 91 | thumbnails = sidebar.find(".product__image-2024-thumbnails", first=True) 92 | if thumbnails is None: 93 | image_urls.append( 94 | "https:" 95 | + sidebar.find(".product__image-2024 img", first=True).attrs["src"] 96 | ) 97 | else: 98 | for image in thumbnails.find("img"): 99 | image_base_url = "https:" + image.attrs["src"].split(".256p.jpg")[0] 100 | image_urls.append(image_base_url + ".1600.jpg") 101 | 102 | # Vendors 103 | vendors = [] 104 | for row in html.find("#prices table tbody tr:not(.tr--noBorder)"): 105 | vendor_image = row.find(".td__logo img", first=True) 106 | logo_url = "https:" + vendor_image.attrs["src"] 107 | vendor_name = vendor_image.attrs["alt"] 108 | 109 | # Vendor price 110 | base_price_raw = row.find(".td__base", first=True).text 111 | base_price = DECIMAL_RE.search(base_price_raw).group() 112 | currency = base_price_raw.replace(base_price, "").strip() 113 | 114 | # Discounts, shipping, tax and total price 115 | promo = ( 116 | row.find(".td__promo", first=True).text.replace(currency, "").strip() 117 | ) 118 | if promo == "": 119 | promo = "0" 120 | 121 | shipping_raw = row.find(".td__shipping", first=True) 122 | shipping = ( 123 | 0 124 | if "FREE" in shipping_raw.text 125 | or shipping_raw.text.strip() == "" 126 | or shipping_raw.find("img", first=True) is not None 127 | else DECIMAL_RE.search(shipping_raw.text).group() 128 | ) 129 | tax = row.find(".td__tax", first=True).text.replace(currency, "").strip() 130 | if tax == "": 131 | tax = "0" 132 | 133 | final = row.find(".td__finalPrice a", first=True) 134 | total_price = final.text.replace(currency, "").strip().removesuffix("+") 135 | 136 | # Availability and buy url 137 | in_stock = row.find(".td__availability--inStock", first=True) is not None 138 | buy_url = final.attrs["href"] 139 | 140 | vendors.append( 141 | Vendor( 142 | name=vendor_name, 143 | logo_url=logo_url, 144 | in_stock=in_stock, 145 | price=Price( 146 | base=float(base_price), 147 | discounts=float(promo), 148 | shipping=float(shipping), 149 | tax=float(tax), 150 | total=float(total_price), 151 | currency=currency, 152 | ), 153 | buy_url=buy_url, 154 | ) 155 | ) 156 | 157 | cheapest_price = None 158 | in_stock = False 159 | 160 | available_vendors = list(filter(lambda v: v.in_stock, vendors)) 161 | if len(available_vendors) > 0: 162 | in_stock = True 163 | cheapest_price = sorted(available_vendors, key=lambda v: v.price.total)[ 164 | 0 165 | ].price 166 | 167 | base_url = "https://" + urllib.parse.urlparse(res.url).netloc 168 | 169 | reviews = [] 170 | for review in html.find(".partReviews .partReviews__review"): 171 | reviews.append(self.parse_review(review, base_url)) 172 | 173 | return Part( 174 | name=name, 175 | type=type, 176 | image_urls=image_urls, 177 | url=res.url, 178 | cheapest_price=cheapest_price, 179 | in_stock=in_stock, 180 | vendors=vendors, 181 | rating=rating, 182 | specs=specs, 183 | reviews=reviews, 184 | ) 185 | 186 | def parse_review(self, review: HTML, base_url: str) -> Review: 187 | user_details = review.find(".userDetails", first=True) 188 | avatar_url = user_details.find("img", first=True).attrs["src"] 189 | if avatar_url.startswith("//"): 190 | avatar_url = "https:" + avatar_url 191 | else: 192 | avatar_url = base_url + avatar_url 193 | 194 | name_container = user_details.find(".userDetails__userName a", first=True) 195 | profile_url = base_url + name_container.attrs["href"] 196 | username = name_container.text 197 | 198 | user_data = user_details.find(".userDetails__userData", first=True) 199 | points = int(user_data.find("li:first-child", first=True).text.split(" ")[0]) 200 | created_at = user_data.find("li:last-child", first=True).text 201 | 202 | review_name = review.find(".partReviews__name", first=True) 203 | stars = len(review_name.find(".product--rating .shape-star-full")) 204 | 205 | build_name = None 206 | build_url = None 207 | build_a = review_name.find("a", first=True) 208 | if build_a is not None: 209 | build_name = build_a.text 210 | build_url = base_url + build_a.attrs["href"] 211 | 212 | content = review.find(".partReviews__writeup", first=True).text 213 | 214 | return Review( 215 | author=User(username, avatar_url, profile_url), 216 | points=points, 217 | stars=stars, 218 | created_at=created_at, 219 | content=content, 220 | build_name=build_name, 221 | build_url=build_url, 222 | ) 223 | 224 | def prepare_part_reviews_url( 225 | self, 226 | id_url: str, 227 | page: int = 1, 228 | rating: Optional[int] = None, 229 | ): 230 | base = self.prepare_part_url(id_url) 231 | if rating is None: 232 | return f"{base}{PART_REVIEWS_PATH}?page={page}" 233 | return f"{base}{PART_REVIEWS_PATH}?page={page}&rating={rating}" 234 | 235 | def parse_reviews(self, res: Response): 236 | html: HTML = res.html 237 | base_url = "https://" + urllib.parse.urlparse(res.url).netloc 238 | reviews = [] 239 | for review in html.find(".partReviews .partReviews__review"): 240 | reviews.append(self.parse_review(review, base_url)) 241 | 242 | pagination = html.find("#module-pagination", first=True) 243 | 244 | try: 245 | current_page = int(pagination.find(".pagination--current", first=True).text) 246 | total_pages = int(pagination.find("li:last-child", first=True).text) 247 | except AttributeError: 248 | current_page = 0 249 | total_pages = 0 250 | 251 | return PartReviewsResult( 252 | reviews=reviews, page=current_page, total_pages=total_pages 253 | ) 254 | 255 | def prepare_part_list_url(self, id_url: str, region: str = None) -> str: 256 | match = PART_LIST_URL_RE.match(id_url) 257 | override_region = region 258 | if match is None: 259 | url = ID_RE.match(id_url) 260 | if url is None: 261 | raise ValueError("Invalid pcpartpicker part list URL or ID.") 262 | 263 | id_url = url.group(1) 264 | region = "us" 265 | else: 266 | region = "us" if match.group(2) is None else match.group(2)[:-1] 267 | id_url = match.group(3) 268 | 269 | if override_region is not None: 270 | region = override_region 271 | 272 | if id_url is None: 273 | raise ValueError("Invalid pcpartpicker part list URL or ID.") 274 | 275 | return self.__get_base_url(region) + PART_LIST_PATH + id_url 276 | 277 | def parse_part_list(self, res: Response) -> PartList: 278 | html: HTML = res.html 279 | wrapper = html.find(".partlist__wrapper", first=True) 280 | part_list = html.find(".partlist", first=True) 281 | 282 | estimated_wattage = ( 283 | wrapper.find(".partlist__keyMetric", first=True) 284 | .text.removeprefix("Estimated Wattage:") 285 | .strip() 286 | ) 287 | 288 | # Parts 289 | parts = [] 290 | for row in part_list.find("table tbody tr.tr__product"): 291 | type = row.find(".td__component", first=True).text.strip() 292 | 293 | image = row.find(".td__image img", first=True) 294 | image_urls = [] 295 | if image is not None: 296 | image_urls = [image.attrs["src"]] 297 | 298 | name = "\n".join( 299 | filter( 300 | lambda s: len(s) > 0, 301 | ( 302 | row.find(".td__name", first=True) 303 | .text.replace("From parametric selection:", "") 304 | .strip() 305 | ).split("\n"), 306 | ) 307 | ) 308 | part_link = row.find(".td__name a", first=True) 309 | url = None 310 | if part_link is not None: 311 | url = ( 312 | "https://" 313 | + urllib.parse.urlparse(res.url).netloc 314 | + part_link.attrs["href"] 315 | ) 316 | 317 | base_price_raw = ( 318 | row.find(".td__base", first=True).text.replace("Base", "").strip() 319 | ) 320 | base_price = ( 321 | None 322 | if base_price_raw == "" 323 | else DECIMAL_RE.search(base_price_raw).group() 324 | ) 325 | currency = ( 326 | None if base_price is None else base_price_raw.replace(base_price, "") 327 | ) 328 | 329 | vendors = [] 330 | in_stock = False 331 | total_price = None 332 | 333 | # Price parsing is painful... they're often missing or contain weird invisible text artefacts 334 | if base_price is not None: 335 | promo_raw = row.find(".td__promo", first=True).text 336 | promo = float( 337 | 0 338 | if currency not in promo_raw 339 | else DECIMAL_RE.search(promo_raw).group() 340 | ) 341 | 342 | shipping_raw = row.find(".td__shipping", first=True).text.strip() 343 | shipping = float( 344 | 0 345 | if "FREE" in shipping_raw 346 | or shipping_raw == "" 347 | or currency not in shipping_raw 348 | else DECIMAL_RE.search(shipping_raw).group() 349 | ) 350 | 351 | tax_raw = row.find(".td__tax", first=True).text.strip() 352 | tax = float( 353 | 0 354 | if tax_raw == "" or currency not in tax_raw 355 | else DECIMAL_RE.search(tax_raw).group() 356 | ) 357 | 358 | total_price = float( 359 | DECIMAL_RE.search(row.find(".td__price", first=True).text).group() 360 | ) 361 | in_stock = True 362 | 363 | vendor = row.find(".td__where a", first=True) 364 | buy_url = vendor.attrs["href"] 365 | vendor_logo = vendor.find("img", first=True) 366 | vendor_name = vendor_logo.attrs["alt"] 367 | logo_url = "https:" + vendor_logo.attrs["src"] 368 | 369 | vendors = [ 370 | Vendor( 371 | name=vendor_name, 372 | logo_url=logo_url, 373 | in_stock=in_stock, 374 | price=Price( 375 | None if base_price is None else float(base_price), 376 | None if promo is None else -promo, 377 | shipping, 378 | tax, 379 | total_price, 380 | currency, 381 | ), 382 | buy_url=buy_url, 383 | ) 384 | ] 385 | else: 386 | total_price_raw = row.find(".td__price", first=True).text.strip() 387 | if ( 388 | "No Prices Available" not in total_price_raw 389 | and total_price_raw != "" 390 | ): 391 | total_price = DECIMAL_RE.search(total_price_raw).group() 392 | currency = ( 393 | total_price_raw.replace(total_price, "") 394 | .replace("Price", "") 395 | .strip() 396 | ) 397 | total_price = float(total_price) 398 | 399 | parts.append( 400 | Part( 401 | name, 402 | type, 403 | image_urls, 404 | url, 405 | ( 406 | Price( 407 | base=base_price, 408 | discounts=0, 409 | shipping=0, 410 | tax=0, 411 | total=total_price, 412 | currency=currency, 413 | ) 414 | if total_price is not None 415 | else ( 416 | None 417 | if vendors == [] 418 | else None if currency is None else vendors[0].price 419 | ) 420 | ), 421 | in_stock, 422 | vendors=vendors, 423 | rating=None, 424 | specs=None, 425 | ) 426 | ) 427 | 428 | currency = None 429 | total_price = 0 430 | total = part_list.find(".tr__total--final .td__price", first=True) 431 | if total is not None: 432 | total_price = DECIMAL_RE.search(total.text).group() 433 | currency = total.text.replace(total_price, "").strip() 434 | 435 | return PartList( 436 | parts=parts, 437 | url=res.url, 438 | estimated_wattage=estimated_wattage, 439 | total_price=float(total_price), 440 | currency=currency, 441 | ) 442 | 443 | def prepare_search_url(self, query: str, page: int, region: Optional[str] = "us"): 444 | return ( 445 | self.__get_base_url("us" if region is None else region) 446 | + SEARCH_PATH 447 | + f"?q={urllib.parse.quote(query)}&page={page}" 448 | ) 449 | 450 | def parse_part_search(self, res: Response) -> PartSearchResult: 451 | html: HTML = res.html 452 | 453 | # Case for which the search redirects to the product page 454 | if html.find(".pageTitle", first=True).text != "Product Search": 455 | return [self.parse_part(res)] 456 | 457 | results = [] 458 | for result in html.find(".search-results__pageContent li"): 459 | image_url = ( 460 | "https:" 461 | + result.find(".search_results--img img", first=True).attrs["src"] 462 | ) 463 | link = result.find(".search_results--link a", first=True) 464 | 465 | url = ( 466 | "https://" + urllib.parse.urlparse(res.url).netloc + link.attrs["href"] 467 | ) 468 | name = link.text 469 | 470 | price = result.find(".search_results--price", first=True).text.strip() 471 | cheapest_price = None 472 | if price != "": 473 | total = DECIMAL_RE.search(price).group() 474 | currency = price.replace(total, "").strip() 475 | cheapest_price = Price( 476 | base=None, 477 | discounts=None, 478 | shipping=None, 479 | tax=None, 480 | total=total, 481 | currency=currency, 482 | ) 483 | 484 | type = None 485 | 486 | match "(".join(name.split("(")[:-1]).split(" ")[-4:-1]: 487 | case [*_, "Processor"]: 488 | type = "CPU" 489 | case [_, "Fan", "Controller"]: 490 | type = "Fan Controller" 491 | case [_, "Network", "Adapter"]: 492 | type = "Wired Network Adapter" 493 | case [_, "Wi-Fi", "Adapter"]: 494 | type = "Wireless Network Adapter" 495 | case [_, "Video", "Card"]: 496 | type = "Video Card" 497 | case [_, "CPU", "Cooler"]: 498 | type = "CPU Cooler" 499 | case [_, "Power", "Supply"]: 500 | type = "Power Supply" 501 | case [_, "Thermal", "Paste"]: 502 | type = "Thermal Compound" 503 | case [_, "Sound", "Card"]: 504 | type = "Sound Card" 505 | case [_, "Fans", _] | [*_, "Fan"]: 506 | type = "Case Fan" 507 | case ["External", _, _] | [_, "External", _]: 508 | type = "External Storage" 509 | case [*_, "Writer"]: 510 | type = "Optical Drive" 511 | case [*_, "Headset"] | [*_, "Headphones"]: 512 | type = "Headphones" 513 | case ["Solid", "State", "Drive"] | [_, "Hard", "Drive"]: 514 | type = "Storage" 515 | case [*_, a]: 516 | type = a 517 | if "Windows" in name: 518 | type = "Operating System" 519 | 520 | results.append( 521 | Part( 522 | name=name, 523 | type=type, 524 | image_urls=[image_url], 525 | url=url, 526 | cheapest_price=cheapest_price, 527 | in_stock=cheapest_price is not None, 528 | vendors=None, 529 | rating=None, 530 | specs=None, 531 | ) 532 | ) 533 | 534 | pagination = html.find("#module-pagination", first=True) 535 | 536 | try: 537 | current_page = int(pagination.find(".pagination--current", first=True).text) 538 | total_pages = int(pagination.find("li:last-child", first=True).text) 539 | except AttributeError: 540 | current_page = 0 541 | total_pages = 0 542 | 543 | return PartSearchResult( 544 | parts=results, page=current_page, total_pages=total_pages 545 | ) 546 | 547 | # def prepare_parts_url( 548 | # self, 549 | # product_path: str, 550 | # page: int = 1, 551 | # region: Optional[str] = "us", 552 | # compatible_with: Optional[str] = None, 553 | # ): 554 | # if product_path not in PRODUCT_PATHS: 555 | # raise ValueError(f"Invalid product path: {product_path}") 556 | # region = "us" if region is None else region 557 | 558 | # if compatible_with is not None: 559 | # # Extract ID from part URL/ID 560 | # id = self.prepare_part_url(compatible_with).rsplit("/", 1)[-1] 561 | # return f"{self.__get_base_url(region)}{PRODUCTS_PATH}{product_path}?page={page}&compatible_with={id}" 562 | 563 | # return f"{self.__get_base_url(region)}{PRODUCTS_PATH}{product_path}?page={page}" 564 | 565 | # def parse_parts(self, res: Response) -> PartSearchResult: 566 | # html: HTML = res.html 567 | # table = html.find("#paginated_table", first=True) 568 | # base_url = "https://" + urllib.parse.urlparse(res.url).netloc 569 | 570 | # type = html.find(".pageTitle", first=True).text.removeprefix("Choose A").strip() 571 | 572 | # spec_titles = [] 573 | # for header in table.find("thead .th--sortable"): 574 | # if header.text in ("Name", "Rating"): 575 | # continue 576 | # spec_titles.append(header.text) 577 | 578 | # parts = [] 579 | 580 | # for row in table.find("tbody tr"): 581 | # name_a = row.find(".td__name", first=True) 582 | # name = name_a.text 583 | # url = base_url + name_a.attrs["href"] 584 | # image_url = name_a.find("img", first=True).attrs["src"] 585 | 586 | # specs = {} 587 | # for spec_key, spec_val in zip(spec_titles, row.find(".td__spec")): 588 | # specs[spec_key] = spec_val.text 589 | 590 | # rating_container = row.find(".td__rating") 591 | # ratings_count = int(rating_container.text.strip("()")) 592 | # stars = ( 593 | # len(rating_container.find(".shape-star-full")) 594 | # + len(rating_container.find(".shape-star-half")) * 0.5 595 | # ) 596 | 597 | # rating = Rating(stars=stars, count=ratings_count, average=None) 598 | 599 | # cheapest_price = None 600 | # in_stock = False 601 | # total_price_raw = row.find(".td__price").text.removesuffix("Add").strip() 602 | # if total_price_raw is not None: 603 | # total_price = DECIMAL_RE.search(total_price_raw) 604 | # currency = total_price_raw.replace(total_price, "") 605 | # cheapest_price = Price( 606 | # None, None, None, None, float(total_price), currency 607 | # ) 608 | # in_stock = True 609 | 610 | # parts.append( 611 | # Part( 612 | # name=name, 613 | # type=type, 614 | # image_urls=[image_url], 615 | # url=url, 616 | # cheapest_price=cheapest_price, 617 | # in_stock=in_stock, 618 | # vendors=None, 619 | # rating=rating, 620 | # specs=specs, 621 | # reviews=None, 622 | # ) 623 | # ) 624 | 625 | # pagination = html.find("#module-pagination", first=True) 626 | 627 | # try: 628 | # current_page = int(pagination.find(".pagination--current", first=True).text) 629 | # total_pages = int(pagination.find("li:last-child", first=True).text) 630 | # except AttributeError: 631 | # current_page = 0 632 | # total_pages = 0 633 | 634 | # return PartSearchResult(parts=parts, page=current_page, total_pages=total_pages) 635 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "appdirs" 5 | version = "1.4.4" 6 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 7 | optional = false 8 | python-versions = "*" 9 | files = [ 10 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 11 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 12 | ] 13 | 14 | [[package]] 15 | name = "beautifulsoup4" 16 | version = "4.12.3" 17 | description = "Screen-scraping library" 18 | optional = false 19 | python-versions = ">=3.6.0" 20 | files = [ 21 | {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, 22 | {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, 23 | ] 24 | 25 | [package.dependencies] 26 | soupsieve = ">1.2" 27 | 28 | [package.extras] 29 | cchardet = ["cchardet"] 30 | chardet = ["chardet"] 31 | charset-normalizer = ["charset-normalizer"] 32 | html5lib = ["html5lib"] 33 | lxml = ["lxml"] 34 | 35 | [[package]] 36 | name = "bs4" 37 | version = "0.0.2" 38 | description = "Dummy package for Beautiful Soup (beautifulsoup4)" 39 | optional = false 40 | python-versions = "*" 41 | files = [ 42 | {file = "bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc"}, 43 | {file = "bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925"}, 44 | ] 45 | 46 | [package.dependencies] 47 | beautifulsoup4 = "*" 48 | 49 | [[package]] 50 | name = "certifi" 51 | version = "2024.12.14" 52 | description = "Python package for providing Mozilla's CA Bundle." 53 | optional = false 54 | python-versions = ">=3.6" 55 | files = [ 56 | {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, 57 | {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, 58 | ] 59 | 60 | [[package]] 61 | name = "charset-normalizer" 62 | version = "3.4.0" 63 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 64 | optional = false 65 | python-versions = ">=3.7.0" 66 | files = [ 67 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, 68 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, 69 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, 70 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, 71 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, 72 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, 73 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, 74 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, 75 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, 76 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, 77 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, 78 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, 79 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, 80 | {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, 81 | {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, 82 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, 83 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, 84 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, 85 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, 86 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, 87 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, 88 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, 89 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, 90 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, 91 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, 92 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, 93 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, 94 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, 95 | {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, 96 | {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, 97 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, 98 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, 99 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, 100 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, 101 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, 102 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, 103 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, 104 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, 105 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, 106 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, 107 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, 108 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, 109 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, 110 | {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, 111 | {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, 112 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, 113 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, 114 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, 115 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, 116 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, 117 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, 118 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, 119 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, 120 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, 121 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, 122 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, 123 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, 124 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, 125 | {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, 126 | {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, 127 | {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, 128 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, 129 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, 130 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, 131 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, 132 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, 133 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, 134 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, 135 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, 136 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, 137 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, 138 | {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, 139 | {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, 140 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, 141 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, 142 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, 143 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, 144 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, 145 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, 146 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, 147 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, 148 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, 149 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, 150 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, 151 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, 152 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, 153 | {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, 154 | {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, 155 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, 156 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, 157 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, 158 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, 159 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, 160 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, 161 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, 162 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, 163 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, 164 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, 165 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, 166 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, 167 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, 168 | {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, 169 | {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, 170 | {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, 171 | {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, 172 | ] 173 | 174 | [[package]] 175 | name = "colorama" 176 | version = "0.4.6" 177 | description = "Cross-platform colored terminal text." 178 | optional = false 179 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 180 | files = [ 181 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 182 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 183 | ] 184 | 185 | [[package]] 186 | name = "cssselect" 187 | version = "1.2.0" 188 | description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" 189 | optional = false 190 | python-versions = ">=3.7" 191 | files = [ 192 | {file = "cssselect-1.2.0-py2.py3-none-any.whl", hash = "sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e"}, 193 | {file = "cssselect-1.2.0.tar.gz", hash = "sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc"}, 194 | ] 195 | 196 | [[package]] 197 | name = "fake-useragent" 198 | version = "2.0.3" 199 | description = "Up-to-date simple useragent faker with real world database" 200 | optional = false 201 | python-versions = ">=3.9" 202 | files = [ 203 | {file = "fake_useragent-2.0.3-py3-none-any.whl", hash = "sha256:8bae50abb72c309a5b3ae2f01a0b82426613fd5c4e2a04dca9332399ec44daa1"}, 204 | {file = "fake_useragent-2.0.3.tar.gz", hash = "sha256:af86a26ef8229efece8fed529b4aeb5b73747d889b60f01cd477b6f301df46e6"}, 205 | ] 206 | 207 | [[package]] 208 | name = "idna" 209 | version = "3.10" 210 | description = "Internationalized Domain Names in Applications (IDNA)" 211 | optional = false 212 | python-versions = ">=3.6" 213 | files = [ 214 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 215 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 216 | ] 217 | 218 | [package.extras] 219 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 220 | 221 | [[package]] 222 | name = "lxml" 223 | version = "5.3.0" 224 | description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." 225 | optional = false 226 | python-versions = ">=3.6" 227 | files = [ 228 | {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, 229 | {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, 230 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a"}, 231 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"}, 232 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330"}, 233 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965"}, 234 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22"}, 235 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b"}, 236 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7"}, 237 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8"}, 238 | {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32"}, 239 | {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86"}, 240 | {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5"}, 241 | {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03"}, 242 | {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7"}, 243 | {file = "lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80"}, 244 | {file = "lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3"}, 245 | {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b"}, 246 | {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18"}, 247 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442"}, 248 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4"}, 249 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f"}, 250 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e"}, 251 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c"}, 252 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16"}, 253 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79"}, 254 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080"}, 255 | {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654"}, 256 | {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d"}, 257 | {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763"}, 258 | {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec"}, 259 | {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be"}, 260 | {file = "lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9"}, 261 | {file = "lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1"}, 262 | {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, 263 | {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, 264 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, 265 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, 266 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, 267 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, 268 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, 269 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, 270 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, 271 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, 272 | {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, 273 | {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, 274 | {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, 275 | {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, 276 | {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, 277 | {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, 278 | {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, 279 | {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, 280 | {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, 281 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, 282 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, 283 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, 284 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, 285 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, 286 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, 287 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, 288 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, 289 | {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, 290 | {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, 291 | {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, 292 | {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, 293 | {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, 294 | {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, 295 | {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, 296 | {file = "lxml-5.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e"}, 297 | {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f"}, 298 | {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94"}, 299 | {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99"}, 300 | {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237"}, 301 | {file = "lxml-5.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577"}, 302 | {file = "lxml-5.3.0-cp36-cp36m-win32.whl", hash = "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70"}, 303 | {file = "lxml-5.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c"}, 304 | {file = "lxml-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033"}, 305 | {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391"}, 306 | {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6"}, 307 | {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d"}, 308 | {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512"}, 309 | {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b"}, 310 | {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5"}, 311 | {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11"}, 312 | {file = "lxml-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84"}, 313 | {file = "lxml-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e"}, 314 | {file = "lxml-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753"}, 315 | {file = "lxml-5.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040"}, 316 | {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22"}, 317 | {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22"}, 318 | {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15"}, 319 | {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920"}, 320 | {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945"}, 321 | {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42"}, 322 | {file = "lxml-5.3.0-cp38-cp38-win32.whl", hash = "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e"}, 323 | {file = "lxml-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903"}, 324 | {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de"}, 325 | {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc"}, 326 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be"}, 327 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a"}, 328 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540"}, 329 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70"}, 330 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa"}, 331 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf"}, 332 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229"}, 333 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe"}, 334 | {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2"}, 335 | {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71"}, 336 | {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3"}, 337 | {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727"}, 338 | {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a"}, 339 | {file = "lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff"}, 340 | {file = "lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2"}, 341 | {file = "lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c"}, 342 | {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a"}, 343 | {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005"}, 344 | {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce"}, 345 | {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83"}, 346 | {file = "lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba"}, 347 | {file = "lxml-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27"}, 348 | {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b"}, 349 | {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce"}, 350 | {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e"}, 351 | {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f"}, 352 | {file = "lxml-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875"}, 353 | {file = "lxml-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19"}, 354 | {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2"}, 355 | {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab"}, 356 | {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469"}, 357 | {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8"}, 358 | {file = "lxml-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1"}, 359 | {file = "lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21"}, 360 | {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2"}, 361 | {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f"}, 362 | {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab"}, 363 | {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9"}, 364 | {file = "lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c"}, 365 | {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, 366 | ] 367 | 368 | [package.dependencies] 369 | lxml-html-clean = {version = "*", optional = true, markers = "extra == \"html-clean\""} 370 | 371 | [package.extras] 372 | cssselect = ["cssselect (>=0.7)"] 373 | html-clean = ["lxml-html-clean"] 374 | html5 = ["html5lib"] 375 | htmlsoup = ["BeautifulSoup4"] 376 | source = ["Cython (>=3.0.11)"] 377 | 378 | [[package]] 379 | name = "lxml-html-clean" 380 | version = "0.4.1" 381 | description = "HTML cleaner from lxml project" 382 | optional = false 383 | python-versions = "*" 384 | files = [ 385 | {file = "lxml_html_clean-0.4.1-py3-none-any.whl", hash = "sha256:b704f2757e61d793b1c08bf5ad69e4c0b68d6696f4c3c1429982caf90050bcaf"}, 386 | {file = "lxml_html_clean-0.4.1.tar.gz", hash = "sha256:40c838bbcf1fc72ba4ce811fbb3135913017b27820d7c16e8bc412ae1d8bc00b"}, 387 | ] 388 | 389 | [package.dependencies] 390 | lxml = "*" 391 | 392 | [[package]] 393 | name = "parse" 394 | version = "1.20.2" 395 | description = "parse() is the opposite of format()" 396 | optional = false 397 | python-versions = "*" 398 | files = [ 399 | {file = "parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558"}, 400 | {file = "parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce"}, 401 | ] 402 | 403 | [[package]] 404 | name = "pyee" 405 | version = "12.1.1" 406 | description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" 407 | optional = false 408 | python-versions = ">=3.8" 409 | files = [ 410 | {file = "pyee-12.1.1-py3-none-any.whl", hash = "sha256:18a19c650556bb6b32b406d7f017c8f513aceed1ef7ca618fb65de7bd2d347ef"}, 411 | {file = "pyee-12.1.1.tar.gz", hash = "sha256:bbc33c09e2ff827f74191e3e5bbc6be7da02f627b7ec30d86f5ce1a6fb2424a3"}, 412 | ] 413 | 414 | [package.dependencies] 415 | typing-extensions = "*" 416 | 417 | [package.extras] 418 | dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio", "pytest-trio", "sphinx", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"] 419 | 420 | [[package]] 421 | name = "pyppeteer" 422 | version = "0.0.25" 423 | description = "Headless chrome/chromium automation library (unofficial port of puppeteer)" 424 | optional = false 425 | python-versions = ">=3.5" 426 | files = [ 427 | {file = "pyppeteer-0.0.25.tar.gz", hash = "sha256:51fe769b722a1718043b74d12c20420f29e0dd9eeea2b66652b7f93a9ad465dd"}, 428 | ] 429 | 430 | [package.dependencies] 431 | appdirs = "*" 432 | pyee = "*" 433 | tqdm = "*" 434 | urllib3 = "*" 435 | websockets = "*" 436 | 437 | [[package]] 438 | name = "pyquery" 439 | version = "2.0.1" 440 | description = "A jquery-like library for python" 441 | optional = false 442 | python-versions = "*" 443 | files = [ 444 | {file = "pyquery-2.0.1-py3-none-any.whl", hash = "sha256:aedfa0bd0eb9afc94b3ddbec8f375a6362b32bc9662f46e3e0d866483f4771b0"}, 445 | {file = "pyquery-2.0.1.tar.gz", hash = "sha256:0194bb2706b12d037db12c51928fe9ebb36b72d9e719565daba5a6c595322faf"}, 446 | ] 447 | 448 | [package.dependencies] 449 | cssselect = ">=1.2.0" 450 | lxml = ">=2.1" 451 | 452 | [package.extras] 453 | test = ["pytest", "pytest-cov", "requests", "webob", "webtest"] 454 | 455 | [[package]] 456 | name = "requests" 457 | version = "2.32.3" 458 | description = "Python HTTP for Humans." 459 | optional = false 460 | python-versions = ">=3.8" 461 | files = [ 462 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 463 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 464 | ] 465 | 466 | [package.dependencies] 467 | certifi = ">=2017.4.17" 468 | charset-normalizer = ">=2,<4" 469 | idna = ">=2.5,<4" 470 | urllib3 = ">=1.21.1,<3" 471 | 472 | [package.extras] 473 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 474 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 475 | 476 | [[package]] 477 | name = "requests-html" 478 | version = "0.10.0" 479 | description = "HTML Parsing for Humans." 480 | optional = false 481 | python-versions = ">=3.6.0" 482 | files = [ 483 | {file = "requests-html-0.10.0.tar.gz", hash = "sha256:7e929ecfed95fb1d0994bb368295d6d7c4d06b03fcb900c33d7d0b17e6003947"}, 484 | {file = "requests_html-0.10.0-py3-none-any.whl", hash = "sha256:cb8a78cf829c4eca9d6233f28524f65dd2bfaafb4bdbbc407f0a0b8f487df6e2"}, 485 | ] 486 | 487 | [package.dependencies] 488 | bs4 = "*" 489 | fake-useragent = "*" 490 | parse = "*" 491 | pyppeteer = ">=0.0.14" 492 | pyquery = "*" 493 | requests = "*" 494 | w3lib = "*" 495 | 496 | [[package]] 497 | name = "soupsieve" 498 | version = "2.6" 499 | description = "A modern CSS selector implementation for Beautiful Soup." 500 | optional = false 501 | python-versions = ">=3.8" 502 | files = [ 503 | {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, 504 | {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, 505 | ] 506 | 507 | [[package]] 508 | name = "tqdm" 509 | version = "4.67.1" 510 | description = "Fast, Extensible Progress Meter" 511 | optional = false 512 | python-versions = ">=3.7" 513 | files = [ 514 | {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, 515 | {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, 516 | ] 517 | 518 | [package.dependencies] 519 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 520 | 521 | [package.extras] 522 | dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] 523 | discord = ["requests"] 524 | notebook = ["ipywidgets (>=6)"] 525 | slack = ["slack-sdk"] 526 | telegram = ["requests"] 527 | 528 | [[package]] 529 | name = "typing-extensions" 530 | version = "4.12.2" 531 | description = "Backported and Experimental Type Hints for Python 3.8+" 532 | optional = false 533 | python-versions = ">=3.8" 534 | files = [ 535 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 536 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 537 | ] 538 | 539 | [[package]] 540 | name = "urllib3" 541 | version = "2.3.0" 542 | description = "HTTP library with thread-safe connection pooling, file post, and more." 543 | optional = false 544 | python-versions = ">=3.9" 545 | files = [ 546 | {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, 547 | {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, 548 | ] 549 | 550 | [package.extras] 551 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 552 | h2 = ["h2 (>=4,<5)"] 553 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 554 | zstd = ["zstandard (>=0.18.0)"] 555 | 556 | [[package]] 557 | name = "w3lib" 558 | version = "2.2.1" 559 | description = "Library of web-related functions" 560 | optional = false 561 | python-versions = ">=3.8" 562 | files = [ 563 | {file = "w3lib-2.2.1-py3-none-any.whl", hash = "sha256:e56d81c6a6bf507d7039e0c95745ab80abd24b465eb0f248af81e3eaa46eb510"}, 564 | {file = "w3lib-2.2.1.tar.gz", hash = "sha256:756ff2d94c64e41c8d7c0c59fea12a5d0bc55e33a531c7988b4a163deb9b07dd"}, 565 | ] 566 | 567 | [[package]] 568 | name = "websockets" 569 | version = "14.1" 570 | description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 571 | optional = false 572 | python-versions = ">=3.9" 573 | files = [ 574 | {file = "websockets-14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a0adf84bc2e7c86e8a202537b4fd50e6f7f0e4a6b6bf64d7ccb96c4cd3330b29"}, 575 | {file = "websockets-14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90b5d9dfbb6d07a84ed3e696012610b6da074d97453bd01e0e30744b472c8179"}, 576 | {file = "websockets-14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2177ee3901075167f01c5e335a6685e71b162a54a89a56001f1c3e9e3d2ad250"}, 577 | {file = "websockets-14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f14a96a0034a27f9d47fd9788913924c89612225878f8078bb9d55f859272b0"}, 578 | {file = "websockets-14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f874ba705deea77bcf64a9da42c1f5fc2466d8f14daf410bc7d4ceae0a9fcb0"}, 579 | {file = "websockets-14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9607b9a442392e690a57909c362811184ea429585a71061cd5d3c2b98065c199"}, 580 | {file = "websockets-14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bea45f19b7ca000380fbd4e02552be86343080120d074b87f25593ce1700ad58"}, 581 | {file = "websockets-14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:219c8187b3ceeadbf2afcf0f25a4918d02da7b944d703b97d12fb01510869078"}, 582 | {file = "websockets-14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad2ab2547761d79926effe63de21479dfaf29834c50f98c4bf5b5480b5838434"}, 583 | {file = "websockets-14.1-cp310-cp310-win32.whl", hash = "sha256:1288369a6a84e81b90da5dbed48610cd7e5d60af62df9851ed1d1d23a9069f10"}, 584 | {file = "websockets-14.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0744623852f1497d825a49a99bfbec9bea4f3f946df6eb9d8a2f0c37a2fec2e"}, 585 | {file = "websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512"}, 586 | {file = "websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac"}, 587 | {file = "websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280"}, 588 | {file = "websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1"}, 589 | {file = "websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3"}, 590 | {file = "websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6"}, 591 | {file = "websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0"}, 592 | {file = "websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89"}, 593 | {file = "websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23"}, 594 | {file = "websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e"}, 595 | {file = "websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09"}, 596 | {file = "websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed"}, 597 | {file = "websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d"}, 598 | {file = "websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707"}, 599 | {file = "websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a"}, 600 | {file = "websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45"}, 601 | {file = "websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58"}, 602 | {file = "websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058"}, 603 | {file = "websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4"}, 604 | {file = "websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05"}, 605 | {file = "websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0"}, 606 | {file = "websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f"}, 607 | {file = "websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9"}, 608 | {file = "websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b"}, 609 | {file = "websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3"}, 610 | {file = "websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59"}, 611 | {file = "websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2"}, 612 | {file = "websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da"}, 613 | {file = "websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9"}, 614 | {file = "websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7"}, 615 | {file = "websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a"}, 616 | {file = "websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6"}, 617 | {file = "websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0"}, 618 | {file = "websockets-14.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:01bb2d4f0a6d04538d3c5dfd27c0643269656c28045a53439cbf1c004f90897a"}, 619 | {file = "websockets-14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:414ffe86f4d6f434a8c3b7913655a1a5383b617f9bf38720e7c0799fac3ab1c6"}, 620 | {file = "websockets-14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fda642151d5affdee8a430bd85496f2e2517be3a2b9d2484d633d5712b15c56"}, 621 | {file = "websockets-14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd7c11968bc3860d5c78577f0dbc535257ccec41750675d58d8dc66aa47fe52c"}, 622 | {file = "websockets-14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a032855dc7db987dff813583d04f4950d14326665d7e714d584560b140ae6b8b"}, 623 | {file = "websockets-14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7e7ea2f782408c32d86b87a0d2c1fd8871b0399dd762364c731d86c86069a78"}, 624 | {file = "websockets-14.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:39450e6215f7d9f6f7bc2a6da21d79374729f5d052333da4d5825af8a97e6735"}, 625 | {file = "websockets-14.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ceada5be22fa5a5a4cdeec74e761c2ee7db287208f54c718f2df4b7e200b8d4a"}, 626 | {file = "websockets-14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3fc753451d471cff90b8f467a1fc0ae64031cf2d81b7b34e1811b7e2691bc4bc"}, 627 | {file = "websockets-14.1-cp39-cp39-win32.whl", hash = "sha256:14839f54786987ccd9d03ed7f334baec0f02272e7ec4f6e9d427ff584aeea8b4"}, 628 | {file = "websockets-14.1-cp39-cp39-win_amd64.whl", hash = "sha256:d9fd19ecc3a4d5ae82ddbfb30962cf6d874ff943e56e0c81f5169be2fda62979"}, 629 | {file = "websockets-14.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5dc25a9dbd1a7f61eca4b7cb04e74ae4b963d658f9e4f9aad9cd00b688692c8"}, 630 | {file = "websockets-14.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:04a97aca96ca2acedf0d1f332c861c5a4486fdcba7bcef35873820f940c4231e"}, 631 | {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df174ece723b228d3e8734a6f2a6febbd413ddec39b3dc592f5a4aa0aff28098"}, 632 | {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:034feb9f4286476f273b9a245fb15f02c34d9586a5bc936aff108c3ba1b21beb"}, 633 | {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c308dabd2b380807ab64b62985eaccf923a78ebc572bd485375b9ca2b7dc7"}, 634 | {file = "websockets-14.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a42d3ecbb2db5080fc578314439b1d79eef71d323dc661aa616fb492436af5d"}, 635 | {file = "websockets-14.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddaa4a390af911da6f680be8be4ff5aaf31c4c834c1a9147bc21cbcbca2d4370"}, 636 | {file = "websockets-14.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a4c805c6034206143fbabd2d259ec5e757f8b29d0a2f0bf3d2fe5d1f60147a4a"}, 637 | {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:205f672a6c2c671a86d33f6d47c9b35781a998728d2c7c2a3e1cf3333fcb62b7"}, 638 | {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef440054124728cc49b01c33469de06755e5a7a4e83ef61934ad95fc327fbb0"}, 639 | {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7591d6f440af7f73c4bd9404f3772bfee064e639d2b6cc8c94076e71b2471c1"}, 640 | {file = "websockets-14.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:25225cc79cfebc95ba1d24cd3ab86aaa35bcd315d12fa4358939bd55e9bd74a5"}, 641 | {file = "websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e"}, 642 | {file = "websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8"}, 643 | ] 644 | 645 | [metadata] 646 | lock-version = "2.0" 647 | python-versions = "^3.10" 648 | content-hash = "48ad7f14711359f978601eee9b7b37677c5fd04380051e69e98b1ae0c390867e" 649 | --------------------------------------------------------------------------------