formatting change. replaced setuptools with poetry

This commit is contained in:
Eddy Hintze 2024-07-31 19:59:58 -04:00
parent e6fa9e93ee
commit 623a64d501
12 changed files with 1037 additions and 251 deletions

191
.markdownlint.yaml Normal file
View File

@ -0,0 +1,191 @@
# Example markdownlint configuration with all properties set to their default value
# Default state for all rules
default: true
# MD001/heading-increment : Heading levels should only increment by one level at a time : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md001.md
MD001: true
# MD007/ul-indent : Unordered list indentation : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md007.md
MD007:
# Spaces for indent
indent: 4
# Whether to indent the first level of the list
start_indented: false
# Spaces for first level indent (when start_indented is set)
start_indent: 4
# MD009/no-trailing-spaces : Trailing spaces : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md009.md
MD009:
# Spaces for line break
br_spaces: 2
# Allow spaces for empty lines in list items
list_item_empty_lines: false
# Include unnecessary breaks
strict: false
# MD010/no-hard-tabs : Hard tabs : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md010.md
MD010:
# Include code blocks
code_blocks: true
# Fenced code languages to ignore
ignore_code_languages: []
# Number of spaces for each hard tab
spaces_per_tab: 4
# MD012/no-multiple-blanks : Multiple consecutive blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md012.md
MD012:
# Consecutive blank lines
maximum: 2
# MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md013.md
MD013:
# Number of characters
line_length: 119
# Number of characters for headings
heading_line_length: 119
# Number of characters for code blocks
code_block_line_length: 119
# Include code blocks
code_blocks: true
# Include tables
tables: true
# Include headings
headings: true
# Strict length checking
strict: false
# Stern length checking
stern: false
# MD022/blanks-around-headings : Headings should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md022.md
MD022:
# Blank lines above heading
lines_above: 2
# Blank lines below heading
lines_below: 1
# MD023/heading-start-left : Headings must start at the beginning of the line : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md023.md
MD023: true
# MD025/single-title/single-h1 : Multiple top-level headings in the same document : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md025.md
MD025:
# Heading level
level: 1
# RegExp for matching title in front matter
front_matter_title: "^\\s*title\\s*[:=]"
# MD026/no-trailing-punctuation : Trailing punctuation in heading : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md026.md
MD026:
# Punctuation characters
punctuation: ".,;:!。,;:!"
# MD027/no-multiple-space-blockquote : Multiple spaces after blockquote symbol : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md027.md
MD027: true
# MD028/no-blanks-blockquote : Blank line inside blockquote : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md028.md
MD028: true
# MD029/ol-prefix : Ordered list item prefix : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md029.md
MD029:
# List style
style: "ordered"
# MD031/blanks-around-fences : Fenced code blocks should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md031.md
MD031:
# Include list items
list_items: true
# MD032/blanks-around-lists : Lists should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md032.md
MD032: true
# MD034/no-bare-urls : Bare URL used : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md034.md
MD034: true
# MD036/no-emphasis-as-heading : Emphasis used instead of a heading : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md036.md
MD036:
# Punctuation characters
punctuation: ".,;:!?。,;:!?"
# MD037/no-space-in-emphasis : Spaces inside emphasis markers : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md037.md
MD037: true
# MD038/no-space-in-code : Spaces inside code span elements : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md038.md
MD038: true
# MD039/no-space-in-links : Spaces inside link text : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md039.md
MD039: true
# MD040/fenced-code-language : Fenced code blocks should have a language specified : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md040.md
MD040:
# Require language only
language_only: true
# MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md041.md
MD041:
# Heading level
level: 1
# RegExp for matching title in front matter
front_matter_title: "^\\s*title\\s*[:=]"
# MD042/no-empty-links : No empty links : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md042.md
MD042: true
# MD046/code-block-style : Code block style : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md046.md
MD046:
# Block style
style: "consistent"
# MD047/single-trailing-newline : Files should end with a single newline character : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md047.md
MD047: true
# MD048/code-fence-style : Code fence style : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md048.md
MD048:
# Code fence style
style: "consistent"
# MD049/emphasis-style : Emphasis style : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md049.md
MD049:
# Emphasis style
style: "consistent"
# MD050/strong-style : Strong style : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md050.md
MD050:
# Strong style
style: "consistent"
# MD051/link-fragments : Link fragments should be valid : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md051.md
MD051: true
# MD052/reference-links-images : Reference links and images should use a label that is defined : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md052.md
MD052:
# Include shortcut syntax
shortcut_syntax: false
# MD053/link-image-reference-definitions : Link and image reference definitions should be needed : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md053.md
MD053:
# Ignored definitions
ignored_definitions:
- "//"
# MD054/link-image-style : Link and image style : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md054.md
MD054:
# Allow autolinks
autolink: true
# Allow inline links and images
inline: true
# Allow full reference links and images
full: true
# Allow collapsed reference links and images
collapsed: true
# Allow shortcut reference links and images
shortcut: true
# Allow URLs as inline links
url_inline: true
# MD055/table-pipe-style : Table pipe style : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md055.md
MD055:
# Table pipe style
style: "consistent"
# MD056/table-column-count : Table column count : https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md056.md
MD056: true

25
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,25 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.3.2
hooks:
# Run the linter.
- id: ruff
args: [ --fix ]
# Run the formatter.
- id: ruff-format
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.39.0
hooks:
- id: markdownlint
args: ["-f"]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- id: check-merge-conflict
- id: mixed-line-ending

View File

@ -1,76 +1,101 @@
# Change log # Change log
### 0.4.0 ## 0.4.1
- Fixed crash when missing cli args ([#48](https://github.com/xtream1101/humblebundle-downloader/pull/48))
- Updated the Trove url ([#59](https://github.com/xtream1101/humblebundle-downloader/pull/59))
- Using [pre-commit](https://pre-commit.com/) hooks for formatting and linting
- Moved from setuptools to poetry for packaging
## 0.4.0
- Deprecate the `download` argument. It is no longer needed since that is the only action that can be taken - Deprecate the `download` argument. It is no longer needed since that is the only action that can be taken
### 0.3.4 ## 0.3.4
- Merged in [PR 35](https://github.com/xtream1101/humblebundle-downloader/pull/35) to fix some trove games not downloading - Merged in [PR 35](https://github.com/xtream1101/humblebundle-downloader/pull/35) to fix some trove games not downloading
### 0.3.3 ## 0.3.3
- Fixed crashing when file is missing on humblebundle - Fixed crashing when file is missing on humblebundle
- Updated cookie info in readme - Updated cookie info in readme
- Supports passing in the cookie value of `_simpleauth_sess` by using `--session-auth` - Supports passing in the cookie value of `_simpleauth_sess` by using `--session-auth`
### 0.3.1 ## 0.3.1
- Added support for netscape cookies - Added support for netscape cookies
### 0.3.0 ## 0.3.0
- pip install now requires python version 3.4+ - pip install now requires python version 3.4+
- `--trove` will only download trove products, nothing else - `--trove` will only download trove products, nothing else
- Filtering flags now work when downloading trove content - Filtering flags now work when downloading trove content
### 0.2.2 ## 0.2.2
- Confirm the download is complete by checking the expected size to what downloaded - Confirm the download is complete by checking the expected size to what downloaded
- Fixed the platform filter - Fixed the platform filter
### 0.2.1 ## 0.2.1
- Fixed include & exclude logic being switched in v0.2.0 - Fixed include & exclude logic being switched in v0.2.0
### 0.2.0 ## 0.2.0
- Added **Humble Trove** support _(`--trove` to also check/download trove content)_ - Added **Humble Trove** support _(`--trove` to also check/download trove content)_
- Now by default only new content is downloaded. Use `--update` to also check for updated content - Now by default only new content is downloaded. Use `--update` to also check for updated content
### 0.1.3 ## 0.1.3
- Fixed re-downloading for real this time - Fixed re-downloading for real this time
- Only use the url last modified time as the check for new versions - Only use the url last modified time as the check for new versions
### 0.1.2 ## 0.1.2
- Stop using md5 & sha1 hashes to check if file is unique (was creating duplicate downloads of the same file) - Stop using md5 & sha1 hashes to check if file is unique (was creating duplicate downloads of the same file)
- Strip periods from end of directory & file names - Strip periods from end of directory & file names
- Rename older versions of a file before download the new one - Rename older versions of a file before download the new one
### 0.1.1 ## 0.1.1
- Delete failed downloaded files - Delete failed downloaded files
### 0.1.0 ## 0.1.0
- Filename saved is now the original name of the file - Filename saved is now the original name of the file
- key used in cache is different due to changing the file name (this may result in duplicate downloads if you have run the older version) - key used in cache is different due to changing the file name
- Support for downloading a single Bundle/Purchase by using the flag `-k` or `--key` and getting the key from the url of a purchase - _this may result in duplicate downloads if you have run the older version_
- Support for downloading a single Bundle/Purchase by using the
flag `-k` or `--key` and getting the key from the url of a purchase
### 0.0.8 ## 0.0.8
- gen-cookies now works with SSO feature and 2FA logins - gen-cookies now works with SSO feature and 2FA logins
- Added `--include` & `--exclude` cli args to filter file types - Added `--include` & `--exclude` cli args to filter file types
### 0.0.7 ## 0.0.7
- Replace `:` with ` -` in filenames
- Replace `:` with `-` in filenames
- Ignore items that do not have a web url - Ignore items that do not have a web url
### 0.0.6 ## 0.0.6
- Started change log - Started change log
- Added more detail to readme - Added more detail to readme
- Removed the use of f-strings to support more python versions - Removed the use of f-strings to support more python versions

View File

@ -1,13 +1,16 @@
# Humble Bundle Downloader # Humble Bundle Downloader
[![PyPI](https://img.shields.io/pypi/v/humblebundle-downloader.svg)](https://pypi.python.org/pypi/humblebundle-downloader) [![PyPI](https://img.shields.io/pypi/v/humblebundle-downloader.svg)](https://pypi.python.org/pypi/humblebundle-downloader)
[![PyPI](https://img.shields.io/pypi/l/humblebundle-downloader.svg)](https://pypi.python.org/pypi/humblebundle-downloader) [![PyPI](https://img.shields.io/pypi/l/humblebundle-downloader.svg)](https://pypi.python.org/pypi/humblebundle-downloader)
**Download all of your content from your Humble Bundle Library!** **Download all of your content from your Humble Bundle Library!**
The first time this runs it may take a while because it will download everything. After that it will only download the content that has been updated or is missing. The first time this runs it may take a while because it will download everything.
After that it will only download the content that has been updated or is missing.
## Features ## Features
- support for Humble Trove _(`--trove` flag)_ - support for Humble Trove _(`--trove` flag)_
- downloads new and updated content from your Humble Bundle Library on each run _(only check for updates if using `--update`)_ - downloads new and updated content from your Humble Bundle Library on each run _(only check for updates if using `--update`)_
- cli command for easy use (downloading will also work on a headless system) - cli command for easy use (downloading will also work on a headless system)
@ -18,23 +21,29 @@ The first time this runs it may take a while because it will download everything
## Install ## Install
`pip install humblebundle-downloader` `pip install humblebundle-downloader`
## Instructions ## Instructions
### 1. Getting cookies ### 1. Getting cookies
First thing to do is get your account cookies. This can be done by getting a browser extension that lets you see or export your cookies.
First thing to do is get your account cookies.
This can be done by getting a browser extension that lets you see or export your cookies.
- **Method 1 (recommended)** - **Method 1 (recommended)**
- Get the value of the cookie called `_simpleauth_sess` and pass that value using `-s "COOKIE_VALUE"` - Get the value of the cookie called `_simpleauth_sess` and pass that value using `-s "COOKIE_VALUE"`
- **Method 2** - **Method 2**
- Export the cookies in the Netscape format using an extension. - Export the cookies in the Netscape format using an extension.
If your exported cookie file is not working, it may be a formatting issue, this can be fixed by running the command `curl -b cookies.orig.txt --cookie-jar cookies.txt http://bogus` If your exported cookie file is not working, it may be a formatting issue.
This can be fixed by running the command `curl -b cookies.orig.txt --cookie-jar cookies.txt http://bogus`
### 2. Downloading your library ### 2. Downloading your library
Use the following command to download your Humble Bundle Library: Use the following command to download your Humble Bundle Library:
`hbd --cookie-file cookies.txt --library-path "Downloaded Library" --progress` `hbd --cookie-file cookies.txt --library-path "Downloaded Library" --progress`
@ -43,7 +52,12 @@ This directory structure will be used:
## Notes ## Notes
* Inside your library folder a file named `.cache.json` is saved and keeps track of the files that have been downloaded. This way running the download command again pointing to the same directory will only download new or updated files.
* Use `--help` with all `hbd` commands to see available options - Inside your library folder a file named `.cache.json` is saved and keeps track of the files that have been downloaded.
* Find supported platforms for the `--platform` flag by visiting your Humble Bundle Library and look under the **Platform** dropdown This way running the download command again pointing to the same directory will only download new or updated files.
* Download select bundles by using the `-k` or `--keys` flag. Find these keys by going to your *Purchases* section, click on a products and there should be a `downloads?key=XXXX` in the url. - Use `--help` with all `hbd` commands to see available options
- Find supported platforms for the `--platform` flag by visiting your Humble Bundle Library
and look under the **Platform** dropdown
- Download select bundles by using the `-k` or `--keys` flag.
Find these keys by going to your _Purchases_ section,
click on a products and there should be a `downloads?key=XXXX` in the url.

View File

@ -1 +0,0 @@
__version__ = '0.4.0'

View File

@ -5,17 +5,17 @@ import argparse
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
LOG_LEVEL = os.environ.get('HBD_LOGLEVEL', 'INFO').upper() LOG_LEVEL = os.environ.get("HBD_LOGLEVEL", "INFO").upper()
logging.basicConfig( logging.basicConfig(
level=LOG_LEVEL, level=LOG_LEVEL,
format='%(message)s', format="%(message)s",
) )
# Ignore unwanted logs from the requests lib when debuging # Ignore unwanted logs from the requests lib when debuging
logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING) logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
def parse_args(args): def parse_args(args):
if len(args) > 0 and args[0].lower() == 'download': if len(args) > 0 and args[0].lower() == "download":
args = args[1:] args = args[1:]
raise DeprecationWarning("`download` argument is no longer used") raise DeprecationWarning("`download` argument is no longer used")
@ -23,55 +23,75 @@ def parse_args(args):
cookie = parser.add_mutually_exclusive_group(required=True) cookie = parser.add_mutually_exclusive_group(required=True)
cookie.add_argument( cookie.add_argument(
'-c', '--cookie-file', type=str, "-c",
"--cookie-file",
type=str,
help="Location of the cookies file", help="Location of the cookies file",
) )
cookie.add_argument( cookie.add_argument(
'-s', '--session-auth', type=str, "-s",
"--session-auth",
type=str,
help="Value of the cookie _simpleauth_sess. WRAP IN QUOTES", help="Value of the cookie _simpleauth_sess. WRAP IN QUOTES",
) )
parser.add_argument( parser.add_argument(
'-l', '--library-path', type=str, "-l",
"--library-path",
type=str,
help="Folder to download all content to", help="Folder to download all content to",
required=True, required=True,
) )
parser.add_argument( parser.add_argument(
'-t', '--trove', action='store_true', "-t",
"--trove",
action="store_true",
help="Only check and download Humble Trove content", help="Only check and download Humble Trove content",
) )
parser.add_argument( parser.add_argument(
'-u', '--update', action='store_true', "-u",
help=("Check to see if products have been updated " "--update",
"(still get new products)"), action="store_true",
help=("Check to see if products have been updated " "(still get new products)"),
) )
parser.add_argument( parser.add_argument(
'-p', '--platform', "-p",
type=str, nargs='*', "--platform",
help=("Only get content in a platform. Values can be seen in your " type=str,
"humble bundle's library dropdown. Ex: -p ebook video"), nargs="*",
help=(
"Only get content in a platform. Values can be seen in your "
"humble bundle's library dropdown. Ex: -p ebook video"
),
) )
parser.add_argument( parser.add_argument(
'--progress', "--progress",
action='store_true', action="store_true",
help="Display progress bar for downloads", help="Display progress bar for downloads",
) )
filter_ext = parser.add_mutually_exclusive_group() filter_ext = parser.add_mutually_exclusive_group()
filter_ext.add_argument( filter_ext.add_argument(
'-e', '--exclude', "-e",
type=str, nargs='*', "--exclude",
help=("File extensions to ignore when downloading files. " type=str,
"Ex: -e pdf mobi"), nargs="*",
help=("File extensions to ignore when downloading files. " "Ex: -e pdf mobi"),
) )
filter_ext.add_argument( filter_ext.add_argument(
'-i', '--include', "-i",
type=str, nargs='*', "--include",
type=str,
nargs="*",
help="Only download files with these extensions. Ex: -i pdf mobi", help="Only download files with these extensions. Ex: -i pdf mobi",
) )
parser.add_argument( parser.add_argument(
'-k', '--keys', "-k",
type=str, nargs='*', "--keys",
help=("The purchase download key. Find in the url on the " type=str,
"products/bundle download page. Can set multiple"), nargs="*",
help=(
"The purchase download key. Find in the url on the "
"products/bundle download page. Can set multiple"
),
) )
return parser.parse_args(args) return parser.parse_args(args)
@ -81,6 +101,7 @@ def cli():
cli_args = parse_args(sys.argv[1:]) cli_args = parse_args(sys.argv[1:])
from .download_library import DownloadLibrary from .download_library import DownloadLibrary
DownloadLibrary( DownloadLibrary(
cli_args.library_path, cli_args.library_path,
cookie_path=cli_args.cookie_file, cookie_path=cli_args.cookie_file,

View File

@ -12,27 +12,39 @@ logger = logging.getLogger(__name__)
def _clean_name(dirty_str): def _clean_name(dirty_str):
allowed_chars = (' ', '_', '.', '-', '[', ']') allowed_chars = (" ", "_", ".", "-", "[", "]")
clean = [] clean = []
for c in dirty_str.replace('+', '_').replace(':', ' -'): for c in dirty_str.replace("+", "_").replace(":", " -"):
if c.isalpha() or c.isdigit() or c in allowed_chars: if c.isalpha() or c.isdigit() or c in allowed_chars:
clean.append(c) clean.append(c)
return "".join(clean).strip().rstrip('.') return "".join(clean).strip().rstrip(".")
class DownloadLibrary: class DownloadLibrary:
def __init__(
def __init__(self, library_path, cookie_path=None, cookie_auth=None, self,
progress_bar=False, ext_include=None, ext_exclude=None, library_path,
platform_include=None, purchase_keys=None, trove=False, cookie_path=None,
update=False): cookie_auth=None,
progress_bar=False,
ext_include=None,
ext_exclude=None,
platform_include=None,
purchase_keys=None,
trove=False,
update=False,
):
self.library_path = library_path self.library_path = library_path
self.progress_bar = progress_bar self.progress_bar = progress_bar
self.ext_include = [] if ext_include is None else list(map(str.lower, ext_include)) # noqa: E501 self.ext_include = (
self.ext_exclude = [] if ext_exclude is None else list(map(str.lower, ext_exclude)) # noqa: E501 [] if ext_include is None else list(map(str.lower, ext_include))
)
self.ext_exclude = (
[] if ext_exclude is None else list(map(str.lower, ext_exclude))
)
if platform_include is None or 'all' in platform_include: if platform_include is None or "all" in platform_include:
# if 'all', then do not need to use this check # if 'all', then do not need to use this check
platform_include = [] platform_include = []
self.platform_include = list(map(str.lower, platform_include)) self.platform_include = list(map(str.lower, platform_include))
@ -49,23 +61,24 @@ class DownloadLibrary:
self.session.cookies = cookie_jar self.session.cookies = cookie_jar
except http.cookiejar.LoadError: except http.cookiejar.LoadError:
# Still support the original cookie method # Still support the original cookie method
with open(cookie_path, 'r') as f: with open(cookie_path, "r") as f:
self.session.headers.update({'cookie': f.read().strip()}) self.session.headers.update({"cookie": f.read().strip()})
elif cookie_auth: elif cookie_auth:
self.session.headers.update( self.session.headers.update(
{'cookie': '_simpleauth_sess={}'.format(cookie_auth)} {"cookie": "_simpleauth_sess={}".format(cookie_auth)}
) )
def start(self): def start(self):
self.cache_file = os.path.join(self.library_path, ".cache.json")
self.cache_file = os.path.join(self.library_path, '.cache.json')
self.cache_data = self._load_cache_data(self.cache_file) self.cache_data = self._load_cache_data(self.cache_file)
self.purchase_keys = self.purchase_keys if self.purchase_keys else self._get_purchase_keys() # noqa: E501 self.purchase_keys = (
self.purchase_keys if self.purchase_keys else self._get_purchase_keys()
)
if self.trove is True: if self.trove is True:
logger.info("Only checking the Humble Trove...") logger.info("Only checking the Humble Trove...")
for product in self._get_trove_products(): for product in self._get_trove_products():
title = _clean_name(product['human-name']) title = _clean_name(product["human-name"])
self._process_trove_product(title, product) self._process_trove_product(title, product)
else: else:
for order_id in self.purchase_keys: for order_id in self.purchase_keys:
@ -74,50 +87,56 @@ class DownloadLibrary:
def _get_trove_download_url(self, machine_name, web_name): def _get_trove_download_url(self, machine_name, web_name):
try: try:
sign_r = self.session.post( sign_r = self.session.post(
'https://www.humblebundle.com/api/v1/user/download/sign', "https://www.humblebundle.com/api/v1/user/download/sign",
data={ data={
'machine_name': machine_name, "machine_name": machine_name,
'filename': web_name, "filename": web_name,
}, },
) )
except Exception: except Exception:
logger.error("Failed to get download url for trove product {title}" logger.error(
.format(title=web_name)) "Failed to get download url for trove product {title}".format(
title=web_name
)
)
return None return None
logger.debug("Signed url response {sign_r}".format(sign_r=sign_r)) logger.debug("Signed url response {sign_r}".format(sign_r=sign_r))
if sign_r.json().get('_errors') == 'Unauthorized': if sign_r.json().get("_errors") == "Unauthorized":
logger.critical("Your account does not have access to the Trove") logger.critical("Your account does not have access to the Trove")
sys.exit() sys.exit()
signed_url = sign_r.json()['signed_url'] signed_url = sign_r.json()["signed_url"]
logger.debug("Signed url {signed_url}".format(signed_url=signed_url)) logger.debug("Signed url {signed_url}".format(signed_url=signed_url))
return signed_url return signed_url
def _process_trove_product(self, title, product): def _process_trove_product(self, title, product):
for platform, download in product['downloads'].items(): for platform, download in product["downloads"].items():
# Sometimes the name has a dir in it # Sometimes the name has a dir in it
# Example is "Broken Sword 5 - the Serpent's Curse" # Example is "Broken Sword 5 - the Serpent's Curse"
# Only the windows file has a dir like # Only the windows file has a dir like
# "revolutionsoftware/BS5_v2.2.1-win32.zip" # "revolutionsoftware/BS5_v2.2.1-win32.zip"
if self._should_download_platform(platform) is False: # noqa: E501 if self._should_download_platform(platform) is False:
logger.info("Skipping {platform} for {product_title}" logger.info(
.format(platform=platform, "Skipping {platform} for {product_title}".format(
product_title=title)) platform=platform, product_title=title
)
)
continue continue
web_name = download['url']['web'].split('/')[-1] web_name = download["url"]["web"].split("/")[-1]
ext = web_name.split('.')[-1] ext = web_name.split(".")[-1]
if self._should_download_file_type(ext) is False: if self._should_download_file_type(ext) is False:
logger.info("Skipping the file {web_name}" logger.info("Skipping the file {web_name}".format(web_name=web_name))
.format(web_name=web_name))
continue continue
cache_file_key = 'trove:{name}'.format(name=web_name) cache_file_key = "trove:{name}".format(name=web_name)
file_info = { file_info = {
'uploaded_at': (download.get('uploaded_at') "uploaded_at": (
or download.get('timestamp') download.get("uploaded_at")
or product.get('date_added', '0')), or download.get("timestamp")
'md5': download.get('md5', 'UNKNOWN_MD5'), or product.get("date_added", "0")
),
"md5": download.get("md5", "UNKNOWN_MD5"),
} }
cache_file_info = self.cache_data.get(cache_file_key, {}) cache_file_info = self.cache_data.get(cache_file_key, {})
@ -125,20 +144,21 @@ class DownloadLibrary:
# Do not care about checking for updates at this time # Do not care about checking for updates at this time
continue continue
if (file_info['uploaded_at'] != cache_file_info.get('uploaded_at') if file_info["uploaded_at"] != cache_file_info.get(
and file_info['md5'] != cache_file_info.get('md5')): "uploaded_at"
product_folder = os.path.join( ) and file_info["md5"] != cache_file_info.get("md5"):
self.library_path, 'Humble Trove', title product_folder = os.path.join(self.library_path, "Humble Trove", title)
)
# Create directory to save the files to # Create directory to save the files to
try: os.makedirs(product_folder) # noqa: E701 try:
except OSError: pass # noqa: E701 os.makedirs(product_folder)
except OSError:
pass
local_filename = os.path.join( local_filename = os.path.join(
product_folder, product_folder,
web_name, web_name,
) )
signed_url = self._get_trove_download_url( signed_url = self._get_trove_download_url(
download['machine_name'], download["machine_name"],
web_name, web_name,
) )
if signed_url is None: if signed_url is None:
@ -148,14 +168,14 @@ class DownloadLibrary:
try: try:
product_r = self.session.get(signed_url, stream=True) product_r = self.session.get(signed_url, stream=True)
except Exception: except Exception:
logger.error("Failed to get trove product {title}" logger.error(
.format(title=web_name)) "Failed to get trove product {title}".format(title=web_name)
)
continue continue
if 'uploaded_at' in cache_file_info: if "uploaded_at" in cache_file_info:
uploaded_at = time.strftime( uploaded_at = time.strftime(
'%Y-%m-%d', "%Y-%m-%d", time.localtime(int(cache_file_info["uploaded_at"]))
time.localtime(int(cache_file_info['uploaded_at']))
) )
else: else:
uploaded_at = None uploaded_at = None
@ -171,10 +191,11 @@ class DownloadLibrary:
def _get_trove_products(self): def _get_trove_products(self):
trove_products = [] trove_products = []
idx = 0 idx = 0
trove_base_url = 'https://www.humblebundle.com/client/catalog?index={idx}' # noqa: E501 trove_base_url = "https://www.humblebundle.com/client/catalog?index={idx}"
while True: while True:
logger.debug("Collecting trove product data from api pg:{idx} ..." logger.debug(
.format(idx=idx)) "Collecting trove product data from api pg:{idx} ...".format(idx=idx)
)
trove_page_url = trove_base_url.format(idx=idx) trove_page_url = trove_base_url.format(idx=idx)
try: try:
trove_r = self.session.get(trove_page_url) trove_r = self.session.get(trove_page_url)
@ -193,72 +214,80 @@ class DownloadLibrary:
return trove_products return trove_products
def _process_order_id(self, order_id): def _process_order_id(self, order_id):
order_url = 'https://www.humblebundle.com/api/v1/order/{order_id}?all_tpkds=true'.format(order_id=order_id) # noqa: E501 order_url = "https://www.humblebundle.com/api/v1/order/{order_id}?all_tpkds=true".format(
order_id=order_id
)
try: try:
order_r = self.session.get( order_r = self.session.get(
order_url, order_url,
headers={ headers={
'content-type': 'application/json', "content-type": "application/json",
'content-encoding': 'gzip', "content-encoding": "gzip",
}, },
) )
except Exception: except Exception:
logger.error("Failed to get order key {order_id}" logger.error("Failed to get order key {order_id}".format(order_id=order_id))
.format(order_id=order_id))
return return
logger.debug("Order request: {order_r}".format(order_r=order_r)) logger.debug("Order request: {order_r}".format(order_r=order_r))
order = order_r.json() order = order_r.json()
bundle_title = _clean_name(order['product']['human_name']) bundle_title = _clean_name(order["product"]["human_name"])
logger.info("Checking bundle: " + str(bundle_title)) logger.info("Checking bundle: " + str(bundle_title))
for product in order['subproducts']: for product in order["subproducts"]:
self._process_product(order_id, bundle_title, product) self._process_product(order_id, bundle_title, product)
def _rename_old_file(self, local_filename, append_str): def _rename_old_file(self, local_filename, append_str):
# Check if older file exists, if so rename # Check if older file exists, if so rename
if os.path.isfile(local_filename) is True: if os.path.isfile(local_filename) is True:
filename_parts = local_filename.rsplit('.', 1) filename_parts = local_filename.rsplit(".", 1)
new_name = "{name}_{append_str}.{ext}"\ new_name = "{name}_{append_str}.{ext}".format(
.format(name=filename_parts[0], name=filename_parts[0], append_str=append_str, ext=filename_parts[1]
append_str=append_str, )
ext=filename_parts[1])
os.rename(local_filename, new_name) os.rename(local_filename, new_name)
logger.info("Renamed older file to {new_name}" logger.info("Renamed older file to {new_name}".format(new_name=new_name))
.format(new_name=new_name))
def _process_product(self, order_id, bundle_title, product): def _process_product(self, order_id, bundle_title, product):
product_title = _clean_name(product['human_name']) product_title = _clean_name(product["human_name"])
# Get all types of download for a product # Get all types of download for a product
for download_type in product['downloads']: for download_type in product["downloads"]:
if self._should_download_platform(download_type['platform']) is False: # noqa: E501 if self._should_download_platform(download_type["platform"]) is False:
logger.info("Skipping {platform} for {product_title}" logger.info(
.format(platform=download_type['platform'], "Skipping {platform} for {product_title}".format(
product_title=product_title)) platform=download_type["platform"], product_title=product_title
)
)
continue continue
product_folder = os.path.join( product_folder = os.path.join(
self.library_path, bundle_title, product_title self.library_path, bundle_title, product_title
) )
# Create directory to save the files to # Create directory to save the files to
try: os.makedirs(product_folder) # noqa: E701 try:
except OSError: pass # noqa: E701 os.makedirs(product_folder)
except OSError:
pass
# Download each file type of a product # Download each file type of a product
for file_type in download_type['download_struct']: for file_type in download_type["download_struct"]:
try: try:
url = file_type['url']['web'] url = file_type["url"]["web"]
except KeyError: except KeyError:
logger.info("No url found: {bundle_title}/{product_title}" logger.info(
.format(bundle_title=bundle_title, "No url found: {bundle_title}/{product_title}".format(
product_title=product_title)) bundle_title=bundle_title, product_title=product_title
)
)
continue continue
url_filename = url.split('?')[0].split('/')[-1] url_filename = url.split("?")[0].split("/")[-1]
cache_file_key = order_id + ':' + url_filename cache_file_key = order_id + ":" + url_filename
ext = url_filename.split('.')[-1] ext = url_filename.split(".")[-1]
if self._should_download_file_type(ext) is False: if self._should_download_file_type(ext) is False:
logger.info("Skipping the file {url_filename}" logger.info(
.format(url_filename=url_filename)) "Skipping the file {url_filename}".format(
url_filename=url_filename
)
)
continue continue
local_filename = os.path.join(product_folder, url_filename) local_filename = os.path.join(product_folder, url_filename)
@ -277,23 +306,30 @@ class DownloadLibrary:
# Check to see if the file still exists # Check to see if the file still exists
if product_r.status_code != 200: if product_r.status_code != 200:
logger.debug( logger.debug(
"File missing for {bundle_title}/{product_title}: {url}" "File missing for {bundle_title}/{product_title}: {url}".format(
.format(bundle_title=bundle_title, bundle_title=bundle_title,
product_title=product_title, product_title=product_title,
url=url)) url=url,
)
)
continue continue
logger.debug("Item request: {product_r}, Url: {url}" logger.debug(
.format(product_r=product_r, url=url)) "Item request: {product_r}, Url: {url}".format(
product_r=product_r, url=url
)
)
file_info = { file_info = {
'url_last_modified': product_r.headers['Last-Modified'], "url_last_modified": product_r.headers["Last-Modified"],
} }
if file_info['url_last_modified'] != cache_file_info.get('url_last_modified'): # noqa: E501 if file_info["url_last_modified"] != cache_file_info.get(
if 'url_last_modified' in cache_file_info: "url_last_modified"
):
if "url_last_modified" in cache_file_info:
last_modified = datetime.datetime.strptime( last_modified = datetime.datetime.strptime(
cache_file_info['url_last_modified'], cache_file_info["url_last_modified"],
'%a, %d %b %Y %H:%M:%S %Z' "%a, %d %b %Y %H:%M:%S %Z",
).strftime('%Y-%m-%d') ).strftime("%Y-%m-%d")
else: else:
last_modified = None last_modified = None
self._process_download( self._process_download(
@ -310,14 +346,17 @@ class DownloadLibrary:
# quits it can keep track of the progress # quits it can keep track of the progress
# Note: Only safe because of single thread, # Note: Only safe because of single thread,
# need to change if refactor to multi threading # need to change if refactor to multi threading
with open(self.cache_file, 'w') as outfile: with open(self.cache_file, "w") as outfile:
json.dump( json.dump(
self.cache_data, outfile, self.cache_data,
sort_keys=True, indent=4, outfile,
sort_keys=True,
indent=4,
) )
def _process_download(self, open_r, cache_file_key, file_info, def _process_download(
local_filename, rename_str=None): self, open_r, cache_file_key, file_info, local_filename, rename_str=None
):
try: try:
if rename_str: if rename_str:
self._rename_old_file(local_filename, rename_str) self._rename_old_file(local_filename, rename_str)
@ -328,14 +367,19 @@ class DownloadLibrary:
if self.progress_bar: if self.progress_bar:
# Do not overwrite the progress bar on next print # Do not overwrite the progress bar on next print
print() print()
logger.error("Failed to download file {local_filename}" logger.error(
.format(local_filename=local_filename)) "Failed to download file {local_filename}".format(
local_filename=local_filename
)
)
# Clean up broken downloaded file # Clean up broken downloaded file
try: os.remove(local_filename) # noqa: E701 try:
except OSError: pass # noqa: E701 os.remove(local_filename)
except OSError:
pass
if type(e).__name__ == 'KeyboardInterrupt': if type(e).__name__ == "KeyboardInterrupt":
sys.exit() sys.exit()
else: else:
@ -349,11 +393,12 @@ class DownloadLibrary:
open_r.connection.close() open_r.connection.close()
def _download_file(self, product_r, local_filename): def _download_file(self, product_r, local_filename):
logger.info("Downloading: {local_filename}" logger.info(
.format(local_filename=local_filename)) "Downloading: {local_filename}".format(local_filename=local_filename)
)
with open(local_filename, 'wb') as outfile: with open(local_filename, "wb") as outfile:
total_length = product_r.headers.get('content-length') total_length = product_r.headers.get("content-length")
if total_length is None: # no content length header if total_length is None: # no content length header
outfile.write(product_r.content) outfile.write(product_r.content)
else: else:
@ -365,18 +410,21 @@ class DownloadLibrary:
pb_width = 50 pb_width = 50
done = int(pb_width * dl / total_length) done = int(pb_width * dl / total_length)
if self.progress_bar: if self.progress_bar:
print("\t{percent}% [{filler}{space}]" print(
.format(percent=int(done * (100 / pb_width)), "\t{percent}% [{filler}{space}]".format(
filler='=' * done, percent=int(done * (100 / pb_width)),
space=' ' * (pb_width - done), filler="=" * done,
), end='\r') space=" " * (pb_width - done),
),
end="\r",
)
if dl != total_length: if dl != total_length:
raise ValueError("Download did not complete") raise ValueError("Download did not complete")
def _load_cache_data(self, cache_file): def _load_cache_data(self, cache_file):
try: try:
with open(cache_file, 'r') as f: with open(cache_file, "r") as f:
cache_data = json.load(f) cache_data = json.load(f)
except FileNotFoundError: except FileNotFoundError:
cache_data = {} cache_data = {}
@ -385,18 +433,20 @@ class DownloadLibrary:
def _get_purchase_keys(self): def _get_purchase_keys(self):
try: try:
library_r = self.session.get('https://www.humblebundle.com/home/library') # noqa: E501 library_r = self.session.get("https://www.humblebundle.com/home/library")
except Exception: except Exception:
logger.exception("Failed to get list of purchases") logger.exception("Failed to get list of purchases")
return [] return []
logger.debug("Library request: " + str(library_r)) logger.debug("Library request: " + str(library_r))
library_page = parsel.Selector(text=library_r.text) library_page = parsel.Selector(text=library_r.text)
user_data = library_page.css('#user-home-json-data').xpath('string()').extract_first() # noqa: E501 user_data = (
library_page.css("#user-home-json-data").xpath("string()").extract_first()
)
if user_data is None: if user_data is None:
raise Exception("Unable to download user-data, cookies missing?") raise Exception("Unable to download user-data, cookies missing?")
orders_json = json.loads(user_data) orders_json = json.loads(user_data)
return orders_json['gamekeys'] return orders_json["gamekeys"]
def _should_download_platform(self, platform): def _should_download_platform(self, platform):
platform = platform.lower() platform = platform.lower()

469
poetry.lock generated Normal file
View File

@ -0,0 +1,469 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "certifi"
version = "2024.7.4"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
]
[[package]]
name = "charset-normalizer"
version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
files = [
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
{file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
{file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
{file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
{file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
{file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
{file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
{file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
{file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
{file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
{file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
]
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "cssselect"
version = "1.2.0"
description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0"
optional = false
python-versions = ">=3.7"
files = [
{file = "cssselect-1.2.0-py2.py3-none-any.whl", hash = "sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e"},
{file = "cssselect-1.2.0.tar.gz", hash = "sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc"},
]
[[package]]
name = "exceptiongroup"
version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
]
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "idna"
version = "3.7"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
files = [
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
]
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "jmespath"
version = "1.0.1"
description = "JSON Matching Expressions"
optional = false
python-versions = ">=3.7"
files = [
{file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"},
{file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"},
]
[[package]]
name = "lxml"
version = "5.2.2"
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
optional = false
python-versions = ">=3.6"
files = [
{file = "lxml-5.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:364d03207f3e603922d0d3932ef363d55bbf48e3647395765f9bfcbdf6d23632"},
{file = "lxml-5.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50127c186f191b8917ea2fb8b206fbebe87fd414a6084d15568c27d0a21d60db"},
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74e4f025ef3db1c6da4460dd27c118d8cd136d0391da4e387a15e48e5c975147"},
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:981a06a3076997adf7c743dcd0d7a0415582661e2517c7d961493572e909aa1d"},
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aef5474d913d3b05e613906ba4090433c515e13ea49c837aca18bde190853dff"},
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e275ea572389e41e8b039ac076a46cb87ee6b8542df3fff26f5baab43713bca"},
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5b65529bb2f21ac7861a0e94fdbf5dc0daab41497d18223b46ee8515e5ad297"},
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bcc98f911f10278d1daf14b87d65325851a1d29153caaf146877ec37031d5f36"},
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:b47633251727c8fe279f34025844b3b3a3e40cd1b198356d003aa146258d13a2"},
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:fbc9d316552f9ef7bba39f4edfad4a734d3d6f93341232a9dddadec4f15d425f"},
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:13e69be35391ce72712184f69000cda04fc89689429179bc4c0ae5f0b7a8c21b"},
{file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3b6a30a9ab040b3f545b697cb3adbf3696c05a3a68aad172e3fd7ca73ab3c835"},
{file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a233bb68625a85126ac9f1fc66d24337d6e8a0f9207b688eec2e7c880f012ec0"},
{file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:dfa7c241073d8f2b8e8dbc7803c434f57dbb83ae2a3d7892dd068d99e96efe2c"},
{file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a7aca7964ac4bb07680d5c9d63b9d7028cace3e2d43175cb50bba8c5ad33316"},
{file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ae4073a60ab98529ab8a72ebf429f2a8cc612619a8c04e08bed27450d52103c0"},
{file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ffb2be176fed4457e445fe540617f0252a72a8bc56208fd65a690fdb1f57660b"},
{file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e290d79a4107d7d794634ce3e985b9ae4f920380a813717adf61804904dc4393"},
{file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96e85aa09274955bb6bd483eaf5b12abadade01010478154b0ec70284c1b1526"},
{file = "lxml-5.2.2-cp310-cp310-win32.whl", hash = "sha256:f956196ef61369f1685d14dad80611488d8dc1ef00be57c0c5a03064005b0f30"},
{file = "lxml-5.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:875a3f90d7eb5c5d77e529080d95140eacb3c6d13ad5b616ee8095447b1d22e7"},
{file = "lxml-5.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45f9494613160d0405682f9eee781c7e6d1bf45f819654eb249f8f46a2c22545"},
{file = "lxml-5.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0b3f2df149efb242cee2ffdeb6674b7f30d23c9a7af26595099afaf46ef4e88"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d28cb356f119a437cc58a13f8135ab8a4c8ece18159eb9194b0d269ec4e28083"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:657a972f46bbefdbba2d4f14413c0d079f9ae243bd68193cb5061b9732fa54c1"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b9ea10063efb77a965a8d5f4182806fbf59ed068b3c3fd6f30d2ac7bee734"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07542787f86112d46d07d4f3c4e7c760282011b354d012dc4141cc12a68cef5f"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:303f540ad2dddd35b92415b74b900c749ec2010e703ab3bfd6660979d01fd4ed"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2eb2227ce1ff998faf0cd7fe85bbf086aa41dfc5af3b1d80867ecfe75fb68df3"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:56793b7a1a091a7c286b5f4aa1fe4ae5d1446fe742d00cdf2ffb1077865db10d"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb00b549b13bd6d884c863554566095bf6fa9c3cecb2e7b399c4bc7904cb33b5"},
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab"},
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:8cf85a6e40ff1f37fe0f25719aadf443686b1ac7652593dc53c7ef9b8492b115"},
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:d237ba6664b8e60fd90b8549a149a74fcc675272e0e95539a00522e4ca688b04"},
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0b3f5016e00ae7630a4b83d0868fca1e3d494c78a75b1c7252606a3a1c5fc2ad"},
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23441e2b5339bc54dc949e9e675fa35efe858108404ef9aa92f0456929ef6fe8"},
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb0ba3e8566548d6c8e7dd82a8229ff47bd8fb8c2da237607ac8e5a1b8312e5"},
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa"},
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6dcc3d17eac1df7859ae01202e9bb11ffa8c98949dcbeb1069c8b9a75917e01b"},
{file = "lxml-5.2.2-cp311-cp311-win32.whl", hash = "sha256:4c30a2f83677876465f44c018830f608fa3c6a8a466eb223535035fbc16f3438"},
{file = "lxml-5.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:49095a38eb333aaf44c06052fd2ec3b8f23e19747ca7ec6f6c954ffea6dbf7be"},
{file = "lxml-5.2.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7429e7faa1a60cad26ae4227f4dd0459efde239e494c7312624ce228e04f6391"},
{file = "lxml-5.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:50ccb5d355961c0f12f6cf24b7187dbabd5433f29e15147a67995474f27d1776"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc911208b18842a3a57266d8e51fc3cfaccee90a5351b92079beed912a7914c2"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33ce9e786753743159799fdf8e92a5da351158c4bfb6f2db0bf31e7892a1feb5"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec87c44f619380878bd49ca109669c9f221d9ae6883a5bcb3616785fa8f94c97"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08ea0f606808354eb8f2dfaac095963cb25d9d28e27edcc375d7b30ab01abbf6"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75a9632f1d4f698b2e6e2e1ada40e71f369b15d69baddb8968dcc8e683839b18"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74da9f97daec6928567b48c90ea2c82a106b2d500f397eeb8941e47d30b1ca85"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:0969e92af09c5687d769731e3f39ed62427cc72176cebb54b7a9d52cc4fa3b73"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:9164361769b6ca7769079f4d426a41df6164879f7f3568be9086e15baca61466"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d26a618ae1766279f2660aca0081b2220aca6bd1aa06b2cf73f07383faf48927"},
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab67ed772c584b7ef2379797bf14b82df9aa5f7438c5b9a09624dd834c1c1aaf"},
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3d1e35572a56941b32c239774d7e9ad724074d37f90c7a7d499ab98761bd80cf"},
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:8268cbcd48c5375f46e000adb1390572c98879eb4f77910c6053d25cc3ac2c67"},
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e282aedd63c639c07c3857097fc0e236f984ceb4089a8b284da1c526491e3f3d"},
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfdc2bfe69e9adf0df4915949c22a25b39d175d599bf98e7ddf620a13678585"},
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4aefd911793b5d2d7a921233a54c90329bf3d4a6817dc465f12ffdfe4fc7b8fe"},
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8b8df03a9e995b6211dafa63b32f9d405881518ff1ddd775db4e7b98fb545e1c"},
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f11ae142f3a322d44513de1018b50f474f8f736bc3cd91d969f464b5bfef8836"},
{file = "lxml-5.2.2-cp312-cp312-win32.whl", hash = "sha256:16a8326e51fcdffc886294c1e70b11ddccec836516a343f9ed0f82aac043c24a"},
{file = "lxml-5.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:bbc4b80af581e18568ff07f6395c02114d05f4865c2812a1f02f2eaecf0bfd48"},
{file = "lxml-5.2.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e3d9d13603410b72787579769469af730c38f2f25505573a5888a94b62b920f8"},
{file = "lxml-5.2.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38b67afb0a06b8575948641c1d6d68e41b83a3abeae2ca9eed2ac59892b36706"},
{file = "lxml-5.2.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c689d0d5381f56de7bd6966a4541bff6e08bf8d3871bbd89a0c6ab18aa699573"},
{file = "lxml-5.2.2-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:cf2a978c795b54c539f47964ec05e35c05bd045db5ca1e8366988c7f2fe6b3ce"},
{file = "lxml-5.2.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:739e36ef7412b2bd940f75b278749106e6d025e40027c0b94a17ef7968d55d56"},
{file = "lxml-5.2.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d8bbcd21769594dbba9c37d3c819e2d5847656ca99c747ddb31ac1701d0c0ed9"},
{file = "lxml-5.2.2-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:2304d3c93f2258ccf2cf7a6ba8c761d76ef84948d87bf9664e14d203da2cd264"},
{file = "lxml-5.2.2-cp36-cp36m-win32.whl", hash = "sha256:02437fb7308386867c8b7b0e5bc4cd4b04548b1c5d089ffb8e7b31009b961dc3"},
{file = "lxml-5.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196"},
{file = "lxml-5.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b590b39ef90c6b22ec0be925b211298e810b4856909c8ca60d27ffbca6c12e6"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:c2faf60c583af0d135e853c86ac2735ce178f0e338a3c7f9ae8f622fd2eb788c"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7ff762670cada8e05b32bf1e4dc50b140790909caa8303cfddc4d702b71ea184"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:a6d2092797b388342c1bc932077ad232f914351932353e2e8706851c870bca1f"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61"},
{file = "lxml-5.2.2-cp37-cp37m-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"},
{file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"},
{file = "lxml-5.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7ed07b3062b055d7a7f9d6557a251cc655eed0b3152b76de619516621c56f5d3"},
{file = "lxml-5.2.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60fdd125d85bf9c279ffb8e94c78c51b3b6a37711464e1f5f31078b45002421"},
{file = "lxml-5.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a7e24cb69ee5f32e003f50e016d5fde438010c1022c96738b04fc2423e61706"},
{file = "lxml-5.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23cfafd56887eaed93d07bc4547abd5e09d837a002b791e9767765492a75883f"},
{file = "lxml-5.2.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:19b4e485cd07b7d83e3fe3b72132e7df70bfac22b14fe4bf7a23822c3a35bff5"},
{file = "lxml-5.2.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7ce7ad8abebe737ad6143d9d3bf94b88b93365ea30a5b81f6877ec9c0dee0a48"},
{file = "lxml-5.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e49b052b768bb74f58c7dda4e0bdf7b79d43a9204ca584ffe1fb48a6f3c84c66"},
{file = "lxml-5.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d14a0d029a4e176795cef99c056d58067c06195e0c7e2dbb293bf95c08f772a3"},
{file = "lxml-5.2.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:be49ad33819d7dcc28a309b86d4ed98e1a65f3075c6acd3cd4fe32103235222b"},
{file = "lxml-5.2.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a6d17e0370d2516d5bb9062c7b4cb731cff921fc875644c3d751ad857ba9c5b1"},
{file = "lxml-5.2.2-cp38-cp38-win32.whl", hash = "sha256:5b8c041b6265e08eac8a724b74b655404070b636a8dd6d7a13c3adc07882ef30"},
{file = "lxml-5.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:f61efaf4bed1cc0860e567d2ecb2363974d414f7f1f124b1df368bbf183453a6"},
{file = "lxml-5.2.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fb91819461b1b56d06fa4bcf86617fac795f6a99d12239fb0c68dbeba41a0a30"},
{file = "lxml-5.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d4ed0c7cbecde7194cd3228c044e86bf73e30a23505af852857c09c24e77ec5d"},
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54401c77a63cc7d6dc4b4e173bb484f28a5607f3df71484709fe037c92d4f0ed"},
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:625e3ef310e7fa3a761d48ca7ea1f9d8718a32b1542e727d584d82f4453d5eeb"},
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:519895c99c815a1a24a926d5b60627ce5ea48e9f639a5cd328bda0515ea0f10c"},
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7079d5eb1c1315a858bbf180000757db8ad904a89476653232db835c3114001"},
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:343ab62e9ca78094f2306aefed67dcfad61c4683f87eee48ff2fd74902447726"},
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:cd9e78285da6c9ba2d5c769628f43ef66d96ac3085e59b10ad4f3707980710d3"},
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:546cf886f6242dff9ec206331209db9c8e1643ae642dea5fdbecae2453cb50fd"},
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:02f6a8eb6512fdc2fd4ca10a49c341c4e109aa6e9448cc4859af5b949622715a"},
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:339ee4a4704bc724757cd5dd9dc8cf4d00980f5d3e6e06d5847c1b594ace68ab"},
{file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0a028b61a2e357ace98b1615fc03f76eb517cc028993964fe08ad514b1e8892d"},
{file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f90e552ecbad426eab352e7b2933091f2be77115bb16f09f78404861c8322981"},
{file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d83e2d94b69bf31ead2fa45f0acdef0757fa0458a129734f59f67f3d2eb7ef32"},
{file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a02d3c48f9bb1e10c7788d92c0c7db6f2002d024ab6e74d6f45ae33e3d0288a3"},
{file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6d68ce8e7b2075390e8ac1e1d3a99e8b6372c694bbe612632606d1d546794207"},
{file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:453d037e09a5176d92ec0fd282e934ed26d806331a8b70ab431a81e2fbabf56d"},
{file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:3b019d4ee84b683342af793b56bb35034bd749e4cbdd3d33f7d1107790f8c472"},
{file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb3942960f0beb9f46e2a71a3aca220d1ca32feb5a398656be934320804c0df9"},
{file = "lxml-5.2.2-cp39-cp39-win32.whl", hash = "sha256:ac6540c9fff6e3813d29d0403ee7a81897f1d8ecc09a8ff84d2eea70ede1cdbf"},
{file = "lxml-5.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:610b5c77428a50269f38a534057444c249976433f40f53e3b47e68349cca1425"},
{file = "lxml-5.2.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b537bd04d7ccd7c6350cdaaaad911f6312cbd61e6e6045542f781c7f8b2e99d2"},
{file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4820c02195d6dfb7b8508ff276752f6b2ff8b64ae5d13ebe02e7667e035000b9"},
{file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a09f6184f17a80897172863a655467da2b11151ec98ba8d7af89f17bf63dae"},
{file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76acba4c66c47d27c8365e7c10b3d8016a7da83d3191d053a58382311a8bf4e1"},
{file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b128092c927eaf485928cec0c28f6b8bead277e28acf56800e972aa2c2abd7a2"},
{file = "lxml-5.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ae791f6bd43305aade8c0e22f816b34f3b72b6c820477aab4d18473a37e8090b"},
{file = "lxml-5.2.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a2f6a1bc2460e643785a2cde17293bd7a8f990884b822f7bca47bee0a82fc66b"},
{file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e8d351ff44c1638cb6e980623d517abd9f580d2e53bfcd18d8941c052a5a009"},
{file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bec4bd9133420c5c52d562469c754f27c5c9e36ee06abc169612c959bd7dbb07"},
{file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:55ce6b6d803890bd3cc89975fca9de1dff39729b43b73cb15ddd933b8bc20484"},
{file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ab6a358d1286498d80fe67bd3d69fcbc7d1359b45b41e74c4a26964ca99c3f8"},
{file = "lxml-5.2.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:06668e39e1f3c065349c51ac27ae430719d7806c026fec462e5693b08b95696b"},
{file = "lxml-5.2.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9cd5323344d8ebb9fb5e96da5de5ad4ebab993bbf51674259dbe9d7a18049525"},
{file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89feb82ca055af0fe797a2323ec9043b26bc371365847dbe83c7fd2e2f181c34"},
{file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e481bba1e11ba585fb06db666bfc23dbe181dbafc7b25776156120bf12e0d5a6"},
{file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d6c6ea6a11ca0ff9cd0390b885984ed31157c168565702959c25e2191674a14"},
{file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3d98de734abee23e61f6b8c2e08a88453ada7d6486dc7cdc82922a03968928db"},
{file = "lxml-5.2.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:69ab77a1373f1e7563e0fb5a29a8440367dec051da6c7405333699d07444f511"},
{file = "lxml-5.2.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:34e17913c431f5ae01d8658dbf792fdc457073dcdfbb31dc0cc6ab256e664a8d"},
{file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05f8757b03208c3f50097761be2dea0aba02e94f0dc7023ed73a7bb14ff11eb0"},
{file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a520b4f9974b0a0a6ed73c2154de57cdfd0c8800f4f15ab2b73238ffed0b36e"},
{file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5e097646944b66207023bc3c634827de858aebc226d5d4d6d16f0b77566ea182"},
{file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b5e4ef22ff25bfd4ede5f8fb30f7b24446345f3e79d9b7455aef2836437bc38a"},
{file = "lxml-5.2.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff69a9a0b4b17d78170c73abe2ab12084bdf1691550c5629ad1fe7849433f324"},
{file = "lxml-5.2.2.tar.gz", hash = "sha256:bb2dc4898180bea79863d5487e5f9c7c34297414bad54bcd0f0852aee9cfdb87"},
]
[package.extras]
cssselect = ["cssselect (>=0.7)"]
html-clean = ["lxml-html-clean"]
html5 = ["html5lib"]
htmlsoup = ["BeautifulSoup4"]
source = ["Cython (>=3.0.10)"]
[[package]]
name = "packaging"
version = "24.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
name = "parsel"
version = "1.9.1"
description = "Parsel is a library to extract data from HTML and XML using XPath and CSS selectors"
optional = false
python-versions = ">=3.8"
files = [
{file = "parsel-1.9.1-py2.py3-none-any.whl", hash = "sha256:c4a777ee6c3ff5e39652b58e351c5cf02c12ff420d05b07a7966aebb68ab1700"},
{file = "parsel-1.9.1.tar.gz", hash = "sha256:14e00dc07731c9030db620c195fcae884b5b4848e9f9c523c6119f708ccfa9ac"},
]
[package.dependencies]
cssselect = ">=1.2.0"
jmespath = "*"
lxml = "*"
packaging = "*"
w3lib = ">=1.19.0"
[[package]]
name = "pluggy"
version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pytest"
version = "8.3.2"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
{file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.5,<2"
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "requests"
version = "2.32.3"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
files = [
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
]
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.7"
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[package]]
name = "urllib3"
version = "2.2.2"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
files = [
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
]
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "w3lib"
version = "2.2.1"
description = "Library of web-related functions"
optional = false
python-versions = ">=3.8"
files = [
{file = "w3lib-2.2.1-py3-none-any.whl", hash = "sha256:e56d81c6a6bf507d7039e0c95745ab80abd24b465eb0f248af81e3eaa46eb510"},
{file = "w3lib-2.2.1.tar.gz", hash = "sha256:756ff2d94c64e41c8d7c0c59fea12a5d0bc55e33a531c7988b4a163deb9b07dd"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "67a578afce6d570192e284e76d3745083c4793ad80dffe69a414addd9fedcdce"

26
pyproject.toml Normal file
View File

@ -0,0 +1,26 @@
[tool.poetry]
name = "humblebundle-downloader"
version = "0.4.1"
description = "Download your Humble Bundle library"
authors = ["Eddy Hintze <eddy@gitx.codes>"]
license = "MIT"
readme = "README.md"
[tool.poetry.urls]
homepage = "https://github.com/xtream1101/humblebundle-downloader"
repository = "https://github.com/xtream1101/humblebundle-downloader"
[tool.poetry.scripts]
hbd = "humblebundle_downloader.cli:cli"
[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.32.3"
parsel = "^1.9.1"
[tool.poetry.group.test.dependencies]
pytest = "^8.3.2"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@ -1,34 +0,0 @@
from setuptools import setup
from humblebundle_downloader._version import __version__
with open('README.md', 'r') as f:
long_description = f.read()
setup(
name='humblebundle-downloader',
packages=['humblebundle_downloader'],
version=__version__,
description='Download your Humble Bundle library',
long_description=long_description,
long_description_content_type='text/markdown',
author='Eddy Hintze',
author_email="eddy@hintze.co",
url="https://github.com/xtream1101/humblebundle-downloader",
license='MIT',
classifiers=[
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
],
entry_points={
'console_scripts': [
'hbd=humblebundle_downloader.cli:cli',
],
},
python_requires='~=3.4',
install_requires=[
'requests',
'parsel',
],
)

View File

@ -4,13 +4,13 @@ from humblebundle_downloader.cli import parse_args
def test_old_action_format(): def test_old_action_format():
with pytest.raises(DeprecationWarning): with pytest.raises(DeprecationWarning):
_ = parse_args(['download', '-l', 'some_path', '-c', 'fake_cookie']) _ = parse_args(["download", "-l", "some_path", "-c", "fake_cookie"])
def test_no_action(): def test_no_action():
args = parse_args(['-l', 'some_path', '-c', 'fake_cookie']) args = parse_args(["-l", "some_path", "-c", "fake_cookie"])
assert args.library_path == 'some_path' assert args.library_path == "some_path"
assert args.cookie_file == 'fake_cookie' assert args.cookie_file == "fake_cookie"
def test_no_args(): def test_no_args():

View File

@ -6,46 +6,46 @@ from humblebundle_downloader.download_library import DownloadLibrary
### ###
def test_include_logic_has_values(): def test_include_logic_has_values():
dl = DownloadLibrary( dl = DownloadLibrary(
'fake_library_path', "fake_library_path",
ext_include=['pdf', 'EPub'], ext_include=["pdf", "EPub"],
) )
assert dl._should_download_file_type('pdf') is True assert dl._should_download_file_type("pdf") is True
assert dl._should_download_file_type('df') is False assert dl._should_download_file_type("df") is False
assert dl._should_download_file_type('ePub') is True assert dl._should_download_file_type("ePub") is True
assert dl._should_download_file_type('mobi') is False assert dl._should_download_file_type("mobi") is False
def test_include_logic_empty(): def test_include_logic_empty():
dl = DownloadLibrary( dl = DownloadLibrary(
'fake_library_path', "fake_library_path",
ext_include=[], ext_include=[],
) )
assert dl._should_download_file_type('pdf') is True assert dl._should_download_file_type("pdf") is True
assert dl._should_download_file_type('df') is True assert dl._should_download_file_type("df") is True
assert dl._should_download_file_type('EPub') is True assert dl._should_download_file_type("EPub") is True
assert dl._should_download_file_type('mobi') is True assert dl._should_download_file_type("mobi") is True
def test_exclude_logic_has_values(): def test_exclude_logic_has_values():
dl = DownloadLibrary( dl = DownloadLibrary(
'fake_library_path', "fake_library_path",
ext_exclude=['pdf', 'EPub'], ext_exclude=["pdf", "EPub"],
) )
assert dl._should_download_file_type('pdf') is False assert dl._should_download_file_type("pdf") is False
assert dl._should_download_file_type('df') is True assert dl._should_download_file_type("df") is True
assert dl._should_download_file_type('ePub') is False assert dl._should_download_file_type("ePub") is False
assert dl._should_download_file_type('mobi') is True assert dl._should_download_file_type("mobi") is True
def test_exclude_logic_empty(): def test_exclude_logic_empty():
dl = DownloadLibrary( dl = DownloadLibrary(
'fake_library_path', "fake_library_path",
ext_exclude=[], ext_exclude=[],
) )
assert dl._should_download_file_type('pdf') is True assert dl._should_download_file_type("pdf") is True
assert dl._should_download_file_type('df') is True assert dl._should_download_file_type("df") is True
assert dl._should_download_file_type('EPub') is True assert dl._should_download_file_type("EPub") is True
assert dl._should_download_file_type('mobi') is True assert dl._should_download_file_type("mobi") is True
### ###
@ -53,26 +53,26 @@ def test_exclude_logic_empty():
### ###
def test_download_platform_filter_none(): def test_download_platform_filter_none():
dl = DownloadLibrary( dl = DownloadLibrary(
'fake_library_path', "fake_library_path",
platform_include=None, platform_include=None,
) )
assert dl._should_download_platform('ebook') is True assert dl._should_download_platform("ebook") is True
assert dl._should_download_platform('audio') is True assert dl._should_download_platform("audio") is True
def test_download_platform_filter_blank(): def test_download_platform_filter_blank():
dl = DownloadLibrary( dl = DownloadLibrary(
'fake_library_path', "fake_library_path",
platform_include=[], platform_include=[],
) )
assert dl._should_download_platform('ebook') is True assert dl._should_download_platform("ebook") is True
assert dl._should_download_platform('audio') is True assert dl._should_download_platform("audio") is True
def test_download_platform_filter_audio(): def test_download_platform_filter_audio():
dl = DownloadLibrary( dl = DownloadLibrary(
'fake_library_path', "fake_library_path",
platform_include=['audio'], platform_include=["audio"],
) )
assert dl._should_download_platform('ebook') is False assert dl._should_download_platform("ebook") is False
assert dl._should_download_platform('audio') is True assert dl._should_download_platform("audio") is True