├── tests ├── __init__.py ├── utils │ ├── __init__.py │ └── test_email_filter.py ├── browsers │ ├── __init__.py │ ├── test_requests_browser.py │ └── test_chrome_browser.py ├── models │ ├── __init__.py │ └── test_page_data.py ├── data_extractors │ ├── __init__.py │ ├── test_email_extractor.py │ └── test_linkedin_extractor.py └── link_filters │ ├── __init__.py │ ├── test_default_link_filter.py │ ├── test_link_filter_base.py │ └── test_contact_link_filter.py ├── extract_emails ├── console │ ├── __init__.py │ └── application.py ├── models │ ├── __init__.py │ └── page_data.py ├── data_savers │ ├── __init__.py │ ├── data_saver.py │ └── csv_saver.py ├── utils │ ├── __init__.py │ ├── email_filter.py │ └── _top_level_domains.py ├── workers │ ├── __init__.py │ └── default_worker.py ├── errors │ ├── __init__.py │ └── errors.py ├── __init__.py ├── browsers │ ├── __init__.py │ ├── page_source_getter.py │ ├── httpx_browser.py │ └── chromium_browser.py ├── data_extractors │ ├── __init__.py │ ├── data_extractor.py │ ├── linkedin_extractor.py │ └── email_extractor.py └── link_filters │ ├── __init__.py │ ├── default_link_filter.py │ ├── link_filter_base.py │ └── contact_link_filter.py ├── docs ├── code │ ├── utils.md │ ├── errors.md │ ├── models.md │ ├── workers.md │ ├── browsers.md │ ├── link_filters.md │ └── data_extractors.md ├── index.md ├── quick_start │ ├── logs.md │ ├── save_data.md │ └── intro.md └── changelogs │ └── v5.md ├── images └── email.png ├── .flake8 ├── pytest.ini ├── tox.ini ├── .cursor └── rules │ ├── tests.mdc │ ├── general.mdc │ └── uv.mdc ├── setup.cfg ├── Makefile ├── .pre-commit-config.yaml ├── LICENSE ├── CONTRIBUTING.md ├── .gitignore ├── mkdocs.yml ├── pyproject.toml ├── README.md ├── CHANGELOG.md └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/browsers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data_extractors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/link_filters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /extract_emails/console/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/code/utils.md: -------------------------------------------------------------------------------- 1 | # Utils 2 | 3 | ::: extract_emails.utils.email_filter -------------------------------------------------------------------------------- /docs/code/errors.md: -------------------------------------------------------------------------------- 1 | # Errors 2 | 3 | ::: extract_emails.errors.errors 4 | -------------------------------------------------------------------------------- /docs/code/models.md: -------------------------------------------------------------------------------- 1 | # Models 2 | 3 | ::: extract_emails.models.page_data 4 | -------------------------------------------------------------------------------- /docs/code/workers.md: -------------------------------------------------------------------------------- 1 | # Workers 2 | 3 | ::: extract_emails.workers.default_worker -------------------------------------------------------------------------------- /images/email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmitriiweb/extract-emails/HEAD/images/email.png -------------------------------------------------------------------------------- /extract_emails/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .page_data import PageData 2 | 3 | __all__ = ("PageData",) 4 | -------------------------------------------------------------------------------- /extract_emails/data_savers/__init__.py: -------------------------------------------------------------------------------- 1 | from .csv_saver import CsvSaver 2 | 3 | __all__ = ("CsvSaver",) 4 | -------------------------------------------------------------------------------- /extract_emails/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .email_filter import email_filter 2 | 3 | __all__ = ("email_filter",) 4 | -------------------------------------------------------------------------------- /extract_emails/workers/__init__.py: -------------------------------------------------------------------------------- 1 | from .default_worker import DefaultWorker 2 | 3 | __all__ = ("DefaultWorker",) 4 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | select = C,E,F,W,B,B9 4 | ignore = E203, E501, W503 5 | exclude = __init__.py -------------------------------------------------------------------------------- /extract_emails/errors/__init__.py: -------------------------------------------------------------------------------- 1 | from .errors import BrowserImportError 2 | 3 | __all__ = ("BrowserImportError",) 4 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | slow: marks tests as slow (deselect with '-m "not slow"') 4 | asyncio_mode = auto -------------------------------------------------------------------------------- /extract_emails/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "6.0.1" 2 | from .workers import DefaultWorker 3 | 4 | __all__ = ("DefaultWorker",) 5 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | isolated_build=True 3 | envlist = py310,py311,py312,313 4 | 5 | [testenv] 6 | deps = pytest 7 | commmands = 8 | pytest tests 9 | -------------------------------------------------------------------------------- /extract_emails/errors/errors.py: -------------------------------------------------------------------------------- 1 | class BrowserImportError(Exception): 2 | """Error for cases when required libraries for browsers were not installed""" 3 | 4 | pass 5 | -------------------------------------------------------------------------------- /docs/code/browsers.md: -------------------------------------------------------------------------------- 1 | # Browsers 2 | 3 | ::: extract_emails.browsers.page_source_getter 4 | 5 | ::: extract_emails.browsers.chromium_browser 6 | 7 | ::: extract_emails.browsers.httpx_browser 8 | -------------------------------------------------------------------------------- /docs/code/link_filters.md: -------------------------------------------------------------------------------- 1 | # Link Filters 2 | 3 | ::: extract_emails.link_filters.link_filter_base 4 | ::: extract_emails.link_filters.default_link_filter 5 | ::: extract_emails.link_filters.contact_link_filter 6 | -------------------------------------------------------------------------------- /docs/code/data_extractors.md: -------------------------------------------------------------------------------- 1 | # Data Extractors 2 | 3 | ::: extract_emails.data_extractors.data_extractor 4 | ::: extract_emails.data_extractors.email_extractor 5 | ::: extract_emails.data_extractors.linkedin_extractor 6 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ![Image](https://github.com/dmitriiweb/extract-emails/blob/docs_improvements/images/email.png?raw=true) 2 | 3 | ### Index 4 | - [Quick Start](quick_start/intro.md#Intro) 5 | - [Code References](code/workers.md#Workers) 6 | -------------------------------------------------------------------------------- /extract_emails/browsers/__init__.py: -------------------------------------------------------------------------------- 1 | from .chromium_browser import ChromiumBrowser 2 | from .httpx_browser import HttpxBrowser 3 | from .page_source_getter import PageSourceGetter 4 | 5 | __all__ = ("PageSourceGetter", "ChromiumBrowser", "HttpxBrowser") 6 | -------------------------------------------------------------------------------- /extract_emails/data_extractors/__init__.py: -------------------------------------------------------------------------------- 1 | from .data_extractor import DataExtractor 2 | from .email_extractor import EmailExtractor 3 | from .linkedin_extractor import LinkedinExtractor 4 | 5 | __all__ = ("DataExtractor", "EmailExtractor", "LinkedinExtractor") 6 | -------------------------------------------------------------------------------- /extract_emails/link_filters/__init__.py: -------------------------------------------------------------------------------- 1 | from .contact_link_filter import ContactInfoLinkFilter 2 | from .default_link_filter import DefaultLinkFilter 3 | from .link_filter_base import LinkFilterBase 4 | 5 | __all__ = ("LinkFilterBase", "DefaultLinkFilter", "ContactInfoLinkFilter") 6 | -------------------------------------------------------------------------------- /tests/utils/test_email_filter.py: -------------------------------------------------------------------------------- 1 | from extract_emails.utils import email_filter 2 | 3 | 4 | def test_email_filter(): 5 | test_emails = ["email@email.com", "email@email.com", "2@pic.png"] 6 | filtered_emails = email_filter(test_emails) 7 | 8 | assert len(filtered_emails) == 1 9 | assert "email@email.com" in filtered_emails 10 | -------------------------------------------------------------------------------- /extract_emails/data_savers/data_saver.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Iterable 3 | 4 | from extract_emails.models import PageData 5 | 6 | 7 | class DataSaver(ABC): 8 | def __init__(self, **kwargs): 9 | pass 10 | 11 | @abstractmethod 12 | def save(self, data: Iterable[PageData]): 13 | pass 14 | -------------------------------------------------------------------------------- /.cursor/rules/tests.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: tests/**.py 4 | alwaysApply: false 5 | --- 6 | # Tests Writing Rules 7 | 8 | - use pytest 9 | - each test - one function, don't write classes for tests 10 | - do not mark tests as @pytest.makr.asyncio, because in pyproject.toml settings up autodiscovering 11 | - run tests with: `pytest --cov=extract_emails -vv tests/` -------------------------------------------------------------------------------- /tests/data_extractors/test_email_extractor.py: -------------------------------------------------------------------------------- 1 | from extract_emails.data_extractors import EmailExtractor 2 | 3 | STRING = """ 4 | blah blah email@example.com blah blah 5 | blah blah "email2@example.com" blah blah 6 | blah blah "email2@example.com" blah blah 7 | """ 8 | 9 | 10 | def test_get_data(): 11 | email_extractor = EmailExtractor() 12 | emails = email_extractor.get_data(STRING) 13 | 14 | assert "email2@example.com" in emails 15 | assert len(emails) == 2 16 | -------------------------------------------------------------------------------- /docs/quick_start/logs.md: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | There is [loguru](https://github.com/Delgan/loguru) library under the hood. 4 | 5 | ## Settings 6 | ```python 7 | import sys 8 | 9 | from loguru import logger 10 | 11 | logger.add( 12 | sys.stderr, 13 | format="{time} {level} {message}", 14 | filter="my_module", 15 | level="INFO", 16 | ) 17 | ``` 18 | 19 | ## Disable/Enable 20 | ```python 21 | from loguru import logger 22 | 23 | logger.disable('extract_emails') 24 | logger.enable('extract_emails') 25 | ``` 26 | -------------------------------------------------------------------------------- /tests/link_filters/test_default_link_filter.py: -------------------------------------------------------------------------------- 1 | from extract_emails.link_filters import DefaultLinkFilter 2 | 3 | 4 | def test_default_link_filter(): 5 | test_urls = [ 6 | "https://example.com/page1.html", 7 | "/page.html", 8 | "/page.html", 9 | "https://google.com", 10 | ] 11 | link_filter = DefaultLinkFilter("https://example.com/") 12 | filtered_urls = link_filter.filter(test_urls) 13 | 14 | assert "https://google.com" not in filtered_urls 15 | assert len(filtered_urls) == 2 16 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | max-complexity = 24 6 | max-line-length = 90 7 | exclude = 8 | .git, 9 | .tox, 10 | .venv, 11 | __pycache__, 12 | build, 13 | dist, 14 | docs, 15 | geopy.egg-info 16 | 17 | [isort] 18 | ; https://github.com/timothycrosley/isort#multi-line-output-modes 19 | combine_as_imports = True 20 | force_grid_wrap = 0 21 | include_trailing_comma = True 22 | known_first_party = test 23 | line_length = 88 24 | multi_line_output = 3 25 | not_skip = __init__.py 26 | -------------------------------------------------------------------------------- /extract_emails/data_extractors/data_extractor.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class DataExtractor(ABC): 5 | """Base class for all data extractors""" 6 | 7 | @property 8 | @abstractmethod 9 | def name(self) -> str: 10 | """Name of the data extractor, e.g. email, linkedin""" 11 | 12 | @abstractmethod 13 | def get_data(self, page_source: str) -> set[str]: 14 | """Extract needed data from a string 15 | 16 | Args: 17 | page_source: webpage content 18 | 19 | Returns: 20 | Set of data, e.g. {'email@email.com', 'email2@email.com'} 21 | """ 22 | -------------------------------------------------------------------------------- /docs/changelogs/v5.md: -------------------------------------------------------------------------------- 1 | # V5 2 | ## 5.3.0 3 | ### Changed 4 | - Add custom save mode to csv data saver 5 | 6 | ## 5.2.0 7 | ### Added 8 | - CLI tool 9 | - csv data saver 10 | 11 | ## 5.1.3 12 | ### Changed 13 | - Update dependencies 14 | 15 | ## 5.1.2 16 | ### Added 17 | - Python 3.10 support 18 | - Add CHANGELOG.md 19 | 20 | ## 5.1.0 21 | ### Added 22 | - Add save_as_csv class method to `PageData` model 23 | - Add logs to DefaultWorker 24 | ### Changed 25 | - Check if needed libraries for browsers were installed. If not will show user-friendly error 26 | - Small improvements in the code 27 | 28 | ## 5.0.2 29 | ### Changed 30 | - Fix imports for factories and DefaultWorker 31 | -------------------------------------------------------------------------------- /tests/browsers/test_requests_browser.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from extract_emails.browsers import HttpxBrowser 4 | 5 | 6 | @pytest.fixture 7 | def browser(): 8 | browser = HttpxBrowser() 9 | browser.start() 10 | yield browser 11 | browser.stop() 12 | 13 | 14 | @pytest.mark.slow 15 | def test_get_page_source(browser): 16 | url = "https://en.wikipedia.org/wiki/Python_(programming_language)" 17 | page_source = browser.get_page_source(url) 18 | assert "Python (programming language)" in page_source 19 | 20 | 21 | @pytest.mark.slow 22 | def test_get_page_source_wrong_url(browser): 23 | url = "ttps://en.wikipedia.org/wiki/Python_(programming_language)" 24 | page_source = browser.get_page_source(url) 25 | assert page_source == "" 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | version := $(shell uv run python -c 'from extract_emails import __version__; print(__version__)') 2 | 3 | .PHONY: test 4 | test: 5 | uv run pytest --cov=extract_emails -vv -m "not slow" tests/ 6 | 7 | .PHONY: test-all 8 | test-all: 9 | uv run pytest --cov=extract_emails -vv tests/ 10 | 11 | .PHONY: format 12 | format: 13 | uv run ruff check extract_emails tests --select I --fix 14 | uv run ruff format extract_emails tests 15 | 16 | .PHONY: lint 17 | lint: 18 | uv run ruff check extract_emails 19 | uv run mypy extract_emails 20 | 21 | .PHONY: docs-serve 22 | docs-serve: 23 | uv run mkdocs serve 24 | 25 | .PHONY: docs-publish 26 | docs-publish: 27 | uv run mkdocs gh-deploy --force 28 | 29 | .PHONY: publish 30 | publish: 31 | uv build 32 | uv publish 33 | -------------------------------------------------------------------------------- /extract_emails/utils/email_filter.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | from ._top_level_domains import TOP_LEVEL_DOMAINS 4 | 5 | 6 | def email_filter(emails: Iterable[str]) -> set[str]: 7 | """Remove duplicated emails and strings looks like emails (2@pic.png) 8 | 9 | Examples: 10 | >>> from extract_emails.utils import email_filter 11 | >>> test_emails = ["email@email.com", "email@email.com", "2@pic.png"] 12 | >>> filtered_emails = email_filter(test_emails) 13 | >>> filtered_emails 14 | {"email@email.com"} 15 | 16 | Args: 17 | emails: List of new emails 18 | 19 | Returns: 20 | List of filtered emails 21 | """ 22 | return set( 23 | email for email in emails if "." + email.split(".")[-1] in TOP_LEVEL_DOMAINS 24 | ) 25 | -------------------------------------------------------------------------------- /tests/browsers/test_chrome_browser.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pytest_asyncio 3 | 4 | from extract_emails.browsers import ChromiumBrowser 5 | 6 | @pytest_asyncio.fixture 7 | async def browser(): 8 | browser = ChromiumBrowser() 9 | await browser.astart() 10 | yield browser 11 | await browser.astop() 12 | 13 | @pytest.mark.slow 14 | async def test_get_page_source(browser): 15 | url = "https://en.wikipedia.org/wiki/Python_(programming_language)" 16 | page_source = await browser.aget_page_source(url) 17 | assert "Python (programming language)" in page_source 18 | 19 | @pytest.mark.slow 20 | async def test_get_page_source_wrong_url(browser): 21 | url = "ttps://en.wikipedia.org/wiki/Python_(programming_language)" 22 | page_source = await browser.aget_page_source(url) 23 | assert page_source == "" 24 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 20.8b1 4 | hooks: 5 | - id: black 6 | 7 | - repo: https://gitlab.com/pycqa/flake8 8 | rev: 3.8.4 9 | hooks: 10 | - id: flake8 11 | 12 | - repo: https://github.com/timothycrosley/isort 13 | rev: 5.7.0 14 | hooks: 15 | - id: isort 16 | additional_dependencies: [toml] 17 | exclude: ^.*/?setup\.py$ 18 | 19 | - repo: https://github.com/pre-commit/pre-commit-hooks 20 | rev: v3.4.0 21 | hooks: 22 | - id: trailing-whitespace 23 | exclude: | 24 | (?x)( 25 | ^tests/.*/fixtures/.* 26 | | ^tests/console/commands/debug/test_resolve.py 27 | ) 28 | - id: end-of-file-fixer 29 | exclude: ^tests/.*/fixtures/.* 30 | - id: debug-statements 31 | -------------------------------------------------------------------------------- /tests/link_filters/test_link_filter_base.py: -------------------------------------------------------------------------------- 1 | from extract_emails.link_filters import LinkFilterBase 2 | 3 | HTML_EXAMPLE = """ 4 | 5 | 6 | blah 7 | 8 | 9 | link 10 | link 11 | link 12 | 13 | 14 | """ 15 | 16 | 17 | def test_get_website_address_valid_url(): 18 | website = LinkFilterBase.get_website_address("https://example.com/list?page=1") 19 | assert website == "https://example.com/" 20 | 21 | website = LinkFilterBase.get_website_address( 22 | "https://subexample.example.com/list?page=1" 23 | ) 24 | assert website == "https://subexample.example.com/" 25 | 26 | 27 | def test_get_links(): 28 | links = LinkFilterBase.get_links(HTML_EXAMPLE) 29 | assert links == ["example.com", "/example2.com", "https://example2.com"] 30 | -------------------------------------------------------------------------------- /docs/quick_start/save_data.md: -------------------------------------------------------------------------------- 1 | # Save Data 2 | 3 | Data store as [pydantic](https://pydantic-docs.helpmanual.io) models 4 | 5 | ## Save as CSV 6 | ```python 7 | from extract_emails import DefaultFilterAndEmailFactory as Factory 8 | from extract_emails.browsers.requests_browser import RequestsBrowser as Browser 9 | from extract_emails.models import PageData 10 | from extract_emails.workers import DefaultWorker 11 | 12 | 13 | browser = Browser() 14 | extractor = DefaultWorker("https://example.com", browser) 15 | data = extractor.get_data() 16 | 17 | PageData.save_as_csv(data, "output.csv") 18 | # cat output.csv 19 | website,page_url,email 20 | https://example.com,https://example.com/about-us,email@example.com 21 | https://example.com,https://example.com/about-us,email1@example.com 22 | https://example.com,https://example.com/about-us,email2@example.com 23 | https://example.com,https://example.com/about-us,email3@example.com 24 | 25 | ``` 26 | -------------------------------------------------------------------------------- /tests/data_extractors/test_linkedin_extractor.py: -------------------------------------------------------------------------------- 1 | from extract_emails.data_extractors import LinkedinExtractor 2 | 3 | STRING = """ 4 | The good thing about your LinkedIn URL is that you can have your customized URL. By default, LinkedIn will provide you a profile URL that is alphanumeric and is a combination of your name and numbers. 5 | For example, it will look something similar to this: https://www.linkedin.com/in/venjamin-brant-73381ujy3u 6 | The good thing about your LinkedIn URL is that you can have your customized URL. By default, LinkedIn will provide you a profile URL that is alphanumeric and is a combination of your name and numbers. 7 | For example, it will look something similar to this: https://www.linkedin.com/in/venjamin-brant-73381ujy3u 8 | """ 9 | 10 | 11 | def test_linkedin_get_data(): 12 | linkedin_extractor = LinkedinExtractor() 13 | urls = linkedin_extractor.get_data(STRING) 14 | assert len(urls) == 1 15 | assert "https://www.linkedin.com/in/venjamin-brant-73381ujy3u" in urls 16 | -------------------------------------------------------------------------------- /extract_emails/data_extractors/linkedin_extractor.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from extract_emails.data_extractors import DataExtractor 4 | 5 | 6 | class LinkedinExtractor(DataExtractor): 7 | def __init__(self): 8 | self.regexp = re.compile( 9 | r"(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»“”‘’]))" 10 | ) 11 | 12 | @property 13 | def name(self) -> str: 14 | return "linkedin" 15 | 16 | def get_data(self, page_source: str) -> set[str]: 17 | """Extract links to Linkedin profiles 18 | 19 | Args: 20 | page_source: webpage content 21 | 22 | Returns: 23 | Set of urls, e.g. {'https://www.linkedin.com/in/venjamin-brant-73381ujy3u'} 24 | """ 25 | all_urls = self.regexp.findall(page_source) 26 | url_filter = "linkedin.com/in/" 27 | linkedin_urls = set([i[0] for i in all_urls if url_filter in i[0]]) 28 | return linkedin_urls 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Dmitrii K 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /extract_emails/data_extractors/email_extractor.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from extract_emails.data_extractors import DataExtractor 4 | from extract_emails.utils import email_filter 5 | 6 | 7 | class EmailExtractor(DataExtractor): 8 | def __init__(self): 9 | self.regexp = re.compile( 10 | r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""" 11 | ) 12 | 13 | @property 14 | def name(self) -> str: 15 | return "email" 16 | 17 | def get_data(self, page_source: str) -> set[str]: 18 | """Extract emails from a string 19 | 20 | Args: 21 | page_source: webpage content 22 | 23 | Returns: 24 | Set of emails, e.g. {'email@email.com', 'email2@email.com'} 25 | """ 26 | raw_emails = [i for i in self.regexp.findall(page_source)] 27 | return email_filter(raw_emails) 28 | -------------------------------------------------------------------------------- /.cursor/rules/general.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # Code Rules 7 | 8 | This is a project to extract emails or urls to linkedin accounts from web sites 9 | 10 | ### General Guidelines 11 | - Use Python 3.10+ syntax. 12 | - Follow PEP 8 coding standards. 13 | - Include type hints for all function definitions. 14 | - Add docstrings to all public classes and functions. 15 | - Prefer `pathlib` over `os.path` for file system operations. 16 | - Utilize f-strings for string formatting. 17 | - Avoid using wildcard imports. 18 | - Write idiomatic and readable code. 19 | - Do not write comments for tests 20 | - Do not write comments where you are describing what was changed 21 | - Do not wtrite try/except blocks for large blocks of code, but only for specific lines where exceptions are expected. 22 | - Do not use "from typing install List, Dict, Option", but use python 3.10+ syntax like `list`, `dict`, and `|`. 23 | - Use "" (double quotes) for strings and '' (single quotes) for chars only 24 | - Write docstrings in Google Style 25 | 26 | ## Package management 27 | - Use `uv add` to install python packages 28 | - Use `uv add package-name --optional dev ` to install tests and developer requirements (pytest, linters, etc...) 29 | - Use `uv run` to run the project or scripts 30 | -------------------------------------------------------------------------------- /tests/models/test_page_data.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from pathlib import Path 3 | 4 | import pytest 5 | 6 | from extract_emails.models import PageData 7 | 8 | TEST_FILE = Path(__file__).parent.joinpath("test.csv") 9 | 10 | 11 | @pytest.fixture 12 | def page_data(): 13 | return PageData( 14 | website="https://example.com", page_url="https://example.com/page=123" 15 | ) 16 | 17 | 18 | def test_model_init(page_data: PageData): 19 | assert isinstance(page_data.data, dict) 20 | 21 | 22 | def test_add_vals(page_data: PageData): 23 | page_data.append("emails", ["email@email.com"]) 24 | assert "emails" in page_data.data 25 | assert "email@email.com" == page_data.data["emails"][0] 26 | 27 | 28 | def test_len(page_data: PageData): 29 | page_data.append("emails", ["email@email.com", "email@email.com2"]) 30 | page_data.append("emails2", ["email@email.com", "email@email.com2"]) 31 | 32 | assert len(page_data) == 4 33 | 34 | 35 | def test_to_csv(page_data: PageData): 36 | page_data.append("emails", ["email@email.com", "email@email.com2"]) 37 | page_data.append("emails2", ["email@email.com", "email@email.com2"]) 38 | 39 | PageData.to_csv([page_data], TEST_FILE) 40 | 41 | with open(TEST_FILE, "r") as f: 42 | reader = csv.DictReader(f) 43 | data = list(reader) 44 | 45 | assert len(data) == 2 46 | 47 | for column in ("website", "page_url", "emails", "emails2"): 48 | assert column in data[0].keys() 49 | 50 | TEST_FILE.unlink() 51 | -------------------------------------------------------------------------------- /extract_emails/link_filters/default_link_filter.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | from urllib.parse import urljoin 3 | 4 | from extract_emails.link_filters.link_filter_base import LinkFilterBase 5 | 6 | 7 | class DefaultLinkFilter(LinkFilterBase): 8 | """Default filter for links""" 9 | 10 | def __init__(self, website: str): 11 | super().__init__(website) 12 | self.checked_links: set[str] = set() 13 | 14 | def filter(self, links: Iterable[str]) -> list[str]: 15 | """Will exclude from a list URLs, which not starts with `self.website` and not starts with '/' 16 | 17 | Examples: 18 | >>> from extract_emails.link_filters import DefaultLinkFilter 19 | >>> test_urls = ["https://example.com/page1.html","/page.html","/page.html", "https://google.com"] 20 | >>> link_filter = DefaultLinkFilter("https://example.com/") 21 | >>> filtered_urls = link_filter.filter(test_urls) 22 | >>> filtered_urls 23 | ["https://example.com/page1.html", "https://example.com/page.html"] 24 | 25 | Args: 26 | links: List of links for filtering 27 | 28 | Returns: 29 | Set of filtered URLs 30 | """ 31 | filtered_urls = [] 32 | for link in links: 33 | url = urljoin(self.website, link) 34 | if not url.startswith(self.website): 35 | continue 36 | if url in self.checked_links: 37 | continue 38 | filtered_urls.append(url) 39 | self.checked_links.add(url) 40 | 41 | return filtered_urls 42 | -------------------------------------------------------------------------------- /.cursor/rules/uv.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Package Management with `uv` 3 | globs: 4 | alwaysApply: false 5 | --- 6 | # Package Management with `uv` 7 | 8 | These rules define strict guidelines for managing Python dependencies in this project using the `uv` dependency manager. 9 | 10 | **✅ Use `uv` exclusively** 11 | 12 | - All Python dependencies **must be installed, synchronized, and locked** using `uv`. 13 | - Never use `pip`, `pip-tools`, or `poetry` directly for dependency management. 14 | 15 | **🔁 Managing Dependencies** 16 | 17 | Always use these commands: 18 | 19 | ```bash 20 | # Add or upgrade dependencies 21 | uv add 22 | 23 | # Remove dependencies 24 | uv remove 25 | 26 | # Reinstall all dependencies from lock file 27 | uv sync 28 | 29 | # Reinstall all dependencies with optianal from lock file 30 | uv sync --all-extras 31 | ``` 32 | 33 | **🔁 Scripts** 34 | 35 | ```bash 36 | # Run script with proper dependencies 37 | uv run script.py 38 | ``` 39 | 40 | You can edit inline-metadata manually: 41 | 42 | ```python 43 | # /// script 44 | # requires-python = ">=3.12" 45 | # dependencies = [ 46 | # "torch", 47 | # "torchvision", 48 | # "opencv-python", 49 | # "numpy", 50 | # "matplotlib", 51 | # "Pillow", 52 | # "timm", 53 | # ] 54 | # /// 55 | 56 | print("some python code") 57 | ``` 58 | 59 | Or using uv cli: 60 | 61 | ```bash 62 | # Add or upgrade script dependencies 63 | uv add package-name --script script.py 64 | 65 | # Remove script dependencies 66 | uv remove package-name --script script.py 67 | 68 | # Reinstall all script dependencies from lock file 69 | uv sync --script script.py 70 | ``` 71 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Extract-Emails 2 | 3 | Thanks for taking the time to contribute! 4 | 5 | The main goal of the project is extracting different useful information from web pages. 6 | Please feel free to contribute to the project by adding new features, fixing bugs, or share your ideas in the issues section. 7 | 8 | ## Virtual environment 9 | You can create a virtual environment using [poetry](https://python-poetry.org/docs/): 10 | ```shell 11 | # Install dependencies 12 | uv sync --all-extras 13 | 14 | # Activate the virtual environment 15 | source .venv/bin/activate 16 | ``` 17 | 18 | ## Tests 19 | All testes must be written with [pytest](https://docs.pytest.org/) library. 20 | 21 | To run tests, run the following commands: 22 | ```shell 23 | # run all tests 24 | make test-all 25 | 26 | # run tests without slow cases 27 | make test 28 | ``` 29 | 30 | ## Documentation 31 | For rendering documentation use [mkdocs](https://mkdocs.org/), so all docstrings must be 32 | written in [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html) 33 | 34 | ```shell 35 | # Serve docs 36 | make docs-serve 37 | ``` 38 | 39 | ## Commits 40 | Please use the following template for commits: 41 | ```shell 42 | type: title 43 | 44 | body[optional] 45 | ``` 46 | for example: 47 | ```shell 48 | feat: add new feature 49 | ``` 50 | 51 | The commit type is one of the following: 52 | 53 | - *feat* - a new feature 54 | - *fix* - a bug fix 55 | - *chore* - changes to the project's internals (e.g. dependency updates) 56 | - *refactor* - a change that neither fixes a bug nor adds a feature 57 | - *docs* - documentation only changes 58 | - *test* - changes to the test suite 59 | - etc 60 | -------------------------------------------------------------------------------- /extract_emails/data_savers/csv_saver.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from pathlib import Path 3 | from typing import Any, Iterable 4 | 5 | from extract_emails.models import PageData 6 | 7 | from .data_saver import DataSaver 8 | 9 | 10 | class CsvSaver(DataSaver): 11 | def __init__(self, save_mode="w", **kwargs): 12 | super().__init__(**kwargs) 13 | self.save_mode = save_mode 14 | self.output_path = kwargs.get("output_path") 15 | if self.output_path is None or not isinstance(self.output_path, Path): 16 | raise ValueError("output_path must be a Path object") 17 | 18 | def save(self, data: Iterable[PageData]): 19 | processed_data = self.process_data(data) 20 | headers = self.get_headers(processed_data) 21 | is_new_file = not self.output_path.exists() 22 | 23 | with open(self.output_path, self.save_mode, encoding="utf-8", newline="") as f: 24 | w = csv.DictWriter(f, fieldnames=headers) 25 | if is_new_file: 26 | w.writeheader() 27 | w.writerows(processed_data) 28 | 29 | @staticmethod 30 | def process_data(data: Iterable[PageData]) -> list[dict[str, Any]]: 31 | processed_data = [] 32 | for i in data: 33 | d = {"website": i.website, "page": i.page_url} 34 | for k, v in i.data.items(): 35 | for item in v: 36 | d[k] = item 37 | processed_data.append(d) 38 | return processed_data 39 | 40 | @staticmethod 41 | def get_headers(data: Iterable[dict[str, Any]]) -> list[str]: 42 | headers = [] 43 | for i in data: 44 | headers.extend(list(i.keys())) 45 | return list(set(headers)) 46 | -------------------------------------------------------------------------------- /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | .idea 104 | .pypirc 105 | .vscode 106 | 107 | *.exe -------------------------------------------------------------------------------- /tests/link_filters/test_contact_link_filter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from extract_emails.link_filters import ContactInfoLinkFilter 4 | 5 | test_urls = [ 6 | "https://google.com", 7 | "/page1", 8 | "https://example.com/page", 9 | "https://example.com/about", 10 | "/about-us", 11 | "/about-us", 12 | "https://example.com/not-default-call-us", 13 | "https://example.com/not-default-call-us-2", 14 | "/not-default-call-us-2", 15 | ] 16 | 17 | 18 | def test_default_candidates(): 19 | link_filter = ContactInfoLinkFilter("https://example.com") 20 | filtered_links = link_filter.filter(test_urls) 21 | assert len(filtered_links) == 2 22 | assert "https://google.com" not in filtered_links 23 | assert "https://example.com/page1" not in filtered_links 24 | 25 | 26 | def test_custom_candidates(): 27 | link_filter = ContactInfoLinkFilter("https://example.com", ["call-us"]) 28 | filtered_links = link_filter.filter(test_urls) 29 | assert len(filtered_links) == 2 30 | assert "https://google.com" not in filtered_links 31 | assert "https://example.com/page1" not in filtered_links 32 | assert "https://example.com/not-default-call-us-2" in filtered_links 33 | 34 | 35 | def test_use_default_true(): 36 | link_filter = ContactInfoLinkFilter( 37 | "https://example.com", ["not-call-us"], use_default=True 38 | ) 39 | filtered_links = link_filter.filter(test_urls) 40 | assert len(filtered_links) == 6 41 | assert "https://example.com/page" in filtered_links 42 | 43 | 44 | def test_use_default_false(): 45 | link_filter = ContactInfoLinkFilter( 46 | "https://example.com", ["not-call-us"], use_default=False 47 | ) 48 | filtered_links = link_filter.filter(test_urls) 49 | assert len(filtered_links) == 0 50 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Extract Emails 2 | 3 | plugins: 4 | - search 5 | - mkdocstrings 6 | - autorefs 7 | - copy-to-llm: 8 | repo_url: "https://raw.githubusercontent.com/dmitriiweb/extract-emails/main" 9 | 10 | theme: 11 | name: "material" 12 | palette: 13 | scheme: slate 14 | primary: pink 15 | accent: pink 16 | 17 | markdown_extensions: 18 | - markdown.extensions.admonition 19 | - markdown.extensions.attr_list 20 | - markdown.extensions.def_list 21 | - markdown.extensions.footnotes 22 | - markdown.extensions.meta 23 | - markdown.extensions.toc: 24 | permalink: true 25 | - pymdownx.arithmatex: 26 | generic: true 27 | - pymdownx.betterem: 28 | smart_enable: all 29 | - pymdownx.caret 30 | - pymdownx.critic 31 | - pymdownx.details 32 | - pymdownx.emoji: 33 | emoji_index: !!python/name:material.extensions.emoji.twemoji 34 | emoji_generator: !!python/name:materialx.emoji.to_svg 35 | - pymdownx.highlight 36 | - pymdownx.inlinehilite 37 | - pymdownx.keys 38 | - pymdownx.magiclink: 39 | repo_url_shorthand: true 40 | user: squidfunk 41 | repo: mkdocs-material 42 | - pymdownx.mark 43 | - pymdownx.smartsymbols 44 | - pymdownx.snippets: 45 | check_paths: true 46 | - pymdownx.superfences: 47 | custom_fences: 48 | - name: mermaid 49 | class: mermaid 50 | format: !!python/name:pymdownx.superfences.fence_code_format 51 | - pymdownx.tabbed 52 | - pymdownx.tasklist: 53 | custom_checkbox: true 54 | - pymdownx.tilde 55 | 56 | nav: 57 | - Extract Emails: index.md 58 | - Quick Start: 59 | - quick_start/intro.md 60 | - quick_start/save_data.md 61 | - quick_start/logs.md 62 | - Code References: 63 | - code/workers.md 64 | - code/browsers.md 65 | - code/link_filters.md 66 | - code/models.md 67 | - code/data_extractors.md 68 | - code/utils.md 69 | - code/errors.md 70 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "extract-emails" 3 | version = "6.0.1" 4 | requires-python = ">=3.10,<3.14" 5 | description = "Extract email addresses and linkedin profiles from given URL." 6 | authors = [ 7 | {name = "Dmitrii K", email = "dmitriik@proton.me"} 8 | ] 9 | readme = "README.md" 10 | keywords = ["parser", "email", "linkedin"] 11 | classifiers = [ 12 | "Development Status :: 5 - Production/Stable", 13 | "License :: OSI Approved :: MIT License", 14 | "Operating System :: MacOS", 15 | "Operating System :: Microsoft :: Windows", 16 | "Operating System :: Unix", 17 | "Programming Language :: Python :: 3.10", 18 | "Programming Language :: Python :: 3.11", 19 | "Programming Language :: Python :: 3.12", 20 | "Programming Language :: Python :: 3.13", 21 | "Programming Language :: Python :: Implementation :: CPython", 22 | "Topic :: Utilities", 23 | ] 24 | dependencies = [ 25 | "aiocsv>=1.3.2", 26 | "aiofiles>=24.1.0", 27 | "loguru>=0.7.3", 28 | "pydantic>=2.11.7", 29 | ] 30 | 31 | [project.urls] 32 | repository = "https://github.com/dmitriiweb/extract-emails" 33 | documentation = "https://dmitriiweb.github.io/extract-emails" 34 | 35 | [project.optional-dependencies] 36 | all = [ 37 | "httpx>=0.28.1", 38 | "playwright>=1.52.0", 39 | ] 40 | httpx = [ 41 | "httpx>=0.28.1", 42 | ] 43 | playwright = [ 44 | "playwright>=1.52.0", 45 | ] 46 | dev = [ 47 | "jinja2>=3.1.6", 48 | "mkdocs>=1.6.1", 49 | "mkdocs-autorefs>=1.4.2", 50 | "mkdocs-copy-to-llm>=0.2.6", 51 | "mkdocs-material>=9.6.14", 52 | "mkdocstrings[python]>=0.29.1", 53 | "mypy>=1.16.1", 54 | "pygments>=2.19.1", 55 | "pytest>=8.4.1", 56 | "pytest-async>=0.1.1", 57 | "pytest-asyncio>=1.3.0", 58 | "pytest-cov>=6.2.1", 59 | "ruff>=0.12.0", 60 | "tox>=4.27.0", 61 | ] 62 | 63 | [build-system] 64 | requires = ["hatchling"] 65 | build-backend = "hatchling.build" 66 | 67 | [project.scripts] 68 | extract-emails = "extract_emails.console.application:main" 69 | 70 | [tool.pytest.ini_options] 71 | asyncio_mode = "auto" 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extract Emails 2 | 3 | ![Image](https://github.com/dmitriiweb/extract-emails/blob/docs_improvements/images/email.png?raw=true) 4 | 5 | [![PyPI version](https://badge.fury.io/py/extract-emails.svg)](https://badge.fury.io/py/extract-emails) 6 | 7 | Extract emails and linkedins profiles from a given website 8 | 9 | [Documentation](https://dmitriiweb.github.io/extract-emails/) 10 | 11 | ## Requirements 12 | 13 | - Python >= 3.10 14 | 15 | ## Installation 16 | 17 | ```bash 18 | pip install extract_emails[all] 19 | # or 20 | pip install extract_emails[httpx] 21 | # or 22 | pip install extract_emails[playwright] 23 | playwright install chromium --with-deps 24 | ``` 25 | 26 | ## Quick Usage 27 | ### As library 28 | 29 | ```python 30 | from pathlib import Path 31 | 32 | from extract_emails import DefaultWorker 33 | from extract_emails.browsers import ChromiumBrowser, HttpxBrowser 34 | from extract_emails.models import PageData 35 | 36 | def main(): 37 | with ChromiumBrowser() as browser: 38 | worker = DefaultWorker("https://example.com, browser) 39 | data = worker.get_data() 40 | PageData.to_csv(data, Path("output.csv")) 41 | 42 | with HttpxBrowser() as browser: 43 | worker = DefaultWorker("https://example.com, browser) 44 | data = worker.get_data() 45 | PageData.to_csv(data, Path("output.csv")) 46 | 47 | async def main(): 48 | async with ChromiumBrowser() as browser: 49 | worker = DefaultWorker("https://example.com, browser) 50 | data = await worker.aget_data() 51 | await PageData.to_csv(data, Path("output.csv")) 52 | 53 | async with HttpxBrowser() as browser: 54 | worker = DefaultWorker("https://example.com, browser) 55 | data = await worker.aget_data() 56 | await PageData.to_csv(data, Path("output.csv")) 57 | 58 | ``` 59 | ### As CLI tool 60 | ```bash 61 | $ extract-emails --help 62 | 63 | $ extract-emails --url https://en.wikipedia.org/wiki/Email -of output.csv 64 | $ cat output.csv 65 | email,page,website 66 | bob@b.org,https://en.wikipedia.org/wiki/Email,https://en.wikipedia.org/wiki/Email 67 | ``` -------------------------------------------------------------------------------- /extract_emails/console/application.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import click 4 | 5 | from extract_emails import browsers 6 | from extract_emails.data_extractors import ( 7 | DataExtractor, 8 | EmailExtractor, 9 | LinkedinExtractor, 10 | ) 11 | from extract_emails.models.page_data import PageData 12 | from extract_emails.workers import DefaultWorker 13 | 14 | 15 | def get_data_extractors(data_type: str) -> list[DataExtractor]: 16 | if data_type == "email": 17 | return [EmailExtractor()] 18 | elif data_type == "linkedin": 19 | return [LinkedinExtractor()] 20 | else: 21 | raise ValueError(f"Invalid data type: {data_type}") 22 | 23 | 24 | def get_browser(browser: str) -> browsers.PageSourceGetter: 25 | if browser == "httpx": 26 | return browsers.HttpxBrowser() 27 | elif browser == "chromium": 28 | return browsers.ChromiumBrowser() 29 | else: 30 | raise ValueError(f"Invalid browser: {browser}") 31 | 32 | 33 | @click.command() 34 | @click.option("--url", type=str, required=True, help="URL to extract data from") 35 | @click.option("-of", "--output-file", type=str, required=True, help="Output CSV file") 36 | @click.option( 37 | "-b", 38 | "--browser-name", 39 | type=str, 40 | default="chromium", 41 | help="Browser to use, can be 'chromium' or 'httpx'. Default: chromium", 42 | ) 43 | @click.option( 44 | "-dt", 45 | "--data-type", 46 | type=str, 47 | default="email", 48 | help="Data type to extract, must be a list separated by comma, e.g. 'email,linkedin. " 49 | "Available options: email, linkedin. Default: email,linkedin", 50 | ) 51 | def main(url: str, output_file: str, browser_name: str, data_type: str): 52 | browser = get_browser(browser_name) 53 | browser.start() 54 | 55 | worker = DefaultWorker( 56 | website_url=url, 57 | browser=browser, 58 | data_extractors=get_data_extractors(data_type), 59 | ) 60 | data = worker.get_data() 61 | 62 | browser.stop() 63 | 64 | PageData.to_csv(data, Path(output_file)) 65 | 66 | 67 | if __name__ == "__main__": 68 | main() 69 | -------------------------------------------------------------------------------- /extract_emails/browsers/page_source_getter.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | 5 | 6 | class PageSourceGetter(ABC): 7 | """All browsers must inherit from this class""" 8 | 9 | def __enter__(self) -> PageSourceGetter: 10 | """Context manager enter method. 11 | 12 | Returns: 13 | Self instance for method chaining 14 | """ 15 | self.start() 16 | return self 17 | 18 | def __exit__(self, exc_type, exc_val, exc_tb) -> None: 19 | """Context manager exit method. 20 | 21 | Args: 22 | exc_type: Exception type 23 | exc_val: Exception value 24 | exc_tb: Exception traceback 25 | """ 26 | self.stop() 27 | 28 | async def __aenter__(self) -> PageSourceGetter: 29 | """Async context manager enter method. 30 | 31 | Returns: 32 | Self instance for method chaining 33 | """ 34 | await self.astart() 35 | return self 36 | 37 | async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: 38 | """Async context manager exit method. 39 | 40 | Args: 41 | exc_type: Exception type 42 | exc_val: Exception value 43 | exc_tb: Exception traceback 44 | """ 45 | await self.astop() 46 | 47 | @abstractmethod 48 | def start(self) -> None: ... 49 | 50 | @abstractmethod 51 | def stop(self) -> None: ... 52 | 53 | @abstractmethod 54 | async def astart(self) -> None: ... 55 | 56 | @abstractmethod 57 | async def astop(self) -> None: ... 58 | 59 | @abstractmethod 60 | def get_page_source(self, url: str) -> str: 61 | """Return page content from an URL 62 | 63 | Args: 64 | url: URL 65 | 66 | Returns: 67 | page content (html, json, whatever) 68 | """ 69 | ... 70 | 71 | @abstractmethod 72 | async def aget_page_source(self, url: str) -> str: 73 | """Return page content from an URL asynchronously 74 | 75 | Args: 76 | url: URL 77 | 78 | Returns: 79 | page content (html, json, whatever) 80 | """ 81 | ... 82 | -------------------------------------------------------------------------------- /extract_emails/link_filters/link_filter_base.py: -------------------------------------------------------------------------------- 1 | import re 2 | from abc import ABC, abstractmethod 3 | from typing import Iterable 4 | from urllib.parse import urlparse 5 | 6 | RE_LINKS = re.compile(r']*?\s+)?href=(["\'])(.*?)\1') 7 | 8 | 9 | class LinkFilterBase(ABC): 10 | """Base class for link filters""" 11 | 12 | def __init__(self, website: str): 13 | """ 14 | 15 | Args: 16 | website: website address (scheme and domain), e.g. https://example.com 17 | """ 18 | self.website = website 19 | 20 | @staticmethod 21 | def get_website_address(url: str) -> str: 22 | """Extract scheme and domain name from an URL 23 | 24 | Examples: 25 | >>> from extract_emails.link_filters import LinkFilterBase 26 | >>> website = LinkFilterBase.get_website_address('https://example.com/list?page=134') 27 | >>> website 28 | 'https://example.com/' 29 | 30 | Args: 31 | url: URL for parsing 32 | 33 | Returns: 34 | scheme and domain name from URL, e.g. https://example.com 35 | 36 | """ 37 | parsed_url = urlparse(url) 38 | return f"{parsed_url.scheme}://{parsed_url.netloc}/" 39 | 40 | @staticmethod 41 | def get_links(page_source: str) -> list[str]: 42 | """Extract all URLs corresponding to current website 43 | 44 | Examples: 45 | >>> from extract_emails.link_filters import LinkFilterBase 46 | >>> links = LinkFilterBase.get_links(page_source) 47 | >>> links 48 | ["example.com", "/example.com", "https://example2.com"] 49 | 50 | Args: 51 | page_source: HTML page source 52 | 53 | Returns: 54 | List of URLs 55 | 56 | :param str page_source: HTML page source 57 | :return: List of URLs 58 | """ 59 | links = RE_LINKS.findall(page_source) 60 | links = [x[1] for x in links] 61 | return links 62 | 63 | @abstractmethod 64 | def filter(self, urls: Iterable[str]) -> list[str]: 65 | """Filter links by some parameters 66 | 67 | Args: 68 | urls: List of URLs for filtering 69 | 70 | Returns: 71 | List of filtered URLs 72 | """ 73 | -------------------------------------------------------------------------------- /docs/quick_start/intro.md: -------------------------------------------------------------------------------- 1 | # Intro 2 | 3 | ## Installation 4 | ```bash 5 | pip install extract_emails[all] 6 | # or 7 | pip install extract_emails[httpx] 8 | # or 9 | pip install extract_emails[playwright] 10 | playwright install chromium --with-deps 11 | ``` 12 | 13 | ## Quick Usage 14 | ### As library 15 | 16 | ```python 17 | from pathlib import Path 18 | 19 | from extract_emails import DefaultWorker 20 | from extract_emails.browsers import ChromiumBrowser, HttpxBrowser 21 | from extract_emails.models import PageData 22 | 23 | def main(): 24 | with ChromiumBrowser() as browser: 25 | worker = DefaultWorker("https://example.com, browser) 26 | data = worker.get_data() 27 | PageData.to_csv(data, Path("output.csv")) 28 | 29 | with HttpxBrowser() as browser: 30 | worker = DefaultWorker("https://example.com, browser) 31 | data = worker.get_data() 32 | PageData.to_csv(data, Path("output.csv")) 33 | 34 | async def main(): 35 | async with ChromiumBrowser() as browser: 36 | worker = DefaultWorker("https://example.com, browser) 37 | data = await worker.aget_data() 38 | await PageData.to_csv(data, Path("output.csv")) 39 | 40 | async with HttpxBrowser() as browser: 41 | worker = DefaultWorker("https://example.com, browser) 42 | data = await worker.aget_data() 43 | await PageData.to_csv(data, Path("output.csv")) 44 | 45 | ``` 46 | ### As CLI tool 47 | ```bash 48 | $ extract-emails --help 49 | 50 | $ extract-emails --url https://en.wikipedia.org/wiki/Email -of output.csv 51 | $ cat output.csv 52 | email,page,website 53 | bob@b.org,https://en.wikipedia.org/wiki/Email,https://en.wikipedia.org/wiki/Email 54 | ``` 55 | There are several main parts in the framework: 56 | 57 | - **browser** - Class to navigate through specific website and extract data from the webpages (*httpx*, *playwright* etc.) 58 | - **link filter** - Class to extract URLs from a page corresponding to the website. There are two link filters (`ContactInfoLinkFilter` by default): 59 | - [`DefaultLinkFilter`][extract_emails.link_filters.default_link_filter.DefaultLinkFilter] - Will extract all URLs corresponding to the website 60 | - [`ContactInfoLinkFilter`][extract_emails.link_filters.contact_link_filter.ContactInfoLinkFilter] - Will extract only contact URLs, e.g. */contact/*, */about-us/* etc 61 | - **data extractor** - Class to extract data from a page. At the moment there are two data extractors (both by default): 62 | - [`EmailExtractor`][extract_emails.data_extractors.email_extractor.EmailExtractor] - Will extract all emails from the page 63 | - [`LinkedinExtractor`][extract_emails.data_extractors.linkedin_extractor.LinkedinExtractor] - Will extract all links to Linkedin profiles from the page 64 | - [`DefaultWorker`][extract_emails.workers.default_worker.DefaultWorker] - All data extractions goes here -------------------------------------------------------------------------------- /extract_emails/browsers/httpx_browser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import httpx 4 | from loguru import logger 5 | 6 | from .page_source_getter import PageSourceGetter 7 | 8 | 9 | class _SyncHttpxBrowser: 10 | client: httpx.Client 11 | 12 | def __init__(self, headers: dict[str, str] | None = None): 13 | super().__init__() 14 | self.headers = headers 15 | 16 | def start(self) -> None: 17 | self.client = httpx.Client(headers=self.headers) 18 | self.client.__enter__() 19 | 20 | def stop(self) -> None: 21 | self.client.__exit__(None, None, None) 22 | 23 | def get_page_source(self, url: str) -> str: 24 | response = self.client.get(url) 25 | return response.text 26 | 27 | 28 | class _AsyncHttpxBrowser: 29 | client: httpx.AsyncClient 30 | 31 | def __init__(self, headers: dict[str, str] | None = None): 32 | super().__init__() 33 | self.headers = headers 34 | 35 | async def start(self) -> None: 36 | self.client = httpx.AsyncClient(headers=self.headers) 37 | await self.client.__aenter__() 38 | 39 | async def stop(self) -> None: 40 | await self.client.__aexit__(None, None, None) 41 | 42 | async def get_page_source(self, url: str) -> str: 43 | response = await self.client.get(url) 44 | return response.text 45 | 46 | 47 | class HttpxBrowser(PageSourceGetter): 48 | headers: dict[str, str] | None 49 | _sync_browser: _SyncHttpxBrowser 50 | _async_browser: _AsyncHttpxBrowser 51 | 52 | def __init__(self, headers: dict[str, str] | None = None): 53 | super().__init__() 54 | if headers is None: 55 | headers = { 56 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" 57 | } 58 | self.headers = headers 59 | 60 | def start(self) -> None: 61 | self._sync_browser = _SyncHttpxBrowser(self.headers) 62 | self._sync_browser.start() 63 | 64 | def stop(self) -> None: 65 | self._sync_browser.stop() 66 | 67 | def get_page_source(self, url: str) -> str: 68 | try: 69 | response = self._sync_browser.get_page_source(url) 70 | except Exception as e: 71 | logger.error(f"Could not get page source from {url}: {e}") 72 | return "" 73 | return response 74 | 75 | async def astart(self) -> None: 76 | self._async_browser = _AsyncHttpxBrowser(self.headers) 77 | await self._async_browser.start() 78 | 79 | async def astop(self) -> None: 80 | await self._async_browser.stop() 81 | 82 | async def aget_page_source(self, url: str) -> str: 83 | try: 84 | response = await self._async_browser.get_page_source(url) 85 | except Exception as e: 86 | logger.error(f"Could not get page source from {url}: {e}") 87 | return "" 88 | return response 89 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 6.0.1 4 | 5 | ### Added 6 | - Added `pytest-asyncio` dependency for async test support 7 | 8 | ### Changed 9 | - `HttpxBrowser` now sets a default User-Agent header when none is provided (fixes Wikipedia and other sites that block requests without user-agent) 10 | - Converted `ChromiumBrowser` tests to use async methods for compatibility with pytest asyncio mode 11 | - Updated `pytest.ini` to include `asyncio_mode = auto` for automatic async test detection 12 | - Fixed test method name: `PageData.save_as_csv` → `PageData.to_csv` in test suite 13 | 14 | ### Fixed 15 | - Fixed async/sync API conflict in ChromiumBrowser tests when running in pytest asyncio mode 16 | - Fixed test failures due to missing user-agent header in HttpxBrowser 17 | 18 | ## 6.0.0 19 | 20 | ### Added 21 | - New browser backends: `ChromiumBrowser` (sync/async, via Playwright) and `HttpxBrowser` (sync/async, via httpx) 22 | - Async CSV saving: `PageData.ato_csv` for async CSV export 23 | - Full async/sync support for all main workflows 24 | 25 | ### Changed 26 | - Refactored `PageData.save_as_csv` → `PageData.to_csv` (sync) and added `ato_csv` (async) 27 | - Refactored and improved `DefaultWorker` (sync/async extraction, more robust) 28 | - Updated documentation and quick start guides for new API and CLI 29 | - Updated test suite for new browser backends 30 | 31 | ### Removed 32 | - Removed all old factory classes and related docs 33 | - Removed legacy browsers: `chrome_browser.py`, `requests_browser.py` 34 | - Removed all old factory-based test and code paths 35 | 36 | ### Other 37 | - Switched package/dependency management from Poetry to `uv` 38 | - Updated Python support: now requires Python 3.10+ 39 | - Updated dependencies 40 | 41 | ## 5.3.4 42 | 43 | ### Changed 44 | 45 | - Drop python 3.9 support 46 | - Update dependencies 47 | - Update readme 48 | 49 | ## 5.3.2 50 | 51 | ### Changed 52 | 53 | - Update dependencies and supported python versions 54 | - minor fixes and code formatting 55 | 56 | ## 5.3.1 57 | 58 | ### Changed 59 | 60 | - Add timeout to RequestBrowser 61 | 62 | ```python 63 | from extract_emails.browsers.requests_browser import RequestsBrowser as Browser 64 | 65 | browser = Browser() 66 | browser.requests_timeout = 1 67 | ``` 68 | 69 | ## 5.3.0 70 | 71 | ### Changed 72 | 73 | - Add custom save mode to csv data saver 74 | 75 | ## 5.2.0 76 | 77 | ### Added 78 | 79 | - CLI tool 80 | - csv data saver 81 | 82 | ## 5.1.3 83 | 84 | ### Changed 85 | 86 | - Update dependencies 87 | 88 | ## 5.1.2 89 | 90 | ### Added 91 | 92 | - Python 3.10 support 93 | - Add CHANGELOG.md 94 | 95 | ## 5.1.0 96 | 97 | ### Added 98 | 99 | - Add save_as_csv class method to `PageData` model 100 | - Add logs to DefaultWorker 101 | 102 | ### Changed 103 | 104 | - Check if needed libraries for browsers were installed. If not will show user-friendly error 105 | - Small improvements in the code 106 | 107 | ## 5.0.2 108 | 109 | ### Changed 110 | 111 | - Fix imports for factories and DefaultWorker 112 | -------------------------------------------------------------------------------- /extract_emails/link_filters/contact_link_filter.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | from urllib.parse import urljoin 3 | 4 | from extract_emails.link_filters.link_filter_base import LinkFilterBase 5 | 6 | 7 | class ContactInfoLinkFilter(LinkFilterBase): 8 | """Contact information filter for links. 9 | 10 | Only keep the links might contain the contact information. 11 | 12 | Examples: 13 | >>> from extract_emails.link_filters import ContactInfoLinkFilter 14 | >>> link_filter = ContactInfoLinkFilter("https://example.com") 15 | >>> filtered_links = link_filter.filter(['/about-us', '/search']) 16 | >>> filtered_links 17 | ['https://example.com/about-us'] 18 | 19 | 20 | >>> from extract_emails.link_filters import ContactInfoLinkFilter 21 | >>> link_filter = ContactInfoLinkFilter("https://example.com", use_default=True) 22 | >>> filtered_links = link_filter.filter(['/blog', '/search']) 23 | >>> filtered_links 24 | ['https://example.com/blog', 'https://example.com/search'] 25 | 26 | >>> from extract_emails.link_filters import ContactInfoLinkFilter 27 | >>> link_filter = ContactInfoLinkFilter("https://example.com", use_default=False) 28 | >>> filtered_links = link_filter.filter(['/blog', '/search']) 29 | >>> filtered_links 30 | [] 31 | 32 | >>> from extract_emails.link_filters import ContactInfoLinkFilter 33 | >>> link_filter = ContactInfoLinkFilter("https://example.com", contruct_candidates=['search']) 34 | >>> filtered_links = link_filter.filter(['/blog', '/search']) 35 | >>> filtered_links 36 | ['https://example.com/search'] 37 | """ 38 | 39 | default_contruct_candidates = [ 40 | "about", 41 | "about-us", 42 | "aboutus", 43 | "contact", 44 | "contact-us", 45 | "contactus", 46 | ] 47 | 48 | checked_links: set[str] = set() 49 | 50 | def __init__( 51 | self, 52 | website: str, 53 | contruct_candidates: list[str] | None = None, 54 | use_default: bool = False, 55 | ): 56 | """ 57 | 58 | Args: 59 | website: website address (scheme and domain), e.g. https://example.com 60 | contruct_candidates: keywords for filtering the list of URLs, 61 | default: see `self.default_contruct_candidates` 62 | use_default: if no contactinfo urls found and return filtered_urls, 63 | default: True 64 | """ 65 | super().__init__(website) 66 | self.checked_links = set() 67 | self.candidates = ( 68 | contruct_candidates 69 | if contruct_candidates is not None 70 | else self.default_contruct_candidates 71 | ) 72 | self.use_default = use_default 73 | 74 | def filter(self, urls: Iterable[str]) -> list[str]: 75 | """Filter out the links without keywords 76 | 77 | Args: 78 | urls: List of URLs for filtering 79 | 80 | Returns: 81 | List of filtered URLs 82 | """ 83 | filtered_urls = [] 84 | contactinfo_urls = [] 85 | 86 | for url in urls: 87 | url = urljoin(self.website, url) 88 | 89 | if not url.startswith(self.website): 90 | continue 91 | if url in self.checked_links: 92 | continue 93 | filtered_urls.append(url) 94 | self.checked_links.add(url) 95 | 96 | for cand in self.candidates: 97 | if cand in url.lower(): 98 | contactinfo_urls.append(url) 99 | break 100 | 101 | return ( 102 | filtered_urls 103 | if len(contactinfo_urls) == 0 and self.use_default 104 | else contactinfo_urls 105 | ) 106 | -------------------------------------------------------------------------------- /extract_emails/models/page_data.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import aiofiles 3 | from aiocsv import AsyncDictWriter 4 | from itertools import zip_longest 5 | from pathlib import Path 6 | 7 | from pydantic import BaseModel, Field 8 | 9 | 10 | class PageData(BaseModel): 11 | """Representation for data from a webpage 12 | 13 | Examples: 14 | >>> from extract_emails.models import PageData 15 | >>> page_data = PageData(website='https://example.com', page_url='https://example.com/page123') 16 | 17 | Attributes: 18 | website (str): website address from where data 19 | page_url (str): Page URL from where data 20 | data (Optional[Dict[str, List[str]]]): Data from the page in format: { 'label': [data, data] }, default: {} 21 | """ 22 | 23 | website: str 24 | page_url: str 25 | data: dict[str, list[str]] = Field(default_factory=dict) 26 | 27 | def __len__(self) -> int: 28 | if len(self.data) == 0: 29 | return 0 30 | return sum(len(i) for i in self.data.values()) 31 | 32 | def append(self, label: str, vals: list[str]) -> None: 33 | """Append data from a page to the self.data collection 34 | 35 | Examples: 36 | >>> from extract_emails.models import PageData 37 | >>> page_data = PageData(website='https://example.com', page_url='https://example.com/page123') 38 | >>> page_data.append('email', ['email1@email.com', 'email2@email.com']) 39 | >>> page_data.page 40 | >>> {'email': ['email@email.com', 'email2@email.com']} 41 | 42 | Args: 43 | label: name of collection, e.g. email, linkedin 44 | vals: data from a page, e.g. emails, specific URLs etc. 45 | """ 46 | try: 47 | self.data[label].extend(vals) 48 | except KeyError: 49 | self.data[label] = vals 50 | 51 | @classmethod 52 | def to_csv(cls, data: list["PageData"], filepath: Path) -> None: 53 | """Save list of `PageData` to CSV file 54 | 55 | Args: 56 | data: list of `PageData` 57 | filepath: path to a CSV file 58 | """ 59 | base_headers: list[str] = list(cls.model_json_schema()["properties"].keys()) 60 | base_headers.remove("data") 61 | data_headers = [i for i in data[0].data.keys()] 62 | headers = base_headers + data_headers 63 | is_file_exists = filepath.exists() 64 | 65 | with open(filepath, "a", encoding="utf-8", newline="") as f: 66 | writer = csv.DictWriter(f, fieldnames=headers) 67 | if not is_file_exists: 68 | writer.writeheader() 69 | for page in data: 70 | for data_in_row in zip_longest(*page.data.values()): 71 | new_row = {"website": page.website, "page_url": page.page_url} 72 | for counter, column in enumerate(data_headers): 73 | new_row[column] = data_in_row[counter] 74 | 75 | writer.writerow(new_row) 76 | 77 | @classmethod 78 | async def ato_csv(cls, data: list["PageData"], filepath: Path) -> None: 79 | """Async save list of `PageData` to CSV file 80 | 81 | Args: 82 | data: list of `PageData` 83 | filepath: path to a CSV file 84 | """ 85 | base_headers: list[str] = list(cls.model_json_schema()["properties"].keys()) 86 | base_headers.remove("data") 87 | data_headers = [i for i in data[0].data.keys()] 88 | headers = base_headers + data_headers 89 | is_file_exists = filepath.exists() 90 | 91 | async with aiofiles.open(filepath, "a", encoding="utf-8", newline="") as f: 92 | writer = AsyncDictWriter(f, fieldnames=headers) 93 | if not is_file_exists: 94 | await writer.writeheader() 95 | for page in data: 96 | for data_in_row in zip_longest(*page.data.values()): 97 | new_row = {"website": page.website, "page_url": page.page_url} 98 | for counter, column in enumerate(data_headers): 99 | new_row[column] = data_in_row[counter] 100 | 101 | await writer.writerow(new_row) -------------------------------------------------------------------------------- /extract_emails/browsers/chromium_browser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from loguru import logger 4 | from playwright.async_api import Browser as AsyncBrowser 5 | from playwright.async_api import BrowserContext as AsyncBrowserContext 6 | from playwright.async_api import Page as AsyncPage 7 | from playwright.async_api import async_playwright 8 | from playwright.sync_api import Browser, BrowserContext, Page, sync_playwright 9 | 10 | from .page_source_getter import PageSourceGetter 11 | 12 | 13 | class _SyncChromiumBrowser: 14 | browser: Browser 15 | context: BrowserContext 16 | page: Page 17 | 18 | def __init__(self, headers: dict[str, str] | None = None, headless: bool = True): 19 | super().__init__() 20 | self.headers = headers 21 | self.headless = headless 22 | self.playwright = None 23 | 24 | def start(self) -> None: 25 | self.playwright = sync_playwright().start() # type: ignore 26 | self.browser = self.playwright.chromium.launch(headless=self.headless) # type: ignore 27 | self.context = self.browser.new_context( 28 | extra_http_headers=self.headers if self.headers else {} 29 | ) 30 | self.page = self.context.new_page() 31 | 32 | def stop(self) -> None: 33 | if hasattr(self, "page"): 34 | self.page.close() 35 | if hasattr(self, "context"): 36 | self.context.close() 37 | if hasattr(self, "browser"): 38 | self.browser.close() 39 | if self.playwright: 40 | self.playwright.stop() 41 | 42 | def get_page_source(self, url: str) -> str: 43 | self.page.goto(url) 44 | # Wait for the page to be fully loaded 45 | self.page.wait_for_load_state("networkidle") 46 | return self.page.content() 47 | 48 | 49 | class _AsyncChromiumBrowser: 50 | browser: AsyncBrowser 51 | context: AsyncBrowserContext 52 | page: AsyncPage 53 | 54 | def __init__(self, headers: dict[str, str] | None = None, headless: bool = True): 55 | super().__init__() 56 | self.headers = headers 57 | self.headless = headless 58 | self.playwright = None 59 | 60 | async def start(self) -> None: 61 | self.playwright = await async_playwright().start() # type: ignore 62 | self.browser = await self.playwright.chromium.launch(headless=self.headless) # type: ignore 63 | self.context = await self.browser.new_context( 64 | extra_http_headers=self.headers if self.headers else {} 65 | ) 66 | self.page = await self.context.new_page() 67 | 68 | async def stop(self) -> None: 69 | if hasattr(self, "page"): 70 | await self.page.close() 71 | if hasattr(self, "context"): 72 | await self.context.close() 73 | if hasattr(self, "browser"): 74 | await self.browser.close() 75 | if self.playwright: 76 | await self.playwright.stop() 77 | 78 | async def get_page_source(self, url: str) -> str: 79 | await self.page.goto(url) 80 | # Wait for the page to be fully loaded 81 | await self.page.wait_for_load_state("networkidle") 82 | return await self.page.content() 83 | 84 | 85 | class ChromiumBrowser(PageSourceGetter): 86 | headers: dict[str, str] | None 87 | headless: bool 88 | _sync_browser: _SyncChromiumBrowser 89 | _async_browser: _AsyncChromiumBrowser 90 | 91 | def __init__(self, headers: dict[str, str] | None = None, headless: bool = True): 92 | self.headers = headers 93 | self.headless = headless 94 | 95 | def start(self) -> None: 96 | self._sync_browser = _SyncChromiumBrowser(self.headers, self.headless) 97 | self._sync_browser.start() 98 | 99 | def stop(self) -> None: 100 | self._sync_browser.stop() 101 | 102 | def get_page_source(self, url: str) -> str: 103 | try: 104 | response = self._sync_browser.get_page_source(url) 105 | except Exception as e: 106 | logger.error(f"Could not get page source from {url}: {e}") 107 | return "" 108 | return response 109 | 110 | async def astart(self) -> None: 111 | self._async_browser = _AsyncChromiumBrowser(self.headers, self.headless) 112 | await self._async_browser.start() 113 | 114 | async def astop(self) -> None: 115 | await self._async_browser.stop() 116 | 117 | async def aget_page_source(self, url: str) -> str: 118 | try: 119 | response = await self._async_browser.get_page_source(url) 120 | except Exception as e: 121 | logger.error(f"Could not get page source from {url}: {e}") 122 | return "" 123 | return response 124 | -------------------------------------------------------------------------------- /extract_emails/workers/default_worker.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | 5 | from loguru import logger 6 | 7 | from extract_emails.browsers import PageSourceGetter 8 | from extract_emails.data_extractors import ( 9 | DataExtractor, 10 | EmailExtractor, 11 | LinkedinExtractor, 12 | ) 13 | from extract_emails.link_filters import ContactInfoLinkFilter, LinkFilterBase 14 | from extract_emails.models import PageData 15 | 16 | 17 | class DefaultWorker: 18 | """DefaultWorker is responsible for orchestrating the extraction of emails and LinkedIn URLs from a given website. 19 | 20 | This class utilizes both synchronous and asynchronous workers to perform the extraction process. It manages the 21 | configuration of the extraction process, including the website URL, browser, link filter, data extractors, depth, 22 | and maximum links to extract from a page. 23 | 24 | Attributes: 25 | website_url (str): The URL of the website to extract data from. 26 | browser (PageSourceGetter): The browser instance used to fetch page sources. 27 | link_filter (LinkFilterBase): The filter used to determine which links to follow. 28 | data_extractors (list[DataExtractor]): The list of data extractors to use for extracting information. 29 | depth (int): The maximum depth to traverse the website. 30 | max_links_from_page (int): The maximum number of links to extract from a single page. 31 | links (list[list[str]]): A list of lists containing URLs to be processed at each depth level. 32 | current_depth (int): The current depth level of the extraction process. 33 | """ 34 | 35 | def __init__( 36 | self, 37 | website_url: str, 38 | browser: PageSourceGetter, 39 | *, 40 | link_filter: LinkFilterBase | None = None, 41 | data_extractors: list[DataExtractor] | None = None, 42 | depth: int = 20, 43 | max_links_from_page: int = 20, 44 | ): 45 | self.website_url = website_url.rstrip("/") 46 | self.browser = browser 47 | self.link_filter = link_filter or ContactInfoLinkFilter(self.website_url) 48 | self.data_extractors = data_extractors or [ 49 | EmailExtractor(), 50 | LinkedinExtractor(), 51 | ] 52 | self.depth = depth 53 | self.max_links_from_page = max_links_from_page 54 | 55 | self.links = [[self.website_url]] 56 | self.current_depth = 0 57 | 58 | self._sync_worker = _SyncDefaultWorker( 59 | self.website_url, 60 | self.browser, 61 | link_filter=self.link_filter, 62 | data_extractors=self.data_extractors, 63 | depth=self.depth, 64 | max_links_from_page=self.max_links_from_page, 65 | ) 66 | self._async_worker = _AsyncDefaultWorker( 67 | self.website_url, 68 | self.browser, 69 | link_filter=self.link_filter, 70 | data_extractors=self.data_extractors, 71 | depth=self.depth, 72 | max_links_from_page=self.max_links_from_page, 73 | ) 74 | 75 | def get_data(self) -> list[PageData]: 76 | """Retrieve extracted data synchronously. 77 | 78 | Returns: 79 | list[PageData]: A list of PageData objects containing the extracted information. 80 | """ 81 | return self._sync_worker.get_data() 82 | 83 | async def aget_data(self) -> list[PageData]: 84 | """Retrieve extracted data asynchronously. 85 | 86 | Returns: 87 | list[PageData]: A list of PageData objects containing the extracted information. 88 | """ 89 | return await self._async_worker.get_data() 90 | 91 | 92 | class _DefaultWorker(ABC): 93 | def __init__( 94 | self, 95 | website_url: str, 96 | browser: PageSourceGetter, 97 | *, 98 | link_filter: LinkFilterBase, 99 | data_extractors: list[DataExtractor], 100 | depth: int, 101 | max_links_from_page: int, 102 | ): 103 | self.website_url = website_url.rstrip("/") 104 | self.browser = browser 105 | self.link_filter = link_filter 106 | self.data_extractors = data_extractors 107 | self.depth = depth 108 | self.max_links_from_page = max_links_from_page 109 | 110 | self.links = [[self.website_url]] 111 | self.current_depth = 0 112 | 113 | 114 | class _SyncDefaultWorker(_DefaultWorker): 115 | def get_data(self) -> list[PageData]: 116 | """Extract data from a given website""" 117 | data: list[PageData] = [] 118 | 119 | while len(self.links): 120 | logger.debug(f"current_depth={self.current_depth}") 121 | if self.depth is not None and self.current_depth > self.depth: 122 | break 123 | self.current_depth += 1 124 | 125 | new_data = self._get_new_data() 126 | data.extend(new_data) 127 | 128 | return data 129 | 130 | def _get_new_data(self) -> list[PageData]: 131 | data: list[PageData] = [] 132 | urls = self.links.pop(0) 133 | 134 | for url in urls: 135 | logger.info(f"getting data from {url}") 136 | try: 137 | new_urls, page_data = self._get_page_data(url) 138 | except Exception as e: 139 | logger.error("Cannot get data from {}: {}".format(url, e)) 140 | continue 141 | 142 | logger.info(f"URL: {url} | found {len(page_data)} items") 143 | data.append(page_data) 144 | 145 | if self.max_links_from_page: 146 | new_urls = new_urls[: self.max_links_from_page] 147 | 148 | logger.debug(f"Add {len(new_urls)} new URLs to the queue") 149 | self.links.append(new_urls) 150 | 151 | return data 152 | 153 | def _get_page_data(self, url: str) -> tuple[list[str], PageData]: 154 | page_source = self.browser.get_page_source(url) 155 | 156 | new_links = self.link_filter.get_links(page_source) 157 | filtered_links = self.link_filter.filter(new_links) # type: ignore 158 | 159 | page_data = PageData(website=self.website_url, page_url=url) 160 | 161 | for data_extractor in self.data_extractors: 162 | new_data = data_extractor.get_data(page_source) # type: ignore 163 | page_data.append(data_extractor.name, list(new_data)) # type: ignore 164 | 165 | return filtered_links, page_data 166 | 167 | 168 | class _AsyncDefaultWorker(_DefaultWorker): 169 | async def get_data(self) -> list[PageData]: 170 | """Extract data from a given website""" 171 | data: list[PageData] = [] 172 | 173 | while len(self.links): 174 | logger.debug(f"current_depth={self.current_depth}") 175 | if self.depth is not None and self.current_depth > self.depth: 176 | break 177 | self.current_depth += 1 178 | 179 | new_data = await self._get_new_data() 180 | data.extend(new_data) 181 | 182 | return data 183 | 184 | async def _get_new_data(self) -> list[PageData]: 185 | data: list[PageData] = [] 186 | urls = self.links.pop(0) 187 | 188 | for url in urls: 189 | logger.info(f"getting data from {url}") 190 | try: 191 | new_urls, page_data = await self._get_page_data(url) 192 | except Exception as e: 193 | logger.error("Cannot get data from {}: {}".format(url, e)) 194 | continue 195 | 196 | logger.info(f"URL: {url} | found {len(page_data)} items") 197 | data.append(page_data) 198 | 199 | if self.max_links_from_page: 200 | new_urls = new_urls[: self.max_links_from_page] 201 | 202 | logger.debug(f"Add {len(new_urls)} new URLs to the queue") 203 | self.links.append(new_urls) 204 | 205 | return data 206 | 207 | async def _get_page_data(self, url: str) -> tuple[list[str], PageData]: 208 | page_source = await self.browser.aget_page_source(url) 209 | 210 | new_links = self.link_filter.get_links(page_source) 211 | filtered_links = self.link_filter.filter(new_links) # type: ignore 212 | 213 | page_data = PageData(website=self.website_url, page_url=url) 214 | 215 | for data_extractor in self.data_extractors: 216 | new_data = data_extractor.get_data(page_source) # type: ignore 217 | page_data.append(data_extractor.name, list(new_data)) # type: ignore 218 | 219 | return filtered_links, page_data 220 | -------------------------------------------------------------------------------- /extract_emails/utils/_top_level_domains.py: -------------------------------------------------------------------------------- 1 | TOP_LEVEL_DOMAINS = { 2 | ".com", 3 | ".org", 4 | ".net", 5 | ".int", 6 | ".edu", 7 | ".gov", 8 | ".mil", 9 | ".arpa", 10 | ".ac", 11 | ".ad", 12 | ".ae", 13 | ".af", 14 | ".ag", 15 | ".ai", 16 | ".al", 17 | ".am", 18 | ".an", 19 | ".ao", 20 | ".aq", 21 | ".ar", 22 | ".as", 23 | ".at", 24 | ".au", 25 | ".aw", 26 | ".ax", 27 | ".az", 28 | ".ba", 29 | ".bb", 30 | ".bd", 31 | ".be", 32 | ".bf", 33 | ".bg", 34 | ".bh", 35 | ".bi", 36 | ".bj", 37 | ".bl", 38 | ".bm", 39 | ".bn", 40 | ".bo", 41 | ".bq", 42 | ".br", 43 | ".bs", 44 | ".bt", 45 | ".bv", 46 | ".bw", 47 | ".by", 48 | ".bz", 49 | ".ca", 50 | ".cc", 51 | ".cd", 52 | ".cf", 53 | ".cg", 54 | ".ch", 55 | ".ci", 56 | ".ck", 57 | ".cl", 58 | ".cm", 59 | ".cn", 60 | ".co", 61 | ".cr", 62 | ".cu", 63 | ".cv", 64 | ".cw", 65 | ".cx", 66 | ".cy", 67 | ".cz", 68 | ".de", 69 | ".dj", 70 | ".dk", 71 | ".dm", 72 | ".do", 73 | ".dz", 74 | ".ec", 75 | ".ee", 76 | ".eg", 77 | ".eh", 78 | ".er", 79 | ".es", 80 | ".et", 81 | ".eu", 82 | ".fi", 83 | ".fj", 84 | ".fk", 85 | ".fm", 86 | ".fo", 87 | ".fr", 88 | ".ga", 89 | ".gb", 90 | ".gd", 91 | ".ge", 92 | ".gf", 93 | ".gg", 94 | ".gh", 95 | ".gi", 96 | ".gl", 97 | ".gm", 98 | ".gn", 99 | ".gp", 100 | ".gq", 101 | ".gr", 102 | ".gs", 103 | ".gt", 104 | ".gu", 105 | ".gw", 106 | ".gy", 107 | ".hk", 108 | ".hm", 109 | ".hn", 110 | ".hr", 111 | ".ht", 112 | ".hu", 113 | ".id", 114 | ".ie", 115 | ".il", 116 | ".im", 117 | ".in", 118 | ".io", 119 | ".iq", 120 | ".ir", 121 | ".is", 122 | ".it", 123 | ".je", 124 | ".jm", 125 | ".jo", 126 | ".jp", 127 | ".ke", 128 | ".kg", 129 | ".kh", 130 | ".ki", 131 | ".km", 132 | ".kn", 133 | ".kp", 134 | ".kr", 135 | ".kw", 136 | ".ky", 137 | ".kz", 138 | ".la", 139 | ".lb", 140 | ".lc", 141 | ".li", 142 | ".lk", 143 | ".lr", 144 | ".ls", 145 | ".lt", 146 | ".lu", 147 | ".lv", 148 | ".ly", 149 | ".ma", 150 | ".mc", 151 | ".md", 152 | ".me", 153 | ".mf", 154 | ".mg", 155 | ".mh", 156 | ".mk", 157 | ".ml", 158 | ".mm", 159 | ".mn", 160 | ".mo", 161 | ".mp", 162 | ".mq", 163 | ".mr", 164 | ".ms", 165 | ".mt", 166 | ".mu", 167 | ".mv", 168 | ".mw", 169 | ".mx", 170 | ".my", 171 | ".mz", 172 | ".na", 173 | ".nc", 174 | ".ne", 175 | ".nf", 176 | ".ng", 177 | ".ni", 178 | ".nl", 179 | ".no", 180 | ".np", 181 | ".nr", 182 | ".nu", 183 | ".nz", 184 | ".om", 185 | ".pa", 186 | ".pe", 187 | ".pf", 188 | ".pg", 189 | ".ph", 190 | ".pk", 191 | ".pl", 192 | ".pm", 193 | ".pn", 194 | ".pr", 195 | ".ps", 196 | ".pt", 197 | ".pw", 198 | ".py", 199 | ".qa", 200 | ".re", 201 | ".ro", 202 | ".rs", 203 | ".ru", 204 | ".rw", 205 | ".sa", 206 | ".sb", 207 | ".sc", 208 | ".sd", 209 | ".se", 210 | ".sg", 211 | ".sh", 212 | ".si", 213 | ".sj", 214 | ".sk", 215 | ".sl", 216 | ".sm", 217 | ".sn", 218 | ".so", 219 | ".sr", 220 | ".ss", 221 | ".st", 222 | ".su", 223 | ".sv", 224 | ".sx", 225 | ".sy", 226 | ".sz", 227 | ".tc", 228 | ".td", 229 | ".tf", 230 | ".tg", 231 | ".th", 232 | ".tj", 233 | ".tk", 234 | ".tl", 235 | ".tm", 236 | ".tn", 237 | ".to", 238 | ".tp", 239 | ".tr", 240 | ".tt", 241 | ".tv", 242 | ".tw", 243 | ".tz", 244 | ".ua", 245 | ".ug", 246 | ".uk", 247 | ".um", 248 | ".us", 249 | ".uy", 250 | ".uz", 251 | ".va", 252 | ".vc", 253 | ".ve", 254 | ".vg", 255 | ".vi", 256 | ".vn", 257 | ".vu", 258 | ".wf", 259 | ".ws", 260 | ".ye", 261 | ".yt", 262 | ".za", 263 | ".zm", 264 | ".zw", 265 | "الجزائر.", 266 | ".հայ", 267 | "البحرين.", 268 | ".বাংলা", 269 | ".бел", 270 | ".бг[42]", 271 | ".中国", 272 | ".中國", 273 | "مصر.", 274 | ".ею", 275 | ".გე", 276 | ".ελ[42]", 277 | ".香港", 278 | ".भारत", 279 | "بھارت.", 280 | ".భారత్", 281 | ".ભારત", 282 | ".ਭਾਰਤ", 283 | ".இந்தியா", 284 | ".ভারত", 285 | ".ಭಾರತ", 286 | ".ഭാരതം", 287 | ".ভাৰত", 288 | ".ଭାରତ", 289 | "بارت.", 290 | ".भारतम्", 291 | ".भारोत", 292 | "ڀارت.", 293 | "ایران.", 294 | "عراق.", 295 | "الاردن.", 296 | ".қаз", 297 | ".澳门", 298 | ".澳門", 299 | "مليسيا.", 300 | "موريتانيا.", 301 | ".мон", 302 | "المغرب.", 303 | ".мкд", 304 | "عمان.", 305 | "پاکستان.", 306 | "فلسطين.", 307 | "قطر.", 308 | ".рф", 309 | "السعودية.", 310 | ".срб", 311 | ".新加坡", 312 | ".சிங்கப்பூர்", 313 | ".한국", 314 | ".ලංකා", 315 | ".இலங்கை", 316 | "سودان.", 317 | "سورية.", 318 | ".台湾", 319 | ".台灣", 320 | ".ไทย", 321 | "تونس.", 322 | ".укр", 323 | "امارات.", 324 | "اليمن.", 325 | ".academy", 326 | ".accountant", 327 | ".accountants", 328 | ".active", 329 | ".actor", 330 | ".ads", 331 | ".adult", 332 | ".aero", 333 | ".agency", 334 | ".airforce", 335 | ".analytics", 336 | ".apartments", 337 | ".app", 338 | ".archi", 339 | ".army", 340 | ".art", 341 | ".associates", 342 | ".attorney", 343 | ".auction", 344 | ".audible", 345 | ".audio", 346 | ".author", 347 | ".auto", 348 | ".autos", 349 | ".aws", 350 | ".baby", 351 | ".band", 352 | ".bank", 353 | ".bar", 354 | ".barefoot", 355 | ".bargains", 356 | ".baseball", 357 | ".basketball", 358 | ".beauty", 359 | ".beer", 360 | ".best", 361 | ".bestbuy", 362 | ".bet", 363 | ".bible", 364 | ".bid", 365 | ".bike", 366 | ".bingo", 367 | ".bio", 368 | ".biz", 369 | ".black", 370 | ".blackfriday", 371 | ".blockbuster", 372 | ".blog", 373 | ".blue", 374 | ".boo", 375 | ".book", 376 | ".boots", 377 | ".bot", 378 | ".boutique", 379 | ".box", 380 | ".broadway", 381 | ".broker", 382 | ".build", 383 | ".builders", 384 | ".business", 385 | ".buy", 386 | ".buzz", 387 | ".cab", 388 | ".cafe", 389 | ".call", 390 | ".cam", 391 | ".camera", 392 | ".camp", 393 | ".cancerresearch", 394 | ".capital", 395 | ".car", 396 | ".cards", 397 | ".care", 398 | ".career", 399 | ".careers", 400 | ".cars", 401 | ".case", 402 | ".cash", 403 | ".casino", 404 | ".catering", 405 | ".catholic", 406 | ".center", 407 | ".cern", 408 | ".ceo", 409 | ".cfd", 410 | ".channel", 411 | ".chat", 412 | ".cheap", 413 | ".christmas", 414 | ".church", 415 | ".cipriani", 416 | ".circle", 417 | ".city", 418 | ".claims", 419 | ".cleaning", 420 | ".click", 421 | ".clinic", 422 | ".clothing", 423 | ".cloud", 424 | ".club", 425 | ".coach", 426 | ".codes", 427 | ".coffee", 428 | ".college", 429 | ".community", 430 | ".company", 431 | ".compare", 432 | ".computer", 433 | ".condos", 434 | ".construction", 435 | ".consulting", 436 | ".contact", 437 | ".contractors", 438 | ".cooking", 439 | ".cool", 440 | ".coop", 441 | ".country", 442 | ".coupon", 443 | ".coupons", 444 | ".courses", 445 | ".credit", 446 | ".creditcard", 447 | ".cruise", 448 | ".cricket", 449 | ".cruises", 450 | ".dad", 451 | ".dance", 452 | ".data", 453 | ".date", 454 | ".dating", 455 | ".day", 456 | ".deal", 457 | ".deals", 458 | ".degree", 459 | ".delivery", 460 | ".democrat", 461 | ".dental", 462 | ".dentist", 463 | ".design", 464 | ".dev", 465 | ".diamonds", 466 | ".diet", 467 | ".digital", 468 | ".direct", 469 | ".directory", 470 | ".discount", 471 | ".diy", 472 | ".docs", 473 | ".doctor", 474 | ".dog", 475 | ".domains", 476 | ".dot", 477 | ".download", 478 | ".drive", 479 | ".duck", 480 | ".earth", 481 | ".eat", 482 | ".eco", 483 | ".education", 484 | ".email", 485 | ".energy", 486 | ".engineer", 487 | ".engineering", 488 | ".enterprises", 489 | ".equipment", 490 | ".esq", 491 | ".estate", 492 | ".events", 493 | ".exchange", 494 | ".expert", 495 | ".exposed", 496 | ".express", 497 | ".fail", 498 | ".faith", 499 | ".family", 500 | ".fan", 501 | ".fans", 502 | ".farm", 503 | ".fashion", 504 | ".fast", 505 | ".feedback", 506 | ".film", 507 | ".final", 508 | ".finance", 509 | ".financial", 510 | ".fire", 511 | ".fish", 512 | ".fishing", 513 | ".fit", 514 | ".fitness", 515 | ".flights", 516 | ".florist", 517 | ".flowers", 518 | ".fly", 519 | ".foo", 520 | ".food", 521 | ".foodnetwork", 522 | ".football", 523 | ".forsale", 524 | ".forum", 525 | ".foundation", 526 | ".free", 527 | ".frontdoor", 528 | ".fun", 529 | ".fund", 530 | ".furniture", 531 | ".fyi", 532 | ".gallery", 533 | ".game", 534 | ".games", 535 | ".garden", 536 | ".gdn", 537 | ".gift", 538 | ".gifts", 539 | ".gives", 540 | ".glass", 541 | ".global", 542 | ".gold", 543 | ".golf", 544 | ".gop", 545 | ".graphics", 546 | ".green", 547 | ".gripe", 548 | ".grocery", 549 | ".group", 550 | ".guide", 551 | ".guitars", 552 | ".guru", 553 | ".hair", 554 | ".hangout", 555 | ".health", 556 | ".healthcare", 557 | ".help", 558 | ".here", 559 | ".hiphop", 560 | ".hiv", 561 | ".hockey", 562 | ".holdings", 563 | ".holiday", 564 | ".homegoods", 565 | ".homes", 566 | ".homesense", 567 | ".horse", 568 | ".hospital", 569 | ".host", 570 | ".hosting", 571 | ".hot", 572 | ".hotels", 573 | ".house", 574 | ".how", 575 | ".ice", 576 | ".icu", 577 | ".industries", 578 | ".info", 579 | ".ing", 580 | ".ink", 581 | ".institute[75]", 582 | ".insurance", 583 | ".insure", 584 | ".international", 585 | ".investments", 586 | ".jewelry", 587 | ".jobs", 588 | ".joy", 589 | ".kim", 590 | ".kitchen", 591 | ".land", 592 | ".latino", 593 | ".law", 594 | ".lawyer", 595 | ".lease", 596 | ".legal", 597 | ".lgbt", 598 | ".life", 599 | ".lifeinsurance", 600 | ".lighting", 601 | ".like", 602 | ".limited", 603 | ".limo", 604 | ".link", 605 | ".live", 606 | ".living", 607 | ".loan", 608 | ".loans", 609 | ".locker", 610 | ".lol", 611 | ".lotto", 612 | ".love", 613 | ".ltd", 614 | ".luxury", 615 | ".makeup", 616 | ".management", 617 | ".map", 618 | ".market", 619 | ".marketing", 620 | ".markets", 621 | ".mba", 622 | ".med", 623 | ".media", 624 | ".meet", 625 | ".meme", 626 | ".memorial", 627 | ".men", 628 | ".menu", 629 | ".mint", 630 | ".mobi", 631 | ".mobile", 632 | ".mobily", 633 | ".moe", 634 | ".mom", 635 | ".money", 636 | ".mortgage", 637 | ".motorcycles", 638 | ".mov", 639 | ".movie", 640 | ".museum", 641 | ".music", 642 | ".name", 643 | ".navy", 644 | ".network", 645 | ".new", 646 | ".news", 647 | ".ngo", 648 | ".ninja", 649 | ".now", 650 | ".observer", 651 | ".off", 652 | ".one", 653 | ".ong", 654 | ".onl", 655 | ".online", 656 | ".ooo", 657 | ".open", 658 | ".organic", 659 | ".origins", 660 | ".page", 661 | ".partners", 662 | ".parts", 663 | ".party", 664 | ".pay", 665 | ".pet", 666 | ".pharmacy", 667 | ".phone", 668 | ".photo", 669 | ".photography", 670 | ".photos", 671 | ".physio", 672 | ".pics", 673 | ".pictures", 674 | ".pid", 675 | ".pin", 676 | ".pink", 677 | ".pizza", 678 | ".place", 679 | ".plumbing", 680 | ".plus", 681 | ".poker", 682 | ".porn", 683 | ".post", 684 | ".press", 685 | ".prime", 686 | ".pro", 687 | ".productions", 688 | ".prof", 689 | ".promo", 690 | ".properties", 691 | ".property", 692 | ".protection", 693 | ".pub", 694 | ".qpon", 695 | ".racing", 696 | ".radio", 697 | ".read", 698 | ".realestate", 699 | ".realtor", 700 | ".realty", 701 | ".recipes", 702 | ".red", 703 | ".rehab", 704 | ".reit", 705 | ".ren", 706 | ".rent", 707 | ".rentals", 708 | ".repair", 709 | ".report", 710 | ".republican", 711 | ".rest", 712 | ".restaurant", 713 | ".review", 714 | ".reviews", 715 | ".rich", 716 | ".rip", 717 | ".rocks", 718 | ".rodeo", 719 | ".room", 720 | ".rugby", 721 | ".run", 722 | ".safe", 723 | ".sale", 724 | ".save", 725 | ".scholarships", 726 | ".school", 727 | ".science", 728 | ".search", 729 | ".secure", 730 | ".security", 731 | ".select", 732 | ".services", 733 | ".sex", 734 | ".sexy", 735 | ".shoes", 736 | ".shop", 737 | ".shopping", 738 | ".show", 739 | ".showtime", 740 | ".silk", 741 | ".singles", 742 | ".site", 743 | ".ski", 744 | ".skin", 745 | ".sky", 746 | ".sling", 747 | ".smile", 748 | ".soccer", 749 | ".social", 750 | ".software", 751 | ".solar", 752 | ".solutions", 753 | ".song", 754 | ".space", 755 | ".spot", 756 | ".spreadbetting", 757 | ".storage", 758 | ".store", 759 | ".stream", 760 | ".studio", 761 | ".study", 762 | ".style", 763 | ".sucks", 764 | ".supplies", 765 | ".supply", 766 | ".support", 767 | ".surf", 768 | ".surgery", 769 | ".systems", 770 | ".talk", 771 | ".tattoo", 772 | ".tax", 773 | ".taxi", 774 | ".team", 775 | ".tech", 776 | ".technology", 777 | ".tel", 778 | ".tennis", 779 | ".theater", 780 | ".theatre", 781 | ".tickets", 782 | ".tips", 783 | ".tires", 784 | ".today", 785 | ".tools", 786 | ".top", 787 | ".tours", 788 | ".town", 789 | ".toys", 790 | ".trade", 791 | ".trading", 792 | ".training", 793 | ".travel", 794 | ".travelersinsurance", 795 | ".trust", 796 | ".tube", 797 | ".tunes", 798 | ".uconnect", 799 | ".university", 800 | ".vacations", 801 | ".ventures", 802 | ".vet", 803 | ".video", 804 | ".villas", 805 | ".vip", 806 | ".vision", 807 | ".vodka", 808 | ".vote", 809 | ".voting", 810 | ".voyage", 811 | ".wang", 812 | ".watch", 813 | ".watches", 814 | ".weather", 815 | ".webcam", 816 | ".website", 817 | ".wed", 818 | ".wedding", 819 | ".whoswho", 820 | ".wiki", 821 | ".win", 822 | ".wine", 823 | ".winners", 824 | ".work", 825 | ".works", 826 | ".world", 827 | ".wow", 828 | ".wtf", 829 | ".xxx", 830 | ".xyz", 831 | ".yachts", 832 | ".yoga", 833 | ".you", 834 | ".zero", 835 | ".zone", 836 | ".shouji", 837 | ".tushu", 838 | ".wanggou", 839 | ".weibo", 840 | ".xihuan", 841 | ".arte", 842 | ".clinique", 843 | ".luxe", 844 | ".maison", 845 | ".moi", 846 | ".rsvp", 847 | ".sarl", 848 | ".epost", 849 | ".gmbh", 850 | ".haus", 851 | ".immobilien", 852 | ".jetzt", 853 | ".kaufen", 854 | ".kinder", 855 | ".reise", 856 | ".reisen", 857 | ".schule", 858 | ".versicherung", 859 | ".desi", 860 | ".shiksha", 861 | ".casa", 862 | ".immo", 863 | ".moda", 864 | ".voto", 865 | ".bom", 866 | ".passagens", 867 | ".abogado", 868 | ".gratis", 869 | ".futbol", 870 | ".hoteles", 871 | ".juegos", 872 | ".ltda", 873 | ".soy", 874 | ".tienda", 875 | ".uno", 876 | ".viajes", 877 | ".vuelos", 878 | "موقع.", 879 | ".كوم", 880 | ".موبايلي", 881 | ".كاثوليك", 882 | "شبكة.", 883 | ".بيتك", 884 | "بازار.", 885 | ".在线", 886 | ".中文网", 887 | ".网址", 888 | ".网站", 889 | ".网络", 890 | ".公司", 891 | ".商城", 892 | ".机构", 893 | ".我爱你", 894 | ".商标", 895 | ".世界", 896 | ".集团", 897 | ".慈善", 898 | ".八卦", 899 | ".公益", 900 | ".дети", 901 | ".католик", 902 | ".ком", 903 | ".онлайн", 904 | ".орг", 905 | ".сайт", 906 | ".संगठन", 907 | ".कॉम", 908 | ".नेट", 909 | ".닷컴", 910 | ".닷넷", 911 | ".קום\u200e", 912 | ".みんな", 913 | ".セール", 914 | ".ファッション", 915 | ".ストア", 916 | ".ポイント", 917 | ".クラウド", 918 | ".コム", 919 | ".คอม", 920 | ".africa", 921 | ".capetown", 922 | ".durban", 923 | ".joburg", 924 | ".abudhabi", 925 | ".arab", 926 | ".asia", 927 | ".doha", 928 | ".dubai", 929 | ".krd", 930 | ".kyoto", 931 | ".nagoya", 932 | ".okinawa", 933 | ".osaka", 934 | ".ryukyu", 935 | ".taipei", 936 | ".tatar", 937 | ".tokyo", 938 | ".yokohama", 939 | ".alsace", 940 | ".amsterdam", 941 | ".bcn", 942 | ".barcelona", 943 | ".bayern", 944 | ".berlin", 945 | ".brussels", 946 | ".budapest", 947 | ".bzh", 948 | ".cat", 949 | ".cologne", 950 | ".corsica", 951 | ".cymru", 952 | ".eus", 953 | ".frl", 954 | ".gal", 955 | ".gent", 956 | ".hamburg", 957 | ".helsinki", 958 | ".irish", 959 | ".ist", 960 | ".istanbul", 961 | ".koeln", 962 | ".london", 963 | ".madrid", 964 | ".moscow\xa0[ru]", 965 | ".nrw", 966 | ".paris", 967 | ".ruhr", 968 | ".saarland", 969 | ".scot", 970 | ".stockholm", 971 | ".swiss", 972 | ".tirol", 973 | ".vlaanderen", 974 | ".wales", 975 | ".wien", 976 | ".zuerich", 977 | ".boston", 978 | ".miami", 979 | ".nyc", 980 | ".quebec", 981 | ".vegas", 982 | ".kiwi", 983 | ".melbourne", 984 | ".sydney", 985 | ".lat", 986 | ".rio", 987 | ".佛山", 988 | ".广东", 989 | ".москва\xa0[ru]", 990 | ".рус\xa0[ru]", 991 | ".ابوظبي", 992 | ".عرب", 993 | ".aaa", 994 | ".aarp", 995 | ".abarth", 996 | ".abb", 997 | ".abbott", 998 | ".abbvie", 999 | ".abc", 1000 | ".accenture", 1001 | ".aco", 1002 | ".aeg", 1003 | ".aetna", 1004 | ".afl", 1005 | ".agakhan", 1006 | ".aig", 1007 | ".aigo", 1008 | ".airbus", 1009 | ".airtel", 1010 | ".akdn", 1011 | ".alfaromeo", 1012 | ".alibaba", 1013 | ".alipay", 1014 | ".allfinanz", 1015 | ".allstate", 1016 | ".ally", 1017 | ".alstom", 1018 | ".americanexpress", 1019 | ".amex", 1020 | ".amica", 1021 | ".android", 1022 | ".anz", 1023 | ".aol", 1024 | ".apple", 1025 | ".aquarelle", 1026 | ".aramco", 1027 | ".audi", 1028 | ".auspost", 1029 | ".axa", 1030 | ".azure", 1031 | ".baidu", 1032 | ".bananarepublic", 1033 | ".barclaycard", 1034 | ".barclays", 1035 | ".basketball", 1036 | ".bauhaus", 1037 | ".bbc", 1038 | ".bbt", 1039 | ".bbva", 1040 | ".bcg", 1041 | ".bentley", 1042 | ".bharti", 1043 | ".bing", 1044 | ".blanco", 1045 | ".bloomberg", 1046 | ".bms", 1047 | ".bmw", 1048 | ".bnl", 1049 | ".bnpparibas", 1050 | ".boehringer", 1051 | ".bond", 1052 | ".booking", 1053 | ".bosch", 1054 | ".bostik", 1055 | ".bradesco", 1056 | ".bridgestone", 1057 | ".brother", 1058 | ".bugatti", 1059 | ".cal", 1060 | ".calvinklein", 1061 | ".canon", 1062 | ".capitalone", 1063 | ".caravan", 1064 | ".cartier", 1065 | ".cba", 1066 | ".cbn", 1067 | ".cbre", 1068 | ".cbs", 1069 | ".cern", 1070 | ".cfa", 1071 | ".chanel", 1072 | ".chase", 1073 | ".chintai", 1074 | ".chrome", 1075 | ".chrysler", 1076 | ".cisco", 1077 | ".citadel", 1078 | ".citi", 1079 | ".citic", 1080 | ".clubmed", 1081 | ".comcast", 1082 | ".commbank", 1083 | ".creditunion", 1084 | ".crown", 1085 | ".crs", 1086 | ".csc", 1087 | ".cuisinella", 1088 | ".dabur", 1089 | ".datsun", 1090 | ".dealer", 1091 | ".dell", 1092 | ".deloitte", 1093 | ".delta", 1094 | ".dhl", 1095 | ".discover", 1096 | ".dish", 1097 | ".dnp", 1098 | ".dodge", 1099 | ".dunlop", 1100 | ".dupont", 1101 | ".dvag", 1102 | ".edeka", 1103 | ".emerck", 1104 | ".epson", 1105 | ".ericsson", 1106 | ".erni", 1107 | ".esurance", 1108 | ".etisalat", 1109 | ".eurovision", 1110 | ".everbank", 1111 | ".extraspace", 1112 | ".fage", 1113 | ".fairwinds", 1114 | ".farmers", 1115 | ".fedex", 1116 | ".ferrari", 1117 | ".ferrero", 1118 | ".fiat", 1119 | ".fidelity", 1120 | ".firestone", 1121 | ".firmdale", 1122 | ".flickr", 1123 | ".flir", 1124 | ".flsmidth", 1125 | ".ford", 1126 | ".fox", 1127 | ".fresenius", 1128 | ".forex", 1129 | ".frogans", 1130 | ".frontier", 1131 | ".fujitsu", 1132 | ".fujixerox", 1133 | ".gallo", 1134 | ".gallup", 1135 | ".gap", 1136 | ".gbiz", 1137 | ".gea", 1138 | ".genting", 1139 | ".giving", 1140 | ".gle", 1141 | ".globo", 1142 | ".gmail", 1143 | ".gmo", 1144 | ".gmx", 1145 | ".godaddy", 1146 | ".goldpoint", 1147 | ".goodyear", 1148 | ".goog", 1149 | ".google", 1150 | ".grainger", 1151 | ".guardian", 1152 | ".gucci", 1153 | ".hbo", 1154 | ".hdfc", 1155 | ".hdfcbank", 1156 | ".hermes", 1157 | ".hisamitsu", 1158 | ".hitachi", 1159 | ".hkt", 1160 | ".honda", 1161 | ".honeywell", 1162 | ".hotmail", 1163 | ".hsbc", 1164 | ".hughes", 1165 | ".hyatt", 1166 | ".hyundai", 1167 | ".ibm", 1168 | ".ieee", 1169 | ".ifm", 1170 | ".ikano", 1171 | ".imdb", 1172 | ".infiniti", 1173 | ".intel", 1174 | ".intuit", 1175 | ".ipiranga", 1176 | ".iselect", 1177 | ".itau", 1178 | ".itv", 1179 | ".iveco", 1180 | ".jaguar", 1181 | ".java", 1182 | ".jcb", 1183 | ".jcp", 1184 | ".jeep", 1185 | ".jpmorgan", 1186 | ".juniper", 1187 | ".kddi", 1188 | ".kerryhotels", 1189 | ".kerrylogistics", 1190 | ".kerryproperties", 1191 | ".kfh", 1192 | ".kia", 1193 | ".kinder", 1194 | ".kindle", 1195 | ".komatsu", 1196 | ".kpmg", 1197 | ".kred", 1198 | ".kuokgroup", 1199 | ".lacaixa", 1200 | ".ladbrokes", 1201 | ".lamborghini", 1202 | ".lancaster", 1203 | ".lancia", 1204 | ".lancome", 1205 | ".landrover", 1206 | ".lanxess", 1207 | ".lasalle", 1208 | ".latrobe", 1209 | ".lds", 1210 | ".lego", 1211 | ".liaison", 1212 | ".lexus", 1213 | ".lidl", 1214 | ".lifestyle", 1215 | ".lilly", 1216 | ".lincoln", 1217 | ".linde", 1218 | ".lipsy", 1219 | ".lixil", 1220 | ".locus", 1221 | ".lotte", 1222 | ".lpl", 1223 | ".lplfinancial", 1224 | ".lundbeck", 1225 | ".lupin", 1226 | ".macys", 1227 | ".maif", 1228 | ".man", 1229 | ".mango", 1230 | ".marriott", 1231 | ".maserati", 1232 | ".mattel", 1233 | ".mckinsey", 1234 | ".metlife", 1235 | ".microsoft", 1236 | ".mini", 1237 | ".mit", 1238 | ".mitsubishi", 1239 | ".mlb", 1240 | ".mma", 1241 | ".monash", 1242 | ".mormon", 1243 | ".moto", 1244 | ".movistar", 1245 | ".msd", 1246 | ".mtn", 1247 | ".mtr", 1248 | ".mutual", 1249 | ".nadex", 1250 | ".nationwide", 1251 | ".natura", 1252 | ".nba", 1253 | ".nec", 1254 | ".netflix", 1255 | ".neustar", 1256 | ".newholland", 1257 | ".nexus", 1258 | ".nfl", 1259 | ".nhk", 1260 | ".nico", 1261 | ".nike", 1262 | ".nikon", 1263 | ".nissan", 1264 | ".nissay", 1265 | ".nokia", 1266 | ".northwesternmutual", 1267 | ".norton", 1268 | ".nra", 1269 | ".ntt", 1270 | ".obi", 1271 | ".office", 1272 | ".omega", 1273 | ".oracle", 1274 | ".orange", 1275 | ".otsuka", 1276 | ".ovh", 1277 | ".panasonic", 1278 | ".pccw", 1279 | ".pfizer", 1280 | ".philips", 1281 | ".piaget", 1282 | ".pictet", 1283 | ".ping", 1284 | ".pioneer", 1285 | ".play", 1286 | ".playstation", 1287 | ".pohl", 1288 | ".politie", 1289 | ".praxi", 1290 | ".prod", 1291 | ".progressive", 1292 | ".pru", 1293 | ".prudential", 1294 | ".pwc", 1295 | ".quest", 1296 | ".qvc", 1297 | ".redstone", 1298 | ".reliance", 1299 | ".rexroth", 1300 | ".ricoh", 1301 | ".rmit", 1302 | ".rocher", 1303 | ".rogers", 1304 | ".rwe", 1305 | ".safety", 1306 | ".sakura", 1307 | ".samsung", 1308 | ".sandvik", 1309 | ".sandvikcoromant", 1310 | ".sanofi", 1311 | ".sap", 1312 | ".saxo", 1313 | ".sbi", 1314 | ".sbs", 1315 | ".sca", 1316 | ".scb", 1317 | ".schaeffler", 1318 | ".schmidt", 1319 | ".schwarz", 1320 | ".scjohnson", 1321 | ".scor", 1322 | ".seat", 1323 | ".sener", 1324 | ".ses", 1325 | ".sew", 1326 | ".seven", 1327 | ".sfr", 1328 | ".seek", 1329 | ".shangrila", 1330 | ".sharp", 1331 | ".shaw", 1332 | ".shell", 1333 | ".shriram", 1334 | ".sina", 1335 | ".sky", 1336 | ".skype", 1337 | ".smart", 1338 | ".sncf", 1339 | ".softbank", 1340 | ".sohu", 1341 | ".sony", 1342 | ".spiegel", 1343 | ".stada", 1344 | ".staples", 1345 | ".star", 1346 | ".starhub", 1347 | ".statebank", 1348 | ".statefarm", 1349 | ".statoil", 1350 | ".stc", 1351 | ".stcgroup", 1352 | ".suzuki", 1353 | ".swatch", 1354 | ".swiftcover", 1355 | ".symantec", 1356 | ".taobao", 1357 | ".target", 1358 | ".tatamotors", 1359 | ".tdk", 1360 | ".telecity", 1361 | ".telefonica", 1362 | ".temasek", 1363 | ".teva", 1364 | ".tiffany", 1365 | ".tjx", 1366 | ".toray", 1367 | ".toshiba", 1368 | ".total", 1369 | ".toyota", 1370 | ".travelchannel", 1371 | ".travelers", 1372 | ".tui", 1373 | ".tvs", 1374 | ".ubs", 1375 | ".unicom", 1376 | ".uol", 1377 | ".ups", 1378 | ".vanguard", 1379 | ".verisign", 1380 | ".vig", 1381 | ".viking", 1382 | ".virgin", 1383 | ".visa", 1384 | ".vista", 1385 | ".vistaprint", 1386 | ".vivo", 1387 | ".volkswagen", 1388 | ".volvo", 1389 | ".walmart", 1390 | ".walter", 1391 | ".weatherchannel", 1392 | ".weber", 1393 | ".weir", 1394 | ".williamhill", 1395 | ".windows", 1396 | ".wme", 1397 | ".wolterskluwer", 1398 | ".woodside", 1399 | ".wtc", 1400 | ".xbox", 1401 | ".xerox", 1402 | ".xfinity", 1403 | ".yahoo", 1404 | ".yamaxun", 1405 | ".yandex", 1406 | ".yodobashi", 1407 | ".youtube", 1408 | ".zappos", 1409 | ".zara", 1410 | ".zip", 1411 | ".zippo", 1412 | ".ارامكو", 1413 | ".اتصالات", 1414 | ".联通", 1415 | ".移动", 1416 | ".中信", 1417 | ".香格里拉", 1418 | ".淡马锡", 1419 | ".大众汽车", 1420 | ".vermögensberater", 1421 | ".vermögensberatung", 1422 | ".グーグル", 1423 | ".谷歌", 1424 | ".工行", 1425 | ".嘉里", 1426 | ".嘉里大酒店", 1427 | ".飞利浦", 1428 | ".诺基亚", 1429 | ".電訊盈科", 1430 | ".삼성", 1431 | ".example", 1432 | ".invalid", 1433 | ".local", 1434 | ".localhost", 1435 | ".onion", 1436 | ".test", 1437 | } 1438 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "annotated-types" 5 | version = "0.7.0" 6 | description = "Reusable constraint types to use with typing.Annotated" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, 11 | {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, 12 | ] 13 | 14 | [[package]] 15 | name = "atomicwrites" 16 | version = "1.4.1" 17 | description = "Atomic file writes." 18 | optional = false 19 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 20 | files = [ 21 | {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, 22 | ] 23 | 24 | [[package]] 25 | name = "attrs" 26 | version = "23.2.0" 27 | description = "Classes Without Boilerplate" 28 | optional = false 29 | python-versions = ">=3.7" 30 | files = [ 31 | {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, 32 | {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, 33 | ] 34 | 35 | [package.extras] 36 | cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] 37 | dev = ["attrs[tests]", "pre-commit"] 38 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] 39 | tests = ["attrs[tests-no-zope]", "zope-interface"] 40 | tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] 41 | tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] 42 | 43 | [[package]] 44 | name = "certifi" 45 | version = "2024.6.2" 46 | description = "Python package for providing Mozilla's CA Bundle." 47 | optional = false 48 | python-versions = ">=3.6" 49 | files = [ 50 | {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, 51 | {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, 52 | ] 53 | 54 | [[package]] 55 | name = "cffi" 56 | version = "1.16.0" 57 | description = "Foreign Function Interface for Python calling C code." 58 | optional = false 59 | python-versions = ">=3.8" 60 | files = [ 61 | {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, 62 | {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, 63 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, 64 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, 65 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, 66 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, 67 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, 68 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, 69 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, 70 | {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, 71 | {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, 72 | {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, 73 | {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, 74 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, 75 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, 76 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, 77 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, 78 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, 79 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, 80 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, 81 | {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, 82 | {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, 83 | {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, 84 | {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, 85 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, 86 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, 87 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, 88 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, 89 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, 90 | {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, 91 | {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, 92 | {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, 93 | {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, 94 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, 95 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, 96 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, 97 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, 98 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, 99 | {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, 100 | {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, 101 | {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, 102 | {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, 103 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, 104 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, 105 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, 106 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, 107 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, 108 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, 109 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, 110 | {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, 111 | {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, 112 | {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, 113 | ] 114 | 115 | [package.dependencies] 116 | pycparser = "*" 117 | 118 | [[package]] 119 | name = "cfgv" 120 | version = "3.4.0" 121 | description = "Validate configuration and produce human readable error messages." 122 | optional = false 123 | python-versions = ">=3.8" 124 | files = [ 125 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 126 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 127 | ] 128 | 129 | [[package]] 130 | name = "charset-normalizer" 131 | version = "3.3.2" 132 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 133 | optional = false 134 | python-versions = ">=3.7.0" 135 | files = [ 136 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 137 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 138 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 139 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 140 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 141 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 142 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 143 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 144 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 145 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 146 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 147 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 148 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 149 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 150 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 151 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 152 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 153 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 154 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 155 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 156 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 157 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 158 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 159 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 160 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 161 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 162 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 163 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 164 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 165 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 166 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 167 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 168 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 169 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 170 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 171 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 172 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 173 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 174 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 175 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 176 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 177 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 178 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 179 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 180 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 181 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 182 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 183 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 184 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 185 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 186 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 187 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 188 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 189 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 190 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 191 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 192 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 193 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 194 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 195 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 196 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 197 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 198 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 199 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 200 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 201 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 202 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 203 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 204 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 205 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 206 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 207 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 208 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 209 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 210 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 211 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 212 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 213 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 214 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 215 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 216 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 217 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 218 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 219 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 220 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 221 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 222 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 223 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 224 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 225 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 226 | ] 227 | 228 | [[package]] 229 | name = "click" 230 | version = "8.1.7" 231 | description = "Composable command line interface toolkit" 232 | optional = false 233 | python-versions = ">=3.7" 234 | files = [ 235 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 236 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 237 | ] 238 | 239 | [package.dependencies] 240 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 241 | 242 | [[package]] 243 | name = "colorama" 244 | version = "0.4.6" 245 | description = "Cross-platform colored terminal text." 246 | optional = false 247 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 248 | files = [ 249 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 250 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 251 | ] 252 | 253 | [[package]] 254 | name = "coverage" 255 | version = "7.5.3" 256 | description = "Code coverage measurement for Python" 257 | optional = false 258 | python-versions = ">=3.8" 259 | files = [ 260 | {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"}, 261 | {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"}, 262 | {file = "coverage-7.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286"}, 263 | {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc"}, 264 | {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d"}, 265 | {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83"}, 266 | {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d"}, 267 | {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c"}, 268 | {file = "coverage-7.5.3-cp310-cp310-win32.whl", hash = "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84"}, 269 | {file = "coverage-7.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac"}, 270 | {file = "coverage-7.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974"}, 271 | {file = "coverage-7.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232"}, 272 | {file = "coverage-7.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd"}, 273 | {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807"}, 274 | {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb"}, 275 | {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc"}, 276 | {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8"}, 277 | {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614"}, 278 | {file = "coverage-7.5.3-cp311-cp311-win32.whl", hash = "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9"}, 279 | {file = "coverage-7.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a"}, 280 | {file = "coverage-7.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8"}, 281 | {file = "coverage-7.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3"}, 282 | {file = "coverage-7.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1"}, 283 | {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db"}, 284 | {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd"}, 285 | {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523"}, 286 | {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35"}, 287 | {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84"}, 288 | {file = "coverage-7.5.3-cp312-cp312-win32.whl", hash = "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08"}, 289 | {file = "coverage-7.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb"}, 290 | {file = "coverage-7.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb"}, 291 | {file = "coverage-7.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155"}, 292 | {file = "coverage-7.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24"}, 293 | {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98"}, 294 | {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d"}, 295 | {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d"}, 296 | {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce"}, 297 | {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0"}, 298 | {file = "coverage-7.5.3-cp38-cp38-win32.whl", hash = "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485"}, 299 | {file = "coverage-7.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56"}, 300 | {file = "coverage-7.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85"}, 301 | {file = "coverage-7.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31"}, 302 | {file = "coverage-7.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d"}, 303 | {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341"}, 304 | {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7"}, 305 | {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52"}, 306 | {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303"}, 307 | {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd"}, 308 | {file = "coverage-7.5.3-cp39-cp39-win32.whl", hash = "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d"}, 309 | {file = "coverage-7.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0"}, 310 | {file = "coverage-7.5.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884"}, 311 | {file = "coverage-7.5.3.tar.gz", hash = "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f"}, 312 | ] 313 | 314 | [package.extras] 315 | toml = ["tomli"] 316 | 317 | [[package]] 318 | name = "distlib" 319 | version = "0.3.8" 320 | description = "Distribution utilities" 321 | optional = false 322 | python-versions = "*" 323 | files = [ 324 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 325 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 326 | ] 327 | 328 | [[package]] 329 | name = "exceptiongroup" 330 | version = "1.2.1" 331 | description = "Backport of PEP 654 (exception groups)" 332 | optional = false 333 | python-versions = ">=3.7" 334 | files = [ 335 | {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, 336 | {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, 337 | ] 338 | 339 | [package.extras] 340 | test = ["pytest (>=6)"] 341 | 342 | [[package]] 343 | name = "filelock" 344 | version = "3.14.0" 345 | description = "A platform independent file lock." 346 | optional = false 347 | python-versions = ">=3.8" 348 | files = [ 349 | {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, 350 | {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, 351 | ] 352 | 353 | [package.extras] 354 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 355 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] 356 | typing = ["typing-extensions (>=4.8)"] 357 | 358 | [[package]] 359 | name = "ghp-import" 360 | version = "2.1.0" 361 | description = "Copy your docs directly to the gh-pages branch." 362 | optional = false 363 | python-versions = "*" 364 | files = [ 365 | {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, 366 | {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, 367 | ] 368 | 369 | [package.dependencies] 370 | python-dateutil = ">=2.8.1" 371 | 372 | [package.extras] 373 | dev = ["flake8", "markdown", "twine", "wheel"] 374 | 375 | [[package]] 376 | name = "h11" 377 | version = "0.14.0" 378 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 379 | optional = false 380 | python-versions = ">=3.7" 381 | files = [ 382 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 383 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 384 | ] 385 | 386 | [[package]] 387 | name = "identify" 388 | version = "2.5.36" 389 | description = "File identification library for Python" 390 | optional = false 391 | python-versions = ">=3.8" 392 | files = [ 393 | {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, 394 | {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, 395 | ] 396 | 397 | [package.extras] 398 | license = ["ukkonen"] 399 | 400 | [[package]] 401 | name = "idna" 402 | version = "3.7" 403 | description = "Internationalized Domain Names in Applications (IDNA)" 404 | optional = false 405 | python-versions = ">=3.5" 406 | files = [ 407 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 408 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 409 | ] 410 | 411 | [[package]] 412 | name = "iniconfig" 413 | version = "2.0.0" 414 | description = "brain-dead simple config-ini parsing" 415 | optional = false 416 | python-versions = ">=3.7" 417 | files = [ 418 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 419 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 420 | ] 421 | 422 | [[package]] 423 | name = "isort" 424 | version = "5.13.2" 425 | description = "A Python utility / library to sort Python imports." 426 | optional = false 427 | python-versions = ">=3.8.0" 428 | files = [ 429 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 430 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 431 | ] 432 | 433 | [package.extras] 434 | colors = ["colorama (>=0.4.6)"] 435 | 436 | [[package]] 437 | name = "jinja2" 438 | version = "3.0.3" 439 | description = "A very fast and expressive template engine." 440 | optional = false 441 | python-versions = ">=3.6" 442 | files = [ 443 | {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, 444 | {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, 445 | ] 446 | 447 | [package.dependencies] 448 | MarkupSafe = ">=2.0" 449 | 450 | [package.extras] 451 | i18n = ["Babel (>=2.7)"] 452 | 453 | [[package]] 454 | name = "loguru" 455 | version = "0.5.3" 456 | description = "Python logging made (stupidly) simple" 457 | optional = false 458 | python-versions = ">=3.5" 459 | files = [ 460 | {file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"}, 461 | {file = "loguru-0.5.3.tar.gz", hash = "sha256:b28e72ac7a98be3d28ad28570299a393dfcd32e5e3f6a353dec94675767b6319"}, 462 | ] 463 | 464 | [package.dependencies] 465 | colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} 466 | win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} 467 | 468 | [package.extras] 469 | dev = ["Sphinx (>=2.2.1)", "black (>=19.10b0)", "codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)", "tox-travis (>=0.12)"] 470 | 471 | [[package]] 472 | name = "markdown" 473 | version = "3.6" 474 | description = "Python implementation of John Gruber's Markdown." 475 | optional = false 476 | python-versions = ">=3.8" 477 | files = [ 478 | {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, 479 | {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, 480 | ] 481 | 482 | [package.extras] 483 | docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] 484 | testing = ["coverage", "pyyaml"] 485 | 486 | [[package]] 487 | name = "markupsafe" 488 | version = "2.1.5" 489 | description = "Safely add untrusted strings to HTML/XML markup." 490 | optional = false 491 | python-versions = ">=3.7" 492 | files = [ 493 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, 494 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, 495 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, 496 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, 497 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, 498 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, 499 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, 500 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, 501 | {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, 502 | {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, 503 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, 504 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, 505 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, 506 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, 507 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, 508 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, 509 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, 510 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, 511 | {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, 512 | {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, 513 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, 514 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, 515 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, 516 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, 517 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, 518 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, 519 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, 520 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, 521 | {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, 522 | {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, 523 | {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, 524 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, 525 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, 526 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, 527 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, 528 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, 529 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, 530 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, 531 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, 532 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, 533 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, 534 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, 535 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, 536 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, 537 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, 538 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, 539 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, 540 | {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, 541 | {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, 542 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, 543 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, 544 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, 545 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, 546 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, 547 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, 548 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, 549 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, 550 | {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, 551 | {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, 552 | {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, 553 | ] 554 | 555 | [[package]] 556 | name = "mergedeep" 557 | version = "1.3.4" 558 | description = "A deep merge function for 🐍." 559 | optional = false 560 | python-versions = ">=3.6" 561 | files = [ 562 | {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, 563 | {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, 564 | ] 565 | 566 | [[package]] 567 | name = "mkdocs" 568 | version = "1.6.0" 569 | description = "Project documentation with Markdown." 570 | optional = false 571 | python-versions = ">=3.8" 572 | files = [ 573 | {file = "mkdocs-1.6.0-py3-none-any.whl", hash = "sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7"}, 574 | {file = "mkdocs-1.6.0.tar.gz", hash = "sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512"}, 575 | ] 576 | 577 | [package.dependencies] 578 | click = ">=7.0" 579 | colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} 580 | ghp-import = ">=1.0" 581 | jinja2 = ">=2.11.1" 582 | markdown = ">=3.3.6" 583 | markupsafe = ">=2.0.1" 584 | mergedeep = ">=1.3.4" 585 | mkdocs-get-deps = ">=0.2.0" 586 | packaging = ">=20.5" 587 | pathspec = ">=0.11.1" 588 | pyyaml = ">=5.1" 589 | pyyaml-env-tag = ">=0.1" 590 | watchdog = ">=2.0" 591 | 592 | [package.extras] 593 | i18n = ["babel (>=2.9.0)"] 594 | min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] 595 | 596 | [[package]] 597 | name = "mkdocs-autorefs" 598 | version = "0.2.1" 599 | description = "Automatically link across pages in MkDocs." 600 | optional = false 601 | python-versions = ">=3.6,<4.0" 602 | files = [ 603 | {file = "mkdocs-autorefs-0.2.1.tar.gz", hash = "sha256:b8156d653ed91356e71675ce1fa1186d2b2c2085050012522895c9aa98fca3e5"}, 604 | {file = "mkdocs_autorefs-0.2.1-py3-none-any.whl", hash = "sha256:f301b983a34259df90b3fcf7edc234b5e6c7065bd578781e66fd90b8cfbe76be"}, 605 | ] 606 | 607 | [package.dependencies] 608 | Markdown = ">=3.3,<4.0" 609 | mkdocs = ">=1.1,<2.0" 610 | 611 | [[package]] 612 | name = "mkdocs-get-deps" 613 | version = "0.2.0" 614 | description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" 615 | optional = false 616 | python-versions = ">=3.8" 617 | files = [ 618 | {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, 619 | {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, 620 | ] 621 | 622 | [package.dependencies] 623 | mergedeep = ">=1.3.4" 624 | platformdirs = ">=2.2.0" 625 | pyyaml = ">=5.1" 626 | 627 | [[package]] 628 | name = "mkdocs-material" 629 | version = "7.3.0" 630 | description = "A Material Design theme for MkDocs" 631 | optional = false 632 | python-versions = "*" 633 | files = [ 634 | {file = "mkdocs-material-7.3.0.tar.gz", hash = "sha256:07db0580fa96c3473aee99ec3fb4606a1a5a1e4f4467e64c0cd1ba8da5b6476e"}, 635 | {file = "mkdocs_material-7.3.0-py2.py3-none-any.whl", hash = "sha256:b183c27dc0f44e631bbc32c51057f61a3e2ba8b3c1080e59f944167eeba9ff1d"}, 636 | ] 637 | 638 | [package.dependencies] 639 | markdown = ">=3.2" 640 | mkdocs = ">=1.2.2" 641 | mkdocs-material-extensions = ">=1.0" 642 | Pygments = ">=2.4" 643 | pymdown-extensions = ">=7.0" 644 | 645 | [[package]] 646 | name = "mkdocs-material-extensions" 647 | version = "1.3.1" 648 | description = "Extension pack for Python Markdown and MkDocs Material." 649 | optional = false 650 | python-versions = ">=3.8" 651 | files = [ 652 | {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, 653 | {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, 654 | ] 655 | 656 | [[package]] 657 | name = "mkdocstrings" 658 | version = "0.15.2" 659 | description = "Automatic documentation from sources, for MkDocs." 660 | optional = false 661 | python-versions = ">=3.6,<4.0" 662 | files = [ 663 | {file = "mkdocstrings-0.15.2-py3-none-any.whl", hash = "sha256:8d6cbe64c07ae66739010979ca01d49dd2f64d1a45009f089d217b9cd2a65e36"}, 664 | {file = "mkdocstrings-0.15.2.tar.gz", hash = "sha256:c2fee9a3a644647c06eb2044fdfede1073adfd1a55bf6752005d3db10705fe73"}, 665 | ] 666 | 667 | [package.dependencies] 668 | Jinja2 = ">=2.11.1,<4.0" 669 | Markdown = ">=3.3,<4.0" 670 | MarkupSafe = ">=1.1,<3.0" 671 | mkdocs = ">=1.1.1,<2.0.0" 672 | mkdocs-autorefs = ">=0.1,<0.3" 673 | pymdown-extensions = ">=6.3,<9.0" 674 | pytkdocs = ">=0.2.0,<0.12.0" 675 | 676 | [[package]] 677 | name = "mypy" 678 | version = "1.10.0" 679 | description = "Optional static typing for Python" 680 | optional = false 681 | python-versions = ">=3.8" 682 | files = [ 683 | {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, 684 | {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, 685 | {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, 686 | {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, 687 | {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, 688 | {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, 689 | {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, 690 | {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, 691 | {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, 692 | {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, 693 | {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, 694 | {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, 695 | {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, 696 | {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, 697 | {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, 698 | {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, 699 | {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, 700 | {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, 701 | {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, 702 | {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, 703 | {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, 704 | {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, 705 | {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, 706 | {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, 707 | {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, 708 | {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, 709 | {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, 710 | ] 711 | 712 | [package.dependencies] 713 | mypy-extensions = ">=1.0.0" 714 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 715 | typing-extensions = ">=4.1.0" 716 | 717 | [package.extras] 718 | dmypy = ["psutil (>=4.0)"] 719 | install-types = ["pip"] 720 | mypyc = ["setuptools (>=50)"] 721 | reports = ["lxml"] 722 | 723 | [[package]] 724 | name = "mypy-extensions" 725 | version = "1.0.0" 726 | description = "Type system extensions for programs checked with the mypy type checker." 727 | optional = false 728 | python-versions = ">=3.5" 729 | files = [ 730 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 731 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 732 | ] 733 | 734 | [[package]] 735 | name = "nodeenv" 736 | version = "1.9.0" 737 | description = "Node.js virtual environment builder" 738 | optional = false 739 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 740 | files = [ 741 | {file = "nodeenv-1.9.0-py2.py3-none-any.whl", hash = "sha256:508ecec98f9f3330b636d4448c0f1a56fc68017c68f1e7857ebc52acf0eb879a"}, 742 | {file = "nodeenv-1.9.0.tar.gz", hash = "sha256:07f144e90dae547bf0d4ee8da0ee42664a42a04e02ed68e06324348dafe4bdb1"}, 743 | ] 744 | 745 | [[package]] 746 | name = "outcome" 747 | version = "1.3.0.post0" 748 | description = "Capture the outcome of Python function calls." 749 | optional = false 750 | python-versions = ">=3.7" 751 | files = [ 752 | {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, 753 | {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, 754 | ] 755 | 756 | [package.dependencies] 757 | attrs = ">=19.2.0" 758 | 759 | [[package]] 760 | name = "packaging" 761 | version = "24.0" 762 | description = "Core utilities for Python packages" 763 | optional = false 764 | python-versions = ">=3.7" 765 | files = [ 766 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 767 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 768 | ] 769 | 770 | [[package]] 771 | name = "pathspec" 772 | version = "0.12.1" 773 | description = "Utility library for gitignore style pattern matching of file paths." 774 | optional = false 775 | python-versions = ">=3.8" 776 | files = [ 777 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 778 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 779 | ] 780 | 781 | [[package]] 782 | name = "platformdirs" 783 | version = "4.2.2" 784 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 785 | optional = false 786 | python-versions = ">=3.8" 787 | files = [ 788 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 789 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 790 | ] 791 | 792 | [package.extras] 793 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 794 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 795 | type = ["mypy (>=1.8)"] 796 | 797 | [[package]] 798 | name = "pluggy" 799 | version = "1.5.0" 800 | description = "plugin and hook calling mechanisms for python" 801 | optional = false 802 | python-versions = ">=3.8" 803 | files = [ 804 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 805 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 806 | ] 807 | 808 | [package.extras] 809 | dev = ["pre-commit", "tox"] 810 | testing = ["pytest", "pytest-benchmark"] 811 | 812 | [[package]] 813 | name = "pre-commit" 814 | version = "2.21.0" 815 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 816 | optional = false 817 | python-versions = ">=3.7" 818 | files = [ 819 | {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, 820 | {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, 821 | ] 822 | 823 | [package.dependencies] 824 | cfgv = ">=2.0.0" 825 | identify = ">=1.0.0" 826 | nodeenv = ">=0.11.1" 827 | pyyaml = ">=5.1" 828 | virtualenv = ">=20.10.0" 829 | 830 | [[package]] 831 | name = "py" 832 | version = "1.11.0" 833 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 834 | optional = false 835 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 836 | files = [ 837 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 838 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 839 | ] 840 | 841 | [[package]] 842 | name = "pycparser" 843 | version = "2.22" 844 | description = "C parser in Python" 845 | optional = false 846 | python-versions = ">=3.8" 847 | files = [ 848 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 849 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 850 | ] 851 | 852 | [[package]] 853 | name = "pydantic" 854 | version = "2.7.2" 855 | description = "Data validation using Python type hints" 856 | optional = false 857 | python-versions = ">=3.8" 858 | files = [ 859 | {file = "pydantic-2.7.2-py3-none-any.whl", hash = "sha256:834ab954175f94e6e68258537dc49402c4a5e9d0409b9f1b86b7e934a8372de7"}, 860 | {file = "pydantic-2.7.2.tar.gz", hash = "sha256:71b2945998f9c9b7919a45bde9a50397b289937d215ae141c1d0903ba7149fd7"}, 861 | ] 862 | 863 | [package.dependencies] 864 | annotated-types = ">=0.4.0" 865 | pydantic-core = "2.18.3" 866 | typing-extensions = ">=4.6.1" 867 | 868 | [package.extras] 869 | email = ["email-validator (>=2.0.0)"] 870 | 871 | [[package]] 872 | name = "pydantic-core" 873 | version = "2.18.3" 874 | description = "Core functionality for Pydantic validation and serialization" 875 | optional = false 876 | python-versions = ">=3.8" 877 | files = [ 878 | {file = "pydantic_core-2.18.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:744697428fcdec6be5670460b578161d1ffe34743a5c15656be7ea82b008197c"}, 879 | {file = "pydantic_core-2.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b40c05ced1ba4218b14986fe6f283d22e1ae2ff4c8e28881a70fb81fbfcda7"}, 880 | {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:544a9a75622357076efb6b311983ff190fbfb3c12fc3a853122b34d3d358126c"}, 881 | {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2e253af04ceaebde8eb201eb3f3e3e7e390f2d275a88300d6a1959d710539e2"}, 882 | {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:855ec66589c68aa367d989da5c4755bb74ee92ccad4fdb6af942c3612c067e34"}, 883 | {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d3e42bb54e7e9d72c13ce112e02eb1b3b55681ee948d748842171201a03a98a"}, 884 | {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6ac9ffccc9d2e69d9fba841441d4259cb668ac180e51b30d3632cd7abca2b9b"}, 885 | {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c56eca1686539fa0c9bda992e7bd6a37583f20083c37590413381acfc5f192d6"}, 886 | {file = "pydantic_core-2.18.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:17954d784bf8abfc0ec2a633108207ebc4fa2df1a0e4c0c3ccbaa9bb01d2c426"}, 887 | {file = "pydantic_core-2.18.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:98ed737567d8f2ecd54f7c8d4f8572ca7c7921ede93a2e52939416170d357812"}, 888 | {file = "pydantic_core-2.18.3-cp310-none-win32.whl", hash = "sha256:9f9e04afebd3ed8c15d67a564ed0a34b54e52136c6d40d14c5547b238390e779"}, 889 | {file = "pydantic_core-2.18.3-cp310-none-win_amd64.whl", hash = "sha256:45e4ffbae34f7ae30d0047697e724e534a7ec0a82ef9994b7913a412c21462a0"}, 890 | {file = "pydantic_core-2.18.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b9ebe8231726c49518b16b237b9fe0d7d361dd221302af511a83d4ada01183ab"}, 891 | {file = "pydantic_core-2.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b8e20e15d18bf7dbb453be78a2d858f946f5cdf06c5072453dace00ab652e2b2"}, 892 | {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0d9ff283cd3459fa0bf9b0256a2b6f01ac1ff9ffb034e24457b9035f75587cb"}, 893 | {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f7ef5f0ebb77ba24c9970da18b771711edc5feaf00c10b18461e0f5f5949231"}, 894 | {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73038d66614d2e5cde30435b5afdced2b473b4c77d4ca3a8624dd3e41a9c19be"}, 895 | {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6afd5c867a74c4d314c557b5ea9520183fadfbd1df4c2d6e09fd0d990ce412cd"}, 896 | {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd7df92f28d351bb9f12470f4c533cf03d1b52ec5a6e5c58c65b183055a60106"}, 897 | {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:80aea0ffeb1049336043d07799eace1c9602519fb3192916ff525b0287b2b1e4"}, 898 | {file = "pydantic_core-2.18.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaee40f25bba38132e655ffa3d1998a6d576ba7cf81deff8bfa189fb43fd2bbe"}, 899 | {file = "pydantic_core-2.18.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9128089da8f4fe73f7a91973895ebf2502539d627891a14034e45fb9e707e26d"}, 900 | {file = "pydantic_core-2.18.3-cp311-none-win32.whl", hash = "sha256:fec02527e1e03257aa25b1a4dcbe697b40a22f1229f5d026503e8b7ff6d2eda7"}, 901 | {file = "pydantic_core-2.18.3-cp311-none-win_amd64.whl", hash = "sha256:58ff8631dbab6c7c982e6425da8347108449321f61fe427c52ddfadd66642af7"}, 902 | {file = "pydantic_core-2.18.3-cp311-none-win_arm64.whl", hash = "sha256:3fc1c7f67f34c6c2ef9c213e0f2a351797cda98249d9ca56a70ce4ebcaba45f4"}, 903 | {file = "pydantic_core-2.18.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f0928cde2ae416a2d1ebe6dee324709c6f73e93494d8c7aea92df99aab1fc40f"}, 904 | {file = "pydantic_core-2.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bee9bb305a562f8b9271855afb6ce00223f545de3d68560b3c1649c7c5295e9"}, 905 | {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e862823be114387257dacbfa7d78547165a85d7add33b446ca4f4fae92c7ff5c"}, 906 | {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a36f78674cbddc165abab0df961b5f96b14461d05feec5e1f78da58808b97e7"}, 907 | {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba905d184f62e7ddbb7a5a751d8a5c805463511c7b08d1aca4a3e8c11f2e5048"}, 908 | {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fdd362f6a586e681ff86550b2379e532fee63c52def1c666887956748eaa326"}, 909 | {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24b214b7ee3bd3b865e963dbed0f8bc5375f49449d70e8d407b567af3222aae4"}, 910 | {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:691018785779766127f531674fa82bb368df5b36b461622b12e176c18e119022"}, 911 | {file = "pydantic_core-2.18.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:60e4c625e6f7155d7d0dcac151edf5858102bc61bf959d04469ca6ee4e8381bd"}, 912 | {file = "pydantic_core-2.18.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4e651e47d981c1b701dcc74ab8fec5a60a5b004650416b4abbef13db23bc7be"}, 913 | {file = "pydantic_core-2.18.3-cp312-none-win32.whl", hash = "sha256:ffecbb5edb7f5ffae13599aec33b735e9e4c7676ca1633c60f2c606beb17efc5"}, 914 | {file = "pydantic_core-2.18.3-cp312-none-win_amd64.whl", hash = "sha256:2c8333f6e934733483c7eddffdb094c143b9463d2af7e6bd85ebcb2d4a1b82c6"}, 915 | {file = "pydantic_core-2.18.3-cp312-none-win_arm64.whl", hash = "sha256:7a20dded653e516a4655f4c98e97ccafb13753987434fe7cf044aa25f5b7d417"}, 916 | {file = "pydantic_core-2.18.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:eecf63195be644b0396f972c82598cd15693550f0ff236dcf7ab92e2eb6d3522"}, 917 | {file = "pydantic_core-2.18.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c44efdd3b6125419c28821590d7ec891c9cb0dff33a7a78d9d5c8b6f66b9702"}, 918 | {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e59fca51ffbdd1638b3856779342ed69bcecb8484c1d4b8bdb237d0eb5a45e2"}, 919 | {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70cf099197d6b98953468461d753563b28e73cf1eade2ffe069675d2657ed1d5"}, 920 | {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63081a49dddc6124754b32a3774331467bfc3d2bd5ff8f10df36a95602560361"}, 921 | {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:370059b7883485c9edb9655355ff46d912f4b03b009d929220d9294c7fd9fd60"}, 922 | {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a64faeedfd8254f05f5cf6fc755023a7e1606af3959cfc1a9285744cc711044"}, 923 | {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19d2e725de0f90d8671f89e420d36c3dd97639b98145e42fcc0e1f6d492a46dc"}, 924 | {file = "pydantic_core-2.18.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:67bc078025d70ec5aefe6200ef094576c9d86bd36982df1301c758a9fff7d7f4"}, 925 | {file = "pydantic_core-2.18.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:adf952c3f4100e203cbaf8e0c907c835d3e28f9041474e52b651761dc248a3c0"}, 926 | {file = "pydantic_core-2.18.3-cp38-none-win32.whl", hash = "sha256:9a46795b1f3beb167eaee91736d5d17ac3a994bf2215a996aed825a45f897558"}, 927 | {file = "pydantic_core-2.18.3-cp38-none-win_amd64.whl", hash = "sha256:200ad4e3133cb99ed82342a101a5abf3d924722e71cd581cc113fe828f727fbc"}, 928 | {file = "pydantic_core-2.18.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:304378b7bf92206036c8ddd83a2ba7b7d1a5b425acafff637172a3aa72ad7083"}, 929 | {file = "pydantic_core-2.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c826870b277143e701c9ccf34ebc33ddb4d072612683a044e7cce2d52f6c3fef"}, 930 | {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e201935d282707394f3668380e41ccf25b5794d1b131cdd96b07f615a33ca4b1"}, 931 | {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5560dda746c44b48bf82b3d191d74fe8efc5686a9ef18e69bdabccbbb9ad9442"}, 932 | {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b32c2a1f8032570842257e4c19288eba9a2bba4712af542327de9a1204faff8"}, 933 | {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:929c24e9dea3990bc8bcd27c5f2d3916c0c86f5511d2caa69e0d5290115344a9"}, 934 | {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1a8376fef60790152564b0eab376b3e23dd6e54f29d84aad46f7b264ecca943"}, 935 | {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dccf3ef1400390ddd1fb55bf0632209d39140552d068ee5ac45553b556780e06"}, 936 | {file = "pydantic_core-2.18.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:41dbdcb0c7252b58fa931fec47937edb422c9cb22528f41cb8963665c372caf6"}, 937 | {file = "pydantic_core-2.18.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:666e45cf071669fde468886654742fa10b0e74cd0fa0430a46ba6056b24fb0af"}, 938 | {file = "pydantic_core-2.18.3-cp39-none-win32.whl", hash = "sha256:f9c08cabff68704a1b4667d33f534d544b8a07b8e5d039c37067fceb18789e78"}, 939 | {file = "pydantic_core-2.18.3-cp39-none-win_amd64.whl", hash = "sha256:4afa5f5973e8572b5c0dcb4e2d4fda7890e7cd63329bd5cc3263a25c92ef0026"}, 940 | {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:77319771a026f7c7d29c6ebc623de889e9563b7087911b46fd06c044a12aa5e9"}, 941 | {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:df11fa992e9f576473038510d66dd305bcd51d7dd508c163a8c8fe148454e059"}, 942 | {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d531076bdfb65af593326ffd567e6ab3da145020dafb9187a1d131064a55f97c"}, 943 | {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d33ce258e4e6e6038f2b9e8b8a631d17d017567db43483314993b3ca345dcbbb"}, 944 | {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1f9cd7f5635b719939019be9bda47ecb56e165e51dd26c9a217a433e3d0d59a9"}, 945 | {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cd4a032bb65cc132cae1fe3e52877daecc2097965cd3914e44fbd12b00dae7c5"}, 946 | {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f2718430098bcdf60402136c845e4126a189959d103900ebabb6774a5d9fdb"}, 947 | {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c0037a92cf0c580ed14e10953cdd26528e8796307bb8bb312dc65f71547df04d"}, 948 | {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b95a0972fac2b1ff3c94629fc9081b16371dad870959f1408cc33b2f78ad347a"}, 949 | {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a62e437d687cc148381bdd5f51e3e81f5b20a735c55f690c5be94e05da2b0d5c"}, 950 | {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b367a73a414bbb08507da102dc2cde0fa7afe57d09b3240ce82a16d608a7679c"}, 951 | {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ecce4b2360aa3f008da3327d652e74a0e743908eac306198b47e1c58b03dd2b"}, 952 | {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd4435b8d83f0c9561a2a9585b1de78f1abb17cb0cef5f39bf6a4b47d19bafe3"}, 953 | {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:616221a6d473c5b9aa83fa8982745441f6a4a62a66436be9445c65f241b86c94"}, 954 | {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7e6382ce89a92bc1d0c0c5edd51e931432202b9080dc921d8d003e616402efd1"}, 955 | {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff58f379345603d940e461eae474b6bbb6dab66ed9a851ecd3cb3709bf4dcf6a"}, 956 | {file = "pydantic_core-2.18.3.tar.gz", hash = "sha256:432e999088d85c8f36b9a3f769a8e2b57aabd817bbb729a90d1fe7f18f6f1f39"}, 957 | ] 958 | 959 | [package.dependencies] 960 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 961 | 962 | [[package]] 963 | name = "pygments" 964 | version = "2.18.0" 965 | description = "Pygments is a syntax highlighting package written in Python." 966 | optional = false 967 | python-versions = ">=3.8" 968 | files = [ 969 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 970 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 971 | ] 972 | 973 | [package.extras] 974 | windows-terminal = ["colorama (>=0.4.6)"] 975 | 976 | [[package]] 977 | name = "pymdown-extensions" 978 | version = "8.2" 979 | description = "Extension pack for Python Markdown." 980 | optional = false 981 | python-versions = ">=3.6" 982 | files = [ 983 | {file = "pymdown-extensions-8.2.tar.gz", hash = "sha256:b6daa94aad9e1310f9c64c8b1f01e4ce82937ab7eb53bfc92876a97aca02a6f4"}, 984 | {file = "pymdown_extensions-8.2-py3-none-any.whl", hash = "sha256:141452d8ed61165518f2c923454bf054866b85cf466feedb0eb68f04acdc2560"}, 985 | ] 986 | 987 | [package.dependencies] 988 | Markdown = ">=3.2" 989 | 990 | [[package]] 991 | name = "pysocks" 992 | version = "1.7.1" 993 | description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." 994 | optional = false 995 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 996 | files = [ 997 | {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, 998 | {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, 999 | {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, 1000 | ] 1001 | 1002 | [[package]] 1003 | name = "pytest" 1004 | version = "6.2.5" 1005 | description = "pytest: simple powerful testing with Python" 1006 | optional = false 1007 | python-versions = ">=3.6" 1008 | files = [ 1009 | {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, 1010 | {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, 1011 | ] 1012 | 1013 | [package.dependencies] 1014 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 1015 | attrs = ">=19.2.0" 1016 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 1017 | iniconfig = "*" 1018 | packaging = "*" 1019 | pluggy = ">=0.12,<2.0" 1020 | py = ">=1.8.2" 1021 | toml = "*" 1022 | 1023 | [package.extras] 1024 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 1025 | 1026 | [[package]] 1027 | name = "pytest-async" 1028 | version = "0.1.1" 1029 | description = "pytest-async - Run your coroutine in event loop without decorator" 1030 | optional = false 1031 | python-versions = ">=3.6" 1032 | files = [ 1033 | {file = "pytest_async-0.1.1-py3-none-any.whl", hash = "sha256:11cc41eef82592951d56c2bb9b0e6ab21b2f0f00663e78d95694a80d965be930"}, 1034 | {file = "pytest_async-0.1.1.tar.gz", hash = "sha256:0d6ffd3ebac2f3aa47d606dbae1984750268a89dc8caf4a908ba61c60299cdfd"}, 1035 | ] 1036 | 1037 | [[package]] 1038 | name = "pytest-cov" 1039 | version = "2.12.1" 1040 | description = "Pytest plugin for measuring coverage." 1041 | optional = false 1042 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 1043 | files = [ 1044 | {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, 1045 | {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, 1046 | ] 1047 | 1048 | [package.dependencies] 1049 | coverage = ">=5.2.1" 1050 | pytest = ">=4.6" 1051 | toml = "*" 1052 | 1053 | [package.extras] 1054 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 1055 | 1056 | [[package]] 1057 | name = "python-dateutil" 1058 | version = "2.9.0.post0" 1059 | description = "Extensions to the standard Python datetime module" 1060 | optional = false 1061 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 1062 | files = [ 1063 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 1064 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 1065 | ] 1066 | 1067 | [package.dependencies] 1068 | six = ">=1.5" 1069 | 1070 | [[package]] 1071 | name = "pytkdocs" 1072 | version = "0.11.1" 1073 | description = "Load Python objects documentation." 1074 | optional = false 1075 | python-versions = ">=3.6.1,<4.0.0" 1076 | files = [ 1077 | {file = "pytkdocs-0.11.1-py3-none-any.whl", hash = "sha256:89ca4926d0acc266235beb24cb0b0591aa6bf7adedfae54bf9421d529d782c8d"}, 1078 | {file = "pytkdocs-0.11.1.tar.gz", hash = "sha256:1ec7e028fe8361acc1ce909ada4e6beabec28ef31e629618549109e1d58549f0"}, 1079 | ] 1080 | 1081 | [package.extras] 1082 | numpy-style = ["docstring_parser (>=0.7.3,<0.8.0)"] 1083 | 1084 | [[package]] 1085 | name = "pyyaml" 1086 | version = "6.0.1" 1087 | description = "YAML parser and emitter for Python" 1088 | optional = false 1089 | python-versions = ">=3.6" 1090 | files = [ 1091 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 1092 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 1093 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 1094 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 1095 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 1096 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 1097 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 1098 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 1099 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 1100 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 1101 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 1102 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 1103 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 1104 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 1105 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 1106 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 1107 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 1108 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 1109 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, 1110 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 1111 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 1112 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 1113 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 1114 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 1115 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 1116 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 1117 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 1118 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 1119 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 1120 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 1121 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 1122 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 1123 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 1124 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 1125 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 1126 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 1127 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 1128 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 1129 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 1130 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 1131 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 1132 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 1133 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 1134 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 1135 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 1136 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 1137 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 1138 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 1139 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 1140 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 1141 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 1142 | ] 1143 | 1144 | [[package]] 1145 | name = "pyyaml-env-tag" 1146 | version = "0.1" 1147 | description = "A custom YAML tag for referencing environment variables in YAML files. " 1148 | optional = false 1149 | python-versions = ">=3.6" 1150 | files = [ 1151 | {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, 1152 | {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, 1153 | ] 1154 | 1155 | [package.dependencies] 1156 | pyyaml = "*" 1157 | 1158 | [[package]] 1159 | name = "requests" 1160 | version = "2.32.3" 1161 | description = "Python HTTP for Humans." 1162 | optional = false 1163 | python-versions = ">=3.8" 1164 | files = [ 1165 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 1166 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 1167 | ] 1168 | 1169 | [package.dependencies] 1170 | certifi = ">=2017.4.17" 1171 | charset-normalizer = ">=2,<4" 1172 | idna = ">=2.5,<4" 1173 | urllib3 = ">=1.21.1,<3" 1174 | 1175 | [package.extras] 1176 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 1177 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 1178 | 1179 | [[package]] 1180 | name = "ruff" 1181 | version = "0.1.15" 1182 | description = "An extremely fast Python linter and code formatter, written in Rust." 1183 | optional = false 1184 | python-versions = ">=3.7" 1185 | files = [ 1186 | {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, 1187 | {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, 1188 | {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, 1189 | {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, 1190 | {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, 1191 | {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, 1192 | {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, 1193 | {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, 1194 | {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, 1195 | {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, 1196 | {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, 1197 | {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, 1198 | {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, 1199 | {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, 1200 | {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, 1201 | {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, 1202 | {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, 1203 | ] 1204 | 1205 | [[package]] 1206 | name = "selenium" 1207 | version = "4.21.0" 1208 | description = "" 1209 | optional = false 1210 | python-versions = ">=3.8" 1211 | files = [ 1212 | {file = "selenium-4.21.0-py3-none-any.whl", hash = "sha256:4770ffe5a5264e609de7dc914be6b89987512040d5a8efb2abb181330d097993"}, 1213 | {file = "selenium-4.21.0.tar.gz", hash = "sha256:650dbfa5159895ff00ad16e5ddb6ceecb86b90c7ed2012b3f041f64e6e4904fe"}, 1214 | ] 1215 | 1216 | [package.dependencies] 1217 | certifi = ">=2021.10.8" 1218 | trio = ">=0.17,<1.0" 1219 | trio-websocket = ">=0.9,<1.0" 1220 | typing_extensions = ">=4.9.0" 1221 | urllib3 = {version = ">=1.26,<3", extras = ["socks"]} 1222 | 1223 | [[package]] 1224 | name = "six" 1225 | version = "1.16.0" 1226 | description = "Python 2 and 3 compatibility utilities" 1227 | optional = false 1228 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 1229 | files = [ 1230 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1231 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1232 | ] 1233 | 1234 | [[package]] 1235 | name = "sniffio" 1236 | version = "1.3.1" 1237 | description = "Sniff out which async library your code is running under" 1238 | optional = false 1239 | python-versions = ">=3.7" 1240 | files = [ 1241 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 1242 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 1243 | ] 1244 | 1245 | [[package]] 1246 | name = "sortedcontainers" 1247 | version = "2.4.0" 1248 | description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" 1249 | optional = false 1250 | python-versions = "*" 1251 | files = [ 1252 | {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, 1253 | {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, 1254 | ] 1255 | 1256 | [[package]] 1257 | name = "toml" 1258 | version = "0.10.2" 1259 | description = "Python Library for Tom's Obvious, Minimal Language" 1260 | optional = false 1261 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 1262 | files = [ 1263 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1264 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1265 | ] 1266 | 1267 | [[package]] 1268 | name = "tomli" 1269 | version = "2.0.1" 1270 | description = "A lil' TOML parser" 1271 | optional = false 1272 | python-versions = ">=3.7" 1273 | files = [ 1274 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1275 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1276 | ] 1277 | 1278 | [[package]] 1279 | name = "tox" 1280 | version = "3.28.0" 1281 | description = "tox is a generic virtualenv management and test command line tool" 1282 | optional = false 1283 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 1284 | files = [ 1285 | {file = "tox-3.28.0-py2.py3-none-any.whl", hash = "sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea"}, 1286 | {file = "tox-3.28.0.tar.gz", hash = "sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640"}, 1287 | ] 1288 | 1289 | [package.dependencies] 1290 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 1291 | filelock = ">=3.0.0" 1292 | packaging = ">=14" 1293 | pluggy = ">=0.12.0" 1294 | py = ">=1.4.17" 1295 | six = ">=1.14.0" 1296 | tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} 1297 | virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" 1298 | 1299 | [package.extras] 1300 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 1301 | testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] 1302 | 1303 | [[package]] 1304 | name = "trio" 1305 | version = "0.25.1" 1306 | description = "A friendly Python library for async concurrency and I/O" 1307 | optional = false 1308 | python-versions = ">=3.8" 1309 | files = [ 1310 | {file = "trio-0.25.1-py3-none-any.whl", hash = "sha256:e42617ba091e7b2e50c899052e83a3c403101841de925187f61e7b7eaebdf3fb"}, 1311 | {file = "trio-0.25.1.tar.gz", hash = "sha256:9f5314f014ea3af489e77b001861c535005c3858d38ec46b6b071ebfa339d7fb"}, 1312 | ] 1313 | 1314 | [package.dependencies] 1315 | attrs = ">=23.2.0" 1316 | cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} 1317 | exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} 1318 | idna = "*" 1319 | outcome = "*" 1320 | sniffio = ">=1.3.0" 1321 | sortedcontainers = "*" 1322 | 1323 | [[package]] 1324 | name = "trio-websocket" 1325 | version = "0.11.1" 1326 | description = "WebSocket library for Trio" 1327 | optional = false 1328 | python-versions = ">=3.7" 1329 | files = [ 1330 | {file = "trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f"}, 1331 | {file = "trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"}, 1332 | ] 1333 | 1334 | [package.dependencies] 1335 | exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} 1336 | trio = ">=0.11" 1337 | wsproto = ">=0.14" 1338 | 1339 | [[package]] 1340 | name = "typing-extensions" 1341 | version = "4.12.1" 1342 | description = "Backported and Experimental Type Hints for Python 3.8+" 1343 | optional = false 1344 | python-versions = ">=3.8" 1345 | files = [ 1346 | {file = "typing_extensions-4.12.1-py3-none-any.whl", hash = "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a"}, 1347 | {file = "typing_extensions-4.12.1.tar.gz", hash = "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1"}, 1348 | ] 1349 | 1350 | [[package]] 1351 | name = "urllib3" 1352 | version = "2.2.1" 1353 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1354 | optional = false 1355 | python-versions = ">=3.8" 1356 | files = [ 1357 | {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, 1358 | {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, 1359 | ] 1360 | 1361 | [package.dependencies] 1362 | pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} 1363 | 1364 | [package.extras] 1365 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 1366 | h2 = ["h2 (>=4,<5)"] 1367 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1368 | zstd = ["zstandard (>=0.18.0)"] 1369 | 1370 | [[package]] 1371 | name = "virtualenv" 1372 | version = "20.26.2" 1373 | description = "Virtual Python Environment builder" 1374 | optional = false 1375 | python-versions = ">=3.7" 1376 | files = [ 1377 | {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, 1378 | {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, 1379 | ] 1380 | 1381 | [package.dependencies] 1382 | distlib = ">=0.3.7,<1" 1383 | filelock = ">=3.12.2,<4" 1384 | platformdirs = ">=3.9.1,<5" 1385 | 1386 | [package.extras] 1387 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 1388 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 1389 | 1390 | [[package]] 1391 | name = "watchdog" 1392 | version = "4.0.1" 1393 | description = "Filesystem events monitoring" 1394 | optional = false 1395 | python-versions = ">=3.8" 1396 | files = [ 1397 | {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"}, 1398 | {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"}, 1399 | {file = "watchdog-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b"}, 1400 | {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5"}, 1401 | {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767"}, 1402 | {file = "watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459"}, 1403 | {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175"}, 1404 | {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7"}, 1405 | {file = "watchdog-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28"}, 1406 | {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35"}, 1407 | {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db"}, 1408 | {file = "watchdog-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709"}, 1409 | {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba"}, 1410 | {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235"}, 1411 | {file = "watchdog-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682"}, 1412 | {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7"}, 1413 | {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5"}, 1414 | {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193"}, 1415 | {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625"}, 1416 | {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd"}, 1417 | {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee"}, 1418 | {file = "watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253"}, 1419 | {file = "watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d"}, 1420 | {file = "watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6"}, 1421 | {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57"}, 1422 | {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e"}, 1423 | {file = "watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5"}, 1424 | {file = "watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84"}, 1425 | {file = "watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429"}, 1426 | {file = "watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a"}, 1427 | {file = "watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d"}, 1428 | {file = "watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44"}, 1429 | ] 1430 | 1431 | [package.extras] 1432 | watchmedo = ["PyYAML (>=3.10)"] 1433 | 1434 | [[package]] 1435 | name = "win32-setctime" 1436 | version = "1.1.0" 1437 | description = "A small Python utility to set file creation time on Windows" 1438 | optional = false 1439 | python-versions = ">=3.5" 1440 | files = [ 1441 | {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, 1442 | {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, 1443 | ] 1444 | 1445 | [package.extras] 1446 | dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] 1447 | 1448 | [[package]] 1449 | name = "wsproto" 1450 | version = "1.2.0" 1451 | description = "WebSockets state-machine based protocol implementation" 1452 | optional = false 1453 | python-versions = ">=3.7.0" 1454 | files = [ 1455 | {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, 1456 | {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, 1457 | ] 1458 | 1459 | [package.dependencies] 1460 | h11 = ">=0.9.0,<1" 1461 | 1462 | [extras] 1463 | all = ["requests", "selenium"] 1464 | requests = ["requests"] 1465 | selenium = ["selenium"] 1466 | 1467 | [metadata] 1468 | lock-version = "2.0" 1469 | python-versions = ">=3.10,<3.13" 1470 | content-hash = "6e41d2d5f113837d46e6a1743cfeac494a175ecdf5955f1d744c0c96df13b929" 1471 | --------------------------------------------------------------------------------