From 7bad1b87f5a0ef30fc8547bfdea2380cc776192e Mon Sep 17 00:00:00 2001 From: Steve Canny Date: Sun, 22 Sep 2024 21:03:49 -0700 Subject: [PATCH] rfctr: modernize for Python 3 (#11) - Use uv for package management, get rid of setup.py etc. - Move source to `src/` - Add type-annotations - Blacken code (100-character line-length) - Reformat docstrings to PEP257. - Add GitHub Actions CI. - Get docs working. - Update tox config. --- .github/workflows/ci.yml | 40 + .gitignore | 2 +- HISTORY.rst | 6 + Makefile | 60 +- README.rst => README.md | 14 +- docs/conf.py | 122 +- docs/index.rst | 2 +- docs/install.rst | 16 - docs/quickstart.rst | 14 +- ez_setup.py | 263 ---- features/environment.py | 22 +- features/steps/helpers.py | 107 +- features/steps/opc_diag_steps.py | 376 +++--- features/steps/step_data.py | 112 +- opcdiag/__init__.py | 3 - opcdiag/cli.py | 304 ----- opcdiag/model.py | 202 --- opcdiag/phys_pkg.py | 174 --- opcdiag/presenter.py | 276 ---- opcdiag/view.py | 73 -- pyproject.toml | 127 ++ setup.py | 108 -- src/opcdiag/__init__.py | 0 src/opcdiag/cli.py | 291 +++++ {opcdiag => src/opcdiag}/controller.py | 42 +- src/opcdiag/model.py | 205 +++ src/opcdiag/phys_pkg.py | 156 +++ src/opcdiag/presenter.py | 259 ++++ src/opcdiag/py.typed | 0 src/opcdiag/view.py | 61 + tests/test-files/deleteme/foobar_dir | 1 + tests/{test_files => test-files}/mini_pkg.zip | Bin .../{test_files => test-files}/mini_pkg/uri_1 | 0 .../{test_files => test-files}/mini_pkg/uri_2 | 0 tests/test_cli.py | 323 ++--- tests/test_controller.py | 129 +- tests/test_model.py | 197 +-- tests/test_phys_pkg.py | 126 +- tests/test_presenter.py | 598 +++++---- tests/unitutil.py | 94 +- tox.ini | 18 +- typings/behave/__init__.pyi | 17 + typings/behave/runner.pyi | 3 + uv.lock | 1115 +++++++++++++++++ 44 files changed, 3482 insertions(+), 2576 deletions(-) create mode 100644 .github/workflows/ci.yml rename README.rst => README.md (78%) delete mode 100644 ez_setup.py delete mode 100644 opcdiag/__init__.py delete mode 100644 opcdiag/cli.py delete mode 100644 opcdiag/model.py delete mode 100644 opcdiag/phys_pkg.py delete mode 100644 opcdiag/presenter.py delete mode 100644 opcdiag/view.py create mode 100644 pyproject.toml delete mode 100644 setup.py create mode 100644 src/opcdiag/__init__.py create mode 100644 src/opcdiag/cli.py rename {opcdiag => src/opcdiag}/controller.py (70%) create mode 100644 src/opcdiag/model.py create mode 100644 src/opcdiag/phys_pkg.py create mode 100644 src/opcdiag/presenter.py create mode 100644 src/opcdiag/py.typed create mode 100644 src/opcdiag/view.py create mode 100644 tests/test-files/deleteme/foobar_dir rename tests/{test_files => test-files}/mini_pkg.zip (100%) rename tests/{test_files => test-files}/mini_pkg/uri_1 (100%) rename tests/{test_files => test-files}/mini_pkg/uri_2 (100%) create mode 100644 typings/behave/__init__.pyi create mode 100644 typings/behave/runner.pyi create mode 100644 uv.lock diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..49f7e8b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: ci + +on: + pull_request: + branches: + - master + push: + branches: + - master + +permissions: + contents: write + +jobs: + + build: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v2 + with: + version: "0.4.15" + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Install the project + run: uv sync --all-extras --dev + + - name: Test with pytest + behave + run: | + uv run pytest --cov-report term-missing --cov=opcdiag --cov=tests tests + uv run behave diff --git a/.gitignore b/.gitignore index d20c88f..8aa6951 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ /.coverage /dist/ /docs/.build/ -/*.egg-info +/src/*.egg-info *.pyc _scratch/ Session.vim diff --git a/HISTORY.rst b/HISTORY.rst index e06f4c5..d30c2ac 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,12 @@ History ======= +1.1.0 (2024-09-22) +------------------ + +* Modernize and repackage with support for Python 3. + + 1.0.0 (2014-01-14) ------------------ diff --git a/Makefile b/Makefile index 65a5a1e..aa2540a 100644 --- a/Makefile +++ b/Makefile @@ -1,43 +1,53 @@ -BEHAVE = behave PACKAGE = opcdiag -PYTHON = python -SETUP = $(PYTHON) ./setup.py .PHONY: accept clean coverage readme register test sdist upload help: @echo "Please use \`make ' where is one or more of" - @echo " accept run acceptance tests using behave" - @echo " clean delete intermediate work product and start fresh" - @echo " coverage run nosetests with coverage" - @echo " readme update README.html from README.rst" - @echo " register update metadata (README.rst) on PyPI" - @echo " test run tests using setup.py" - @echo " sdist generate a source distribution into dist/" - @echo " upload upload distribution tarball to PyPI" - + @echo " accept run acceptance tests using behave" + @echo " build generate a source distribution and wheel into dist/" + @echo " clean delete intermediate work product and start fresh" + @echo " cleandocs delete generated HTML documentation" + @echo " coverage run unit tests with coverage" + @echo " docs generate HTML documentation with Sphinx" + @echo " test run unit tests" + @echo " test-upload upload distribution artifacts in dist/ to Test-PyPI" + @echo " upload upload distribution artifacts in dist/ to PyPI" + +.PHONY: accept accept: - $(BEHAVE) --stop + uv run behave --stop + +.PHONY: build +build: + rm -rf dist + uv build +.PHONY: clean clean: find . -type f -name \*.pyc -exec rm {} \; rm -rf dist *.egg-info .coverage .DS_Store -coverage: - py.test --cov-report term-missing --cov=$(PACKAGE) tests/ - -readme: - rst2html README.rst >README.html - open README.html +.PHONY: cleandocs +cleandocs: + $(MAKE) -C docs clean -register: - $(SETUP) register +.PHONY: coverage +coverage: + uv run pytest --cov-report term-missing --cov=$(PACKAGE) --cov=tests tests/ -sdist: - $(SETUP) sdist +.PHONY: docs +docs: + $(MAKE) -C docs html +.PHONY: test test: - $(SETUP) test + uv run pytest tests + +.PHONY: test-upload +test-upload: build + uv run twine upload --repository testpypi dist/* +.PHONY: upload upload: - $(SETUP) sdist upload + uv run twine upload dist/* diff --git a/README.rst b/README.md similarity index 78% rename from README.rst rename to README.md index 7827621..17cf804 100644 --- a/README.rst +++ b/README.md @@ -3,7 +3,7 @@ and PowerPoint files from Office 2007 and later. Also known as *Office Open XML*, the structure of these files adheres to the Open Packaging Convention (OPC), specified by ISO/IEC 29500. -*opc-diag* provides the ``opc`` command, which allows OPC files to be browsed, +*opc-diag* provides the `opc` command, which allows OPC files to be browsed, diff-ed, extracted, repackaged, and parts from one to be substituted into another. @@ -13,16 +13,16 @@ manipulates Microsoft Office documents. A typical use would be diff-ing a Word file from before and after an operation, say inserting a paragraph, to identify the specific changes Word made to the XML. This is handy when one is developing software to do the same without -Word's help:: +Word's help: - $ opc diff before.docx after.docx +```bash +$ opc diff before.docx after.docx +``` Another main use is to diagnose an issue causing an Office document to not load cleanly, typically because the software that generated it has a bug. These problems can be tedious and often difficult to diagnose without tools like *opc-diag*, and were the primary motivation for developing it. -More information is available in the `opc-diag documentation`_. - -.. _`opc-diag documentation`: - https://opc-diag.readthedocs.org/en/latest/ +More information is available in the +[opc-diag documentation](https://opc-diag.readthedocs.org/en/latest/) diff --git a/docs/conf.py b/docs/conf.py index 3108aaf..a3100d3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,38 +15,40 @@ import os import sys -sys.path.insert(0, os.path.abspath('..')) +import tomli -from opcdiag import __version__ +sys.path.insert(0, os.path.abspath("..")) # -- General configuration -------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [] # Add any paths that contain templates here, relative to this directory. -templates_path = ['.templates'] +templates_path = [".templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'opc-diag' -copyright = u'2013, Steve Canny' +project = "opc-diag" +copyright = "2013, Steve Canny" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = __version__ +with open("../pyproject.toml", "rb") as f: + toml = tomli.load(f) +version = toml["project"]["version"] # The full version, including alpha/beta/rc tags. release = version @@ -62,69 +64,69 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['.build'] +exclude_patterns = [".build"] # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output ------------------------------------------------ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'armstrong' +html_theme = "armstrong" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['.themes'] +html_theme_path = [".themes"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -133,47 +135,47 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'opc-diagdoc' +htmlhelp_basename = "opc-diagdoc" # -- Options for LaTeX output ----------------------------------------------- @@ -181,10 +183,8 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. #'preamble': '', } @@ -198,42 +198,38 @@ # documentclass [howto/manual] # ). latex_documents = [ - ('index', 'opc-diag.tex', u'opc-diag Documentation', - u'Steve Canny', 'manual'), + ("index", "opc-diag.tex", "opc-diag Documentation", "Steve Canny", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output ----------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'opc-diag', u'opc-diag Documentation', - [u'Steve Canny'], 1) -] +man_pages = [("index", "opc-diag", "opc-diag Documentation", ["Steve Canny"], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output --------------------------------------------- @@ -242,16 +238,22 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'opc-diag', u'opc-diag Documentation', - u'Steve Canny', 'opc-diag', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "opc-diag", + "opc-diag Documentation", + "Steve Canny", + "opc-diag", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff --git a/docs/index.rst b/docs/index.rst index 18f8507..00e4fa4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,7 +4,7 @@ opc-diag Release v\ |version| (:ref:`Installation `) -.. include:: ../README.rst +.. include:: ../README.md User Guide diff --git a/docs/install.rst b/docs/install.rst index f2709fb..b658dce 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -12,22 +12,6 @@ command line:: $ pip install opc-diag -There are, however, some common difficulties: - -|opcd| depends on the ``lxml`` Python package, which cannot reliably be -installed by ``pip`` or ``easy_install`` on Windows. Building it from source -requires a compiler and other items the typical Windows user will not have -installed. Therefore we recommend Windows users manually install |lxml| using -a GUI installer before installing |opcd|. For that, the precompiled binaries at -http://www.lfd.uci.edu/~gohlke/pythonlibs/ have been the best source so far. - -|lxml| depends on the ``libxslt`` and ``libxml2`` libraries. If those are not -present the |lxml| build will fail during the install. Linux users shouldn't -have too much trouble as these libraries are commonly installed by default. If -not, ``yum`` or ``apt-get`` is your friend for getting them installed. OS -X users running recent versions may also find these already installed. If not, -they can be installed using Homebrew. - Getting the Code ---------------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index f395378..d1bf18f 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -162,6 +162,10 @@ into its parts and perhaps to put it back together again later. The ``extract`` subcommand provides the first half of this process, complemented by the ``repackage`` subcommand discussed next. +This is especially useful when you have a pretty good idea how some particular aspect of +the XML schema works but want to experiment with manual updates directly the the XML to +see what product behaviors those produce in, say, Microsoft Word or LibreOffice. + The command: .. code-block:: bash @@ -175,14 +179,8 @@ workbook will be found at ``example_dir/xl/workbook.xml`` and the thumbnail image that may appear in a desktop icon for the file is found at ``example_dir/docProps/thumbnail.jpeg``. -Users on a \*nix operating system can accomplish much the same thing with the -command: - -.. code-block:: bash - - $ unzip example.xlsx -d example_dir - -but I thought it might be handy from time to time to have it built into |opcd|. +Importantly, all the files are formatted for human readability. This is particularly +important when you plan to edit the XML by hand. Use Case 5: ``repackage`` a package directory into a file diff --git a/ez_setup.py b/ez_setup.py deleted file mode 100644 index 671b544..0000000 --- a/ez_setup.py +++ /dev/null @@ -1,263 +0,0 @@ -#!python -"""Bootstrap setuptools installation - -If you want to use setuptools in your package's setup.py, just include this -file in the same directory with it, and add this to the top of your setup.py:: - - from ez_setup import use_setuptools - use_setuptools() - -If you want to require a specific version of setuptools, set a download -mirror, or use an alternate download directory, you can do so by supplying -the appropriate options to ``use_setuptools()``. - -This file can also be run as a script to install or upgrade setuptools. -""" -import os -import shutil -import sys -import tempfile -import tarfile -import optparse -import subprocess - -from distutils import log - -try: - from site import USER_SITE -except ImportError: - USER_SITE = None - -DEFAULT_VERSION = "0.9.6" -DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" - - -def _python_cmd(*args): - args = (sys.executable,) + args - return subprocess.call(args) == 0 - - -def _install(tarball, install_args=()): - # extracting the tarball - tmpdir = tempfile.mkdtemp() - log.warn('Extracting in %s', tmpdir) - old_wd = os.getcwd() - try: - os.chdir(tmpdir) - tar = tarfile.open(tarball) - _extractall(tar) - tar.close() - - # going in the directory - subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) - os.chdir(subdir) - log.warn('Now working in %s', subdir) - - # installing - log.warn('Installing Setuptools') - if not _python_cmd('setup.py', 'install', *install_args): - log.warn('Something went wrong during the installation.') - log.warn('See the error message above.') - # exitcode will be 2 - return 2 - finally: - os.chdir(old_wd) - shutil.rmtree(tmpdir) - - -def _build_egg(egg, tarball, to_dir): - # extracting the tarball - tmpdir = tempfile.mkdtemp() - log.warn('Extracting in %s', tmpdir) - old_wd = os.getcwd() - try: - os.chdir(tmpdir) - tar = tarfile.open(tarball) - _extractall(tar) - tar.close() - - # going in the directory - subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) - os.chdir(subdir) - log.warn('Now working in %s', subdir) - - # building an egg - log.warn('Building a Setuptools egg in %s', to_dir) - _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) - - finally: - os.chdir(old_wd) - shutil.rmtree(tmpdir) - # returning the result - log.warn(egg) - if not os.path.exists(egg): - raise IOError('Could not build the egg.') - - -def _do_download(version, download_base, to_dir, download_delay): - egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg' - % (version, sys.version_info[0], sys.version_info[1])) - if not os.path.exists(egg): - tarball = download_setuptools(version, download_base, - to_dir, download_delay) - _build_egg(egg, tarball, to_dir) - sys.path.insert(0, egg) - import setuptools - setuptools.bootstrap_install_from = egg - - -def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, download_delay=15): - # making sure we use the absolute path - to_dir = os.path.abspath(to_dir) - was_imported = 'pkg_resources' in sys.modules or \ - 'setuptools' in sys.modules - try: - import pkg_resources - except ImportError: - return _do_download(version, download_base, to_dir, download_delay) - try: - pkg_resources.require("setuptools>=" + version) - return - except pkg_resources.VersionConflict: - e = sys.exc_info()[1] - if was_imported: - sys.stderr.write( - "The required version of setuptools (>=%s) is not available," - "\nand can't be installed while this script is running. Plea" - "se\ninstall a more recent version first, using\n'easy_insta" - "ll -U setuptools'.\n\n(Currently using %r)\n" % - (version, e.args[0]) - ) - sys.exit(2) - else: - del pkg_resources, sys.modules['pkg_resources'] # reload ok - return _do_download(version, download_base, to_dir, - download_delay) - except pkg_resources.DistributionNotFound: # noqa - return _do_download(version, download_base, to_dir, - download_delay) - - -def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, delay=15): - """Download setuptools from a specified location and return its filename - - `version` should be a valid setuptools version number that is available - as an egg for download under the `download_base` URL (which should end - with a '/'). `to_dir` is the directory where the egg will be downloaded. - `delay` is the number of seconds to pause before an actual download - attempt. - """ - # making sure we use the absolute path - to_dir = os.path.abspath(to_dir) - try: - from urllib.request import urlopen - except ImportError: - from urllib2 import urlopen - tgz_name = "setuptools-%s.tar.gz" % version - url = download_base + tgz_name - saveto = os.path.join(to_dir, tgz_name) - src = dst = None - if not os.path.exists(saveto): # Avoid repeated downloads - try: - log.warn("Downloading %s", url) - src = urlopen(url) - # Read/write all in one block, so we don't create a corrupt file - # if the download is interrupted. - data = src.read() - dst = open(saveto, "wb") - dst.write(data) - finally: - if src: - src.close() - if dst: - dst.close() - return os.path.realpath(saveto) - - -def _extractall(self, path=".", members=None): - """Extract all members from the archive to the current working - directory and set owner, modification time and permissions on - directories afterwards. `path' specifies a different directory - to extract to. `members' is optional and must be a subset of the - list returned by getmembers(). - """ - import copy - import operator - from tarfile import ExtractError - directories = [] - - if members is None: - members = self - - for tarinfo in members: - if tarinfo.isdir(): - # Extract directories with a safe mode. - directories.append(tarinfo) - tarinfo = copy.copy(tarinfo) - tarinfo.mode = 448 # decimal for oct 0700 - self.extract(tarinfo, path) - - # Reverse sort directories. - if sys.version_info < (2, 4): - def sorter(dir1, dir2): - return cmp(dir1.name, dir2.name) - directories.sort(sorter) - directories.reverse() - else: - directories.sort(key=operator.attrgetter('name'), reverse=True) - - # Set correct owner, mtime and filemode on directories. - for tarinfo in directories: - dirpath = os.path.join(path, tarinfo.name) - try: - self.chown(tarinfo, dirpath) - self.utime(tarinfo, dirpath) - self.chmod(tarinfo, dirpath) - except ExtractError: - e = sys.exc_info()[1] - if self.errorlevel > 1: - raise - else: - self._dbg(1, "tarfile: %s" % e) - - -def _build_install_args(options): - """ - Build the arguments to 'python setup.py install' on the setuptools package - """ - install_args = [] - if options.user_install: - if sys.version_info < (2, 6): - log.warn("--user requires Python 2.6 or later") - raise SystemExit(1) - install_args.append('--user') - return install_args - - -def _parse_args(): - """ - Parse the command line for options - """ - parser = optparse.OptionParser() - parser.add_option( - '--user', dest='user_install', action='store_true', default=False, - help='install in user site package (requires Python 2.6 or later)') - parser.add_option( - '--download-base', dest='download_base', metavar="URL", - default=DEFAULT_URL, - help='alternative URL from where to download the setuptools package') - options, args = parser.parse_args() - # positional arguments are ignored - return options - - -def main(version=DEFAULT_VERSION): - """Install or upgrade setuptools and EasyInstall""" - options = _parse_args() - tarball = download_setuptools(download_base=options.download_base) - return _install(tarball, _build_install_args(options)) - -if __name__ == '__main__': - sys.exit(main()) diff --git a/features/environment.py b/features/environment.py index 2863904..b6ce9b0 100644 --- a/features/environment.py +++ b/features/environment.py @@ -1,24 +1,12 @@ -# -*- coding: utf-8 -*- -# -# environment.py -# -# Copyright (C) 2013 Steve Canny scanny@cisco.com -# -# This module is part of python-opc and is released under the MIT License: -# http://www.opensource.org/licenses/mit-license.php - -""" -Used by behave to set testing environment before and after running acceptance -tests. -""" +"""Used by behave to set testing environment before and after running acceptance tests.""" import os -scratch_dir = os.path.abspath( - os.path.join(os.path.split(__file__)[0], '_scratch') -) +from behave.runner import Context + +scratch_dir = os.path.abspath(os.path.join(os.path.split(__file__)[0], "_scratch")) -def before_all(context): +def before_all(context: Context): if not os.path.isdir(scratch_dir): os.mkdir(scratch_dir) diff --git a/features/steps/helpers.py b/features/steps/helpers.py index ed1c474..f72fa85 100644 --- a/features/steps/helpers.py +++ b/features/steps/helpers.py @@ -1,66 +1,56 @@ -# -*- coding: utf-8 -*- -# -# helpers.py -# -# Copyright (C) 2012, 2013 Steve Canny scanny@cisco.com -# -# This module is part of opc-diag and is released under the MIT License: -# http://www.opensource.org/licenses/mit-license.php - """Acceptance test helpers.""" -from __future__ import unicode_literals +# pyright: reportPrivateUsage=false + +from __future__ import annotations import os import subprocess -from step_data import Manifest +from step_data import Manifest, _Manifest -def absjoin(*paths): +def absjoin(*paths: str): return os.path.abspath(os.path.join(*paths)) -def ref_pkg_path(filename): - ref_pkg_dir = absjoin(test_file_dir, 'reference_pkgs') +def ref_pkg_path(filename: str): + ref_pkg_dir = absjoin(test_file_dir, "reference_pkgs") return os.path.relpath(absjoin(ref_pkg_dir, filename)) -def scratch_path(name): +def scratch_path(name: str): return os.path.relpath(absjoin(scratch_dir, name)) thisdir = os.path.split(__file__)[0] -scratch_dir = absjoin(thisdir, '../_scratch') -test_file_dir = absjoin(thisdir, '../test_files') +scratch_dir = absjoin(thisdir, "../_scratch") +test_file_dir = absjoin(thisdir, "../test_files") -def assertManifestsMatch(manifest1, manifest2, name1, name2): - """ - Raise |AssertionError| if *manifest1* does not exactly match *manifest2*. - *name1* and *name2* appear in the diff printed if the assertion fails. +def assertManifestsMatch(manifest1: _Manifest, manifest2: _Manifest, name1: str, name2: str): + """Raise |AssertionError| if `manifest1` does not exactly match `manifest2`. + + `name1` and `name2` appear in the diff printed if the assertion fails. """ - msg = ("Package manifests don't match\n\n%s" % - manifest1.diff(manifest2, name1, name2)) + msg = "Package manifests don't match\n\n%s" % manifest1.diff(manifest2, name1, name2) assert manifest1 == manifest2, msg -def assertPackagesMatch(path1, path2): - """ - Raise |AssertionError| if manifest of package at *path1* does not exactly - match that of package at *path2*. +def assertPackagesMatch(path1: str, path2: str): + """Raise if manifest of package at `path1` does not exactly match that of package at `path2`. + + Raises `AssertionError` in that case. """ manifest1, manifest2 = Manifest(path1), Manifest(path2) - msg = ("Package manifests don't match\n\n%s" % - manifest1.diff(manifest2, path1, path2)) + msg = "Package manifests don't match\n\n%s" % manifest1.diff(manifest2, path1, path2) assert manifest1 == manifest2, msg -class OpcCommand(object): - """ - Executes opc-diag command as configured and makes results available. - """ - def __init__(self, subcommand, *args): +class OpcCommand: + """Executes opc-diag command as configured and makes results available.""" + + def __init__(self, subcommand: str, *args: str): self.subcommand = subcommand self.args = args @@ -70,7 +60,7 @@ def assert_stderr_empty(self): the captured output. """ tmpl = "Unexpected output on stderr\n'%s'\n" - assert self.std_err == '', tmpl % self.std_err + assert self.std_err == "", tmpl % self.std_err def assert_stdout_empty(self): """ @@ -78,43 +68,38 @@ def assert_stdout_empty(self): the captured output. """ tmpl = "Unexpected output on stdout\n'%s'\n" - assert self.std_out == '', tmpl % self.std_out + assert self.std_out == "", tmpl % self.std_out - def assert_stdout_matches(self, filename): - """ - Raise AssertionError with helpful diagnostic message if output - captured on stdout doesn't match contents of file identified by - *filename* in known directory. + def assert_stdout_matches(self, filename: str): + """Raise if captured stdout does not match contents of `filename`. + + Raise AssertionError with helpful diagnostic message if output captured on stdout doesn't + match contents of file identified by `filename` in known directory. """ expected_stdout = self._expected_output(filename) - msg = ("\n\nexpected output:\n'%s'\n\nactual output:\n'%s'" % - (expected_stdout, self.std_out)) - std_out = self.std_out.replace('\r\n', '\n') # normalize line endings + msg = "\n\nexpected output:\n'%s'\n\nactual output:\n'%s'" % ( + expected_stdout, + self.std_out, + ) + std_out = self.std_out.replace("\r\n", "\n") # normalize line endings assert std_out == expected_stdout, msg def execute(self): - """ - Execute the configured command in a subprocess and capture the - results. - """ - args = ['python', 'opc-stub'] + """Execute the configured command in a subprocess and capture the results.""" + args = ["python", "opc-stub"] args.append(self.subcommand) args.extend(self.args) - self.proc = subprocess.Popen( - args, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) + self.proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) std_out_bytes, std_err_bytes = self.proc.communicate() - self.std_out = std_out_bytes.decode('utf-8') - self.std_err = std_err_bytes.decode('utf-8') + self.std_out = std_out_bytes.decode("utf-8") + self.std_err = std_err_bytes.decode("utf-8") return self @staticmethod - def _expected_output(filename): - """ - Return contents of file with *filename* in known directory as text. - """ - path = absjoin(test_file_dir, 'expected_output', filename) - with open(path, 'rb') as f: + def _expected_output(filename: str): + """Return contents of file with `filename` in known directory as text.""" + path = absjoin(test_file_dir, "expected_output", filename) + with open(path, "rb") as f: expected_bytes = f.read() - expected_text = expected_bytes.decode('utf-8') + expected_text = expected_bytes.decode("utf-8") return expected_text diff --git a/features/steps/opc_diag_steps.py b/features/steps/opc_diag_steps.py index 5d9be54..3b09fec 100644 --- a/features/steps/opc_diag_steps.py +++ b/features/steps/opc_diag_steps.py @@ -1,266 +1,302 @@ -# encoding: utf-8 +"""Acceptance test steps for opc-diag package.""" -""" -Acceptance test steps for opc-diag package -""" +# pyright: reportPrivateUsage=false + +from __future__ import annotations import os import shutil from behave import given, then, when +from behave.runner import Context +from step_data import Manifest, _Manifest from helpers import ( - assertManifestsMatch, assertPackagesMatch, OpcCommand, ref_pkg_path, - scratch_path + OpcCommand, + assertManifestsMatch, + assertPackagesMatch, + ref_pkg_path, + scratch_path, ) -from step_data import Manifest, _Manifest - -SUBCMD_BROWSE = 'browse' -SUBCMD_DIFF = 'diff' -SUBCMD_DIFF_ITEM = 'diff-item' -SUBCMD_EXTRACT = 'extract' -SUBCMD_REPACKAGE = 'repackage' -SUBCMD_SUBSTITUTE = 'substitute' -URI_CONTENT_TYPES = '[Content_Types].xml' -URI_CORE_PROPS = 'docProps/core.xml' -URI_PKG_RELS = '_rels/.rels' -URI_SLIDE_MASTER = 'ppt/slideMasters/slideMaster1.xml' +SUBCMD_BROWSE = "browse" +SUBCMD_DIFF = "diff" +SUBCMD_DIFF_ITEM = "diff-item" +SUBCMD_EXTRACT = "extract" +SUBCMD_REPACKAGE = "repackage" +SUBCMD_SUBSTITUTE = "substitute" +URI_CONTENT_TYPES = "[Content_Types].xml" +URI_CORE_PROPS = "docProps/core.xml" +URI_PKG_RELS = "_rels/.rels" +URI_SLIDE_MASTER = "ppt/slideMasters/slideMaster1.xml" # commonly used paths ------------------ -base_dir_pkg_path = ref_pkg_path('source') -base_zip_pkg_path = ref_pkg_path('base.pptx') -pkg_paths = {'dir': base_dir_pkg_path, 'zip': base_zip_pkg_path} +base_dir_pkg_path = ref_pkg_path("source") +base_zip_pkg_path = ref_pkg_path("base.pptx") +pkg_paths = {"dir": base_dir_pkg_path, "zip": base_zip_pkg_path} -base_pkg_path = ref_pkg_path('base.pptx') -changed_pkg_path = ref_pkg_path('changed.pptx') -expanded_dir = ref_pkg_path('source') -extract_dir = scratch_path('extracted') -scratch_pkg_path = scratch_path('test_out.pptx') +base_pkg_path = ref_pkg_path("base.pptx") +changed_pkg_path = ref_pkg_path("changed.pptx") +expanded_dir = ref_pkg_path("source") +extract_dir = scratch_path("extracted") +scratch_pkg_path = scratch_path("test_out.pptx") # given ==================================================== -@given('a target directory that does not exist') -def step_remove_target_directory(context): + +@given("a target directory that does not exist") +def step_remove_target_directory(context: Context): if os.path.exists(extract_dir): shutil.rmtree(extract_dir) # when ===================================================== -@when('I issue a command to browse an XML part in a {pkg_type} package') -def step_issue_command_to_browse_pkg_part(context, pkg_type): - context.cmd = OpcCommand(SUBCMD_BROWSE, pkg_paths[pkg_type], - URI_CORE_PROPS).execute() + +@when("I issue a command to browse an XML part in a {pkg_type} package") +def step_issue_command_to_browse_pkg_part(context: Context, pkg_type: str): + context.cmd = OpcCommand(SUBCMD_BROWSE, pkg_paths[pkg_type], URI_CORE_PROPS).execute() -@when('I issue a command to browse the content types of a {pkg_type} package') -def step_issue_command_to_browse_content_types(context, pkg_type): - context.cmd = OpcCommand(SUBCMD_BROWSE, pkg_paths[pkg_type], - URI_CONTENT_TYPES).execute() +@when("I issue a command to browse the content types of a {pkg_type} package") +def step_issue_command_to_browse_content_types(context: Context, pkg_type: str): + context.cmd = OpcCommand(SUBCMD_BROWSE, pkg_paths[pkg_type], URI_CONTENT_TYPES).execute() -@when('I issue a command to browse the package rels of a {pkg_type} package') -def step_issue_command_to_browse_pkg_rels(context, pkg_type): - context.cmd = OpcCommand(SUBCMD_BROWSE, pkg_paths[pkg_type], - URI_PKG_RELS).execute() +@when("I issue a command to browse the package rels of a {pkg_type} package") +def step_issue_command_to_browse_pkg_rels(context: Context, pkg_type: str): + context.cmd = OpcCommand(SUBCMD_BROWSE, pkg_paths[pkg_type], URI_PKG_RELS).execute() -@when('I issue a command to diff the content types between two packages') -def step_command_diff_content_types(context): +@when("I issue a command to diff the content types between two packages") +def step_command_diff_content_types(context: Context): context.cmd = OpcCommand( SUBCMD_DIFF_ITEM, base_pkg_path, changed_pkg_path, URI_CONTENT_TYPES ).execute() -@when('I issue a command to diff the package rels between two packages') -def step_command_diff_pkg_rels_item(context): +@when("I issue a command to diff the package rels between two packages") +def step_command_diff_pkg_rels_item(context: Context): context.cmd = OpcCommand( SUBCMD_DIFF_ITEM, base_pkg_path, changed_pkg_path, URI_PKG_RELS ).execute() -@when('I issue a command to diff the slide master between two packages') -def step_command_diff_slide_master(context): +@when("I issue a command to diff the slide master between two packages") +def step_command_diff_slide_master(context: Context): context.cmd = OpcCommand( SUBCMD_DIFF_ITEM, base_pkg_path, changed_pkg_path, URI_SLIDE_MASTER ).execute() -@when('I issue a command to diff two packages') -def step_command_diff_two_packages(context): - context.cmd = OpcCommand( - SUBCMD_DIFF, base_pkg_path, changed_pkg_path - ).execute() +@when("I issue a command to diff two packages") +def step_command_diff_two_packages(context: Context): + context.cmd = OpcCommand(SUBCMD_DIFF, base_pkg_path, changed_pkg_path).execute() -@when('I issue a command to extract a package') -def step_command_extract_package(context): - context.cmd = OpcCommand( - SUBCMD_EXTRACT, base_pkg_path, extract_dir - ).execute() +@when("I issue a command to extract a package") +def step_command_extract_package(context: Context): + context.cmd = OpcCommand(SUBCMD_EXTRACT, base_pkg_path, extract_dir).execute() -@when('I issue a command to repackage an expanded package directory') -def step_command_repackage_expanded_pkg_dir(context): - context.cmd = OpcCommand( - SUBCMD_REPACKAGE, expanded_dir, scratch_pkg_path - ).execute() +@when("I issue a command to repackage an expanded package directory") +def step_command_repackage_expanded_pkg_dir(context: Context): + context.cmd = OpcCommand(SUBCMD_REPACKAGE, expanded_dir, scratch_pkg_path).execute() -@when('I issue a command to substitute a package item') -def step_command_substitute_pkg_item(context): +@when("I issue a command to substitute a package item") +def step_command_substitute_pkg_item(context: Context): context.cmd = OpcCommand( - SUBCMD_SUBSTITUTE, URI_SLIDE_MASTER, changed_pkg_path, base_pkg_path, - scratch_pkg_path + SUBCMD_SUBSTITUTE, + URI_SLIDE_MASTER, + changed_pkg_path, + base_pkg_path, + scratch_pkg_path, ).execute() # then ===================================================== -@then('a zip package with matching contents appears at the path I specified') -def step_then_matching_zip_pkg_appears_at_specified_path(context): + +@then("a zip package with matching contents appears at the path I specified") +def step_then_matching_zip_pkg_appears_at_specified_path(context: Context): context.cmd.assert_stderr_empty() context.cmd.assert_stdout_empty() assertPackagesMatch(expanded_dir, scratch_pkg_path) -@then('the content types diff appears on stdout') -def step_then_content_types_diff_appears_on_stdout(context): +@then("the content types diff appears on stdout") +def step_then_content_types_diff_appears_on_stdout(context: Context): context.cmd.assert_stderr_empty() - context.cmd.assert_stdout_matches('diff-item.content_types.txt') + context.cmd.assert_stdout_matches("diff-item.content_types.txt") -@then('the formatted content types item appears on stdout') -def step_then_content_types_appear_on_stdout(context): +@then("the formatted content types item appears on stdout") +def step_then_content_types_appear_on_stdout(context: Context): context.cmd.assert_stderr_empty() - context.cmd.assert_stdout_matches('browse.content_types.txt') + context.cmd.assert_stdout_matches("browse.content_types.txt") -@then('the formatted package part XML appears on stdout') -def step_then_pkg_part_xml_appears_on_stdout(context): +@then("the formatted package part XML appears on stdout") +def step_then_pkg_part_xml_appears_on_stdout(context: Context): context.cmd.assert_stderr_empty() - context.cmd.assert_stdout_matches('browse.core_props.txt') + context.cmd.assert_stdout_matches("browse.core_props.txt") -@then('the formatted package rels XML appears on stdout') -def step_then_pkg_rels_xml_appears_on_stdout(context): +@then("the formatted package rels XML appears on stdout") +def step_then_pkg_rels_xml_appears_on_stdout(context: Context): context.cmd.assert_stderr_empty() - context.cmd.assert_stdout_matches('browse.pkg_rels.txt') + context.cmd.assert_stdout_matches("browse.pkg_rels.txt") -@then('the package diff appears on stdout') -def step_then_pkg_diff_appears_on_stdout(context): +@then("the package diff appears on stdout") +def step_then_pkg_diff_appears_on_stdout(context: Context): context.cmd.assert_stderr_empty() - context.cmd.assert_stdout_matches('diff.txt') + context.cmd.assert_stdout_matches("diff.txt") -@then('the package items appear in the target directory') -def step_then_pkg_appears_in_target_dir(context): +@then("the package items appear in the target directory") +def step_then_pkg_appears_in_target_dir(context: Context): context.cmd.assert_stderr_empty() context.cmd.assert_stdout_empty() actual_manifest = Manifest(extract_dir) expected_sha1_list = [ - ('b7377d13b945fd27216d02d50277a350c8c4aea6', - '[Content_Types].xml'), - ('11a0facc96d560bf07b4691f0526b09229264e20', - '_rels/.rels'), - ('c1ae3715531e49808610f18c4810704f70be3767', - 'docProps/app.xml'), - ('775edccda43956b1e55c8fe668ba817934ee17c8', - 'docProps/core.xml'), - ('585be5da0832f70b4e71f66f5784cc8acbcc8e88', - 'docProps/thumbnail.jpeg'), - ('85ff3c93403fee9d07d1f52b08e03e1ad8614343', - 'ppt/_rels/presentation.xml.rels'), - ('21bbd2e84efc65591a76e8a7811c79ce65a7f389', - 'ppt/presProps.xml'), - ('8281415d72c1f9f43e2e0b1cdc4a346e7a0545b3', - 'ppt/presentation.xml'), - ('b0feb4cc107c9b2d135b1940560cf8f045ffb746', - 'ppt/printerSettings/printerSettings1.bin'), - ('fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c', - 'ppt/slideLayouts/_rels/slideLayout1.xml.rels'), - ('fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c', - 'ppt/slideLayouts/_rels/slideLayout10.xml.rels'), - ('fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c', - 'ppt/slideLayouts/_rels/slideLayout11.xml.rels'), - ('fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c', - 'ppt/slideLayouts/_rels/slideLayout2.xml.rels'), - ('fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c', - 'ppt/slideLayouts/_rels/slideLayout3.xml.rels'), - ('fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c', - 'ppt/slideLayouts/_rels/slideLayout4.xml.rels'), - ('fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c', - 'ppt/slideLayouts/_rels/slideLayout5.xml.rels'), - ('fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c', - 'ppt/slideLayouts/_rels/slideLayout6.xml.rels'), - ('fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c', - 'ppt/slideLayouts/_rels/slideLayout7.xml.rels'), - ('fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c', - 'ppt/slideLayouts/_rels/slideLayout8.xml.rels'), - ('fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c', - 'ppt/slideLayouts/_rels/slideLayout9.xml.rels'), - ('ec99dfcf6812f8bd0c9e0a2363d38301e8104803', - 'ppt/slideLayouts/slideLayout1.xml'), - ('8fa04dcb314de8c2321eaec153e6b85263c52fd8', - 'ppt/slideLayouts/slideLayout10.xml'), - ('7531300ef5c76a217f330d3748c82ce484bcb037', - 'ppt/slideLayouts/slideLayout11.xml'), - ('7fba92ff7c76a5050fd9c0acbbdc98600def0264', - 'ppt/slideLayouts/slideLayout2.xml'), - ('ca2c475ce40be637eb271846fbcee05121d61054', - 'ppt/slideLayouts/slideLayout3.xml'), - ('4ceb2a6391cc08f6883515ecfb117dfe6733daae', - 'ppt/slideLayouts/slideLayout4.xml'), - ('4764ea1d5afd93497b4e3bf665cd5b09f6684f62', - 'ppt/slideLayouts/slideLayout5.xml'), - ('3768f6b561eecdfb4530c6a2a939ed4e822f07f5', - 'ppt/slideLayouts/slideLayout6.xml'), - ('ef830f1b546e799c3ae5a8c3df399d5e3346e70a', - 'ppt/slideLayouts/slideLayout7.xml'), - ('749ba47dc5497c6bd0d8b0b034e648e47c336491', - 'ppt/slideLayouts/slideLayout8.xml'), - ('d49c31a3ba055792ca9dd779bb8897795aa46fff', - 'ppt/slideLayouts/slideLayout9.xml'), - ('4b0a95fbb9e8680c1e766d0ab7080bd854a3f7bc', - 'ppt/slideMasters/_rels/slideMaster1.xml.rels'), - ('477117c4c1f2189edcfd35a194103bf4fc1245d5', - 'ppt/slideMasters/slideMaster1.xml'), - ('27bb16052608af395a606ce1de16239bef2d86c3', - 'ppt/tableStyles.xml'), - ('ea60a5ff9290d9ec08a1546fc38945afb3057226', - 'ppt/theme/theme1.xml'), - ('5df90b0fdcd12c199b36ae1cd36e7541ab14ed90', - 'ppt/viewProps.xml'), + ("b7377d13b945fd27216d02d50277a350c8c4aea6", "[Content_Types].xml"), + ("11a0facc96d560bf07b4691f0526b09229264e20", "_rels/.rels"), + ("c1ae3715531e49808610f18c4810704f70be3767", "docProps/app.xml"), + ("775edccda43956b1e55c8fe668ba817934ee17c8", "docProps/core.xml"), + ("585be5da0832f70b4e71f66f5784cc8acbcc8e88", "docProps/thumbnail.jpeg"), + ("85ff3c93403fee9d07d1f52b08e03e1ad8614343", "ppt/_rels/presentation.xml.rels"), + ("21bbd2e84efc65591a76e8a7811c79ce65a7f389", "ppt/presProps.xml"), + ("8281415d72c1f9f43e2e0b1cdc4a346e7a0545b3", "ppt/presentation.xml"), + ( + "b0feb4cc107c9b2d135b1940560cf8f045ffb746", + "ppt/printerSettings/printerSettings1.bin", + ), + ( + "fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c", + "ppt/slideLayouts/_rels/slideLayout1.xml.rels", + ), + ( + "fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c", + "ppt/slideLayouts/_rels/slideLayout10.xml.rels", + ), + ( + "fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c", + "ppt/slideLayouts/_rels/slideLayout11.xml.rels", + ), + ( + "fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c", + "ppt/slideLayouts/_rels/slideLayout2.xml.rels", + ), + ( + "fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c", + "ppt/slideLayouts/_rels/slideLayout3.xml.rels", + ), + ( + "fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c", + "ppt/slideLayouts/_rels/slideLayout4.xml.rels", + ), + ( + "fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c", + "ppt/slideLayouts/_rels/slideLayout5.xml.rels", + ), + ( + "fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c", + "ppt/slideLayouts/_rels/slideLayout6.xml.rels", + ), + ( + "fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c", + "ppt/slideLayouts/_rels/slideLayout7.xml.rels", + ), + ( + "fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c", + "ppt/slideLayouts/_rels/slideLayout8.xml.rels", + ), + ( + "fbccb1d0db1ad72bea6b96449d5033ee7ad3ee3c", + "ppt/slideLayouts/_rels/slideLayout9.xml.rels", + ), + ( + "ec99dfcf6812f8bd0c9e0a2363d38301e8104803", + "ppt/slideLayouts/slideLayout1.xml", + ), + ( + "8fa04dcb314de8c2321eaec153e6b85263c52fd8", + "ppt/slideLayouts/slideLayout10.xml", + ), + ( + "7531300ef5c76a217f330d3748c82ce484bcb037", + "ppt/slideLayouts/slideLayout11.xml", + ), + ( + "7fba92ff7c76a5050fd9c0acbbdc98600def0264", + "ppt/slideLayouts/slideLayout2.xml", + ), + ( + "ca2c475ce40be637eb271846fbcee05121d61054", + "ppt/slideLayouts/slideLayout3.xml", + ), + ( + "4ceb2a6391cc08f6883515ecfb117dfe6733daae", + "ppt/slideLayouts/slideLayout4.xml", + ), + ( + "4764ea1d5afd93497b4e3bf665cd5b09f6684f62", + "ppt/slideLayouts/slideLayout5.xml", + ), + ( + "3768f6b561eecdfb4530c6a2a939ed4e822f07f5", + "ppt/slideLayouts/slideLayout6.xml", + ), + ( + "ef830f1b546e799c3ae5a8c3df399d5e3346e70a", + "ppt/slideLayouts/slideLayout7.xml", + ), + ( + "749ba47dc5497c6bd0d8b0b034e648e47c336491", + "ppt/slideLayouts/slideLayout8.xml", + ), + ( + "d49c31a3ba055792ca9dd779bb8897795aa46fff", + "ppt/slideLayouts/slideLayout9.xml", + ), + ( + "4b0a95fbb9e8680c1e766d0ab7080bd854a3f7bc", + "ppt/slideMasters/_rels/slideMaster1.xml.rels", + ), + ( + "477117c4c1f2189edcfd35a194103bf4fc1245d5", + "ppt/slideMasters/slideMaster1.xml", + ), + ("27bb16052608af395a606ce1de16239bef2d86c3", "ppt/tableStyles.xml"), + ("ea60a5ff9290d9ec08a1546fc38945afb3057226", "ppt/theme/theme1.xml"), + ("5df90b0fdcd12c199b36ae1cd36e7541ab14ed90", "ppt/viewProps.xml"), ] expected_manifest = _Manifest(expected_sha1_list) - assertManifestsMatch( - actual_manifest, expected_manifest, 'actual', 'expected' - ) + assertManifestsMatch(actual_manifest, expected_manifest, "actual", "expected") -@then('the package rels diff appears on stdout') -def step_then_pkg_rels_diff_appears_on_stdout(context): +@then("the package rels diff appears on stdout") +def step_then_pkg_rels_diff_appears_on_stdout(context: Context): context.cmd.assert_stderr_empty() - context.cmd.assert_stdout_matches('diff-item.pkg_rels.txt') + context.cmd.assert_stdout_matches("diff-item.pkg_rels.txt") -@then('the resulting package contains the substituted item') -def step_then_resulting_pkg_contains_substituted_item(context): +@then("the resulting package contains the substituted item") +def step_then_resulting_pkg_contains_substituted_item(context: Context): context.cmd.assert_stderr_empty() - context.cmd.assert_stdout_matches('substitute.txt') + context.cmd.assert_stdout_matches("substitute.txt") subst_sha = Manifest(changed_pkg_path)[URI_SLIDE_MASTER] expected_manifest = Manifest(base_pkg_path) expected_manifest[URI_SLIDE_MASTER] = subst_sha actual_manifest = Manifest(scratch_pkg_path) - assertManifestsMatch( - actual_manifest, expected_manifest, 'actual', 'expected') + assertManifestsMatch(actual_manifest, expected_manifest, "actual", "expected") -@then('the slide master diff appears on stdout') -def step_then_slide_master_diff_appears_on_stdout(context): +@then("the slide master diff appears on stdout") +def step_then_slide_master_diff_appears_on_stdout(context: Context): context.cmd.assert_stderr_empty() - context.cmd.assert_stdout_matches('diff-item.slide_master.txt') + context.cmd.assert_stdout_matches("diff-item.slide_master.txt") diff --git a/features/steps/step_data.py b/features/steps/step_data.py index 7e1d94c..7eb7957 100644 --- a/features/steps/step_data.py +++ b/features/steps/step_data.py @@ -1,17 +1,17 @@ -# encoding: utf-8 +from __future__ import annotations import hashlib import os - from difflib import unified_diff +from typing import Iterable from zipfile import ZipFile -def Manifest(path): - """ - Factory function for _Manifest object. Return |_Manifest| instance for - package at *path*. *path* can point to either a zip package or a - directory containing an extracted package. +def Manifest(path: str): + """Factory function for _Manifest object. + + Return |_Manifest| instance for package at *path*. *path* can point to either a zip package or + a directory containing an extracted package. """ if os.path.isdir(path): return _Manifest.from_dir(path) @@ -19,21 +19,19 @@ def Manifest(path): return _Manifest.from_zip(path) -class _Manifest(object): - """ - A sorted sequence of SHA1, name 2-tuples that unambiguously characterize - the contents of an OPC package, providing a basis for asserting - equivalence of two packages, whether stored as a zip archive or a package - extracted into a directory. +class _Manifest: + """A sorted sequence of (SHA1, name) pairs. + + The pairsthat unambiguously characterize the contents of an OPC package, providing a basis for + asserting equivalence of two packages, whether stored as a zip archive or a package extracted + into a directory. """ - def __init__(self, sha_names): - """ - *sha_names* is a list of SHA1, name 2-tuples, each of which describes - a package item. - """ + + def __init__(self, sha_names: Iterable[tuple[str, str]]): + """`sha_names` is a list of SHA1, name 2-tuples, each of which describes a package item.""" self.sha_names = sorted(sha_names, key=lambda t: t[1]) - def __eq__(self, other): + def __eq__(self, other: object): """ Return true if *other* is a |_Manifest| instance with exactly matching sha_names. @@ -42,21 +40,16 @@ def __eq__(self, other): return False return self.sha_names == other.sha_names - def __getitem__(self, key): - """ - Return SHA1 of sha_name tuple with name that matches *key*. - """ + def __getitem__(self, key: str): + """Return SHA1 of sha_name tuple with name that matches *key*.""" for sha, name in self.sha_names: if name == key: return sha raise KeyError("no item with name '%s'" % key) - def __setitem__(self, key, value): - """ - Set SHA1 of sha_name tuple with name that matches *key* to *value*. - """ - for idx, sha_name in enumerate(self.sha_names): - sha, name = sha_name + def __setitem__(self, key: str, value: str): + """Set SHA1 of sha_name tuple with name that matches *key* to *value*.""" + for idx, (_, name) in enumerate(self.sha_names): if name == key: self.sha_names[idx] = (value, name) return @@ -69,42 +62,37 @@ def __str__(self): """ tmpl = " ('%s',\n '%s')," lines = [tmpl % (sha, name) for sha, name in self.sha_names] - return 'manifest = [\n%s\n]' % '\n'.join(lines) + return "manifest = [\n%s\n]" % "\n".join(lines) - def diff(self, other, filename_1, filename_2): - """ - Return a ``diff`` style unified diff listing between the sha_names of - this manifest and *other*. + def diff(self, other: _Manifest, filename_1: str, filename_2: str): + """diff between the sha_names of this manifest and those of `other`. + + Return value is a `diff` style unified diff listing between the sha_names of this manifest + and `other`. """ text_1, text_2 = str(self), str(other) - lines_1 = text_1.split('\n') - lines_2 = text_2.split('\n') + lines_1 = text_1.split("\n") + lines_2 = text_2.split("\n") diff = unified_diff(lines_1, lines_2, filename_1, filename_2) - return '\n'.join([line for line in diff]) + return "\n".join(line for line in diff) @staticmethod - def from_dir(dirpath): - """ - Return a |_Manifest| instance for the extracted OPC package at - *dirpath* - """ - sha_names = [] + def from_dir(dirpath: str): + """Return a |_Manifest| instance for the extracted OPC package at *dirpath*.""" + sha_names: list[tuple[str, str]] = [] for filepath in sorted(_Manifest._filepaths_in_dir(dirpath)): - with open(filepath, 'rb') as f: + with open(filepath, "rb") as f: blob = f.read() sha = hashlib.sha1(blob).hexdigest() - name = os.path.relpath(filepath, dirpath).replace('\\', '/') + name = os.path.relpath(filepath, dirpath).replace("\\", "/") sha_names.append((sha, name)) return _Manifest(sha_names) @staticmethod - def from_zip(zip_file_path): - """ - Return a |_Manifest| instance for the zip-based OPC package at - *zip_file_path* - """ + def from_zip(zip_file_path: str): + """Return a |_Manifest| instance for the zip-based OPC package at *zip_file_path*.""" zipf = ZipFile(zip_file_path) - sha_names = [] + sha_names: list[tuple[str, str]] = [] for name in sorted(zipf.namelist()): blob = zipf.read(name) sha = hashlib.sha1(blob).hexdigest() @@ -113,14 +101,14 @@ def from_zip(zip_file_path): return _Manifest(sha_names) @staticmethod - def _filepaths_in_dir(dirpath): - """ - Return a sorted list of relative paths, one for each of the files - under *dirpath*, recursively visiting all subdirectories. - """ - filepaths = [] - for root, dirnames, filenames in os.walk(dirpath): - for filename in filenames: - filepath = os.path.join(root, filename) - filepaths.append(filepath) - return sorted(filepaths) + def _filepaths_in_dir(dirpath: str): + """Return a sorted list of relative paths. + + Contains one for each of the files under `dirpath`, recursively visiting all + subdirectories. + """ + return sorted( + os.path.join(root, filename) + for root, _, filenames in os.walk(dirpath) + for filename in filenames + ) diff --git a/opcdiag/__init__.py b/opcdiag/__init__.py deleted file mode 100644 index f1bdfa6..0000000 --- a/opcdiag/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# encoding: utf-8 - -__version__ = '1.0.0' # pragma: no cover diff --git a/opcdiag/cli.py b/opcdiag/cli.py deleted file mode 100644 index ee62cb6..0000000 --- a/opcdiag/cli.py +++ /dev/null @@ -1,304 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 - -# opc.py -# -# Command-line interface for operations on one or more Open Packaging -# Convention (OPC) files, such as .docx, .pptx, and .xlsx files. - -import argparse -import os -import sys - - -from opcdiag.controller import OpcController - - -class CommandController(object): - """ - Orchestrates processing of commands in the form of a list of arguments - (*argv*). A new instance is created using the :meth:`new` staticmethod. - Once instantiated, it can process any number of commands by calling its - :meth:`execute` method, once for each command. - """ - def __init__(self, parser, app_controller): - self._parser = parser - self._app_controller = app_controller - - @staticmethod - def new(): - """ - Return a newly created instance of |CommandController| fitted with a - fully configured parser and an instance of the application controller - to dispatch parsed commands to. - """ - parser = Command.parser() - app_controller = OpcController() - return CommandController(parser, app_controller) - - def execute(self, argv=None): - """ - Interpret the command indicated by the arguments in *argv* and - execute it. If *argv* is |None|, ``sys.argv`` is used. - """ - # print help and exit if no args - arg_count = len(argv if argv else sys.argv) - if arg_count < 2: - self._parser.print_help() - sys.exit(1) - - args = self._parser.parse_args(argv) - command = args.command - command.validate(args) - command.execute(args, self._app_controller) - - -class Command(object): - """ - Base class for sub-commands - """ - def __init__(self, parser): - super(Command, self).__init__() - self._parser = parser - - @staticmethod - def parser(): - """ - Return an instance of :class:`argparse.ArgumentParser` configured - with a subcommand parser for each of the commands that are a subclass - of |Command|. - """ - desc = ( - 'Browse and diff Microsoft Office .docx, .xlsx, and .pptx files.' - ) - epilog = "'opc --help' lists command-specific help" - parser = argparse.ArgumentParser( - prog='opc', description=desc, epilog=epilog - ) - subparsers = parser.add_subparsers(title='available commands') - for command_cls in Command.__subclasses__(): - command_parser = command_cls.add_command_parser_to(subparsers) - command = command_cls(command_parser) - command_parser.set_defaults(command=command) - return parser - - def execute(self, args, app_controller): - """ - Abstract method, each command must implement - """ - msg = 'execute() must be implemented by all subclasses of Command' - raise NotImplementedError(msg) - - def validate(self, args): - """ - Abstract method, each command must implement; just pass if there's - nothing to validate. - """ - msg = 'validate() must be implemented by all subclasses of Command' - raise NotImplementedError(msg) - - -class BrowseCommand(Command): - - def __init__(self, parser): - super(BrowseCommand, self).__init__(parser) - - @staticmethod - def add_command_parser_to(subparsers): - parser = subparsers.add_parser( - 'browse', - help='List pretty-printed XML for a specified package part') - parser.add_argument( - 'pkg_path', metavar='PKG_PATH', - help='Path to OPC package file') - parser.add_argument( - 'filename', metavar='FILENAME', - help='Filename portion of the pack URI for the part to browse') - return parser - - def execute(self, args, app_controller): - app_controller.browse(args.pkg_path, args.filename) - - def validate(self, args): - try: - msg = "PKG_PATH '%s' does not exist" % args.pkg_path - assert os.path.exists(args.pkg_path), msg - except AssertionError as e: - self._parser.error(str(e)) - - -class DiffCommand(Command): - - def __init__(self, parser): - super(DiffCommand, self).__init__(parser) - - @staticmethod - def add_command_parser_to(subparsers): - parser = subparsers.add_parser( - 'diff', help='Show differences between two OPC package files') - parser.add_argument( - 'pkg_1_path', metavar='PKG_1_PATH', - help='first package to compare') - parser.add_argument( - 'pkg_2_path', metavar='PKG_2_PATH', - help='second package to compare') - return parser - - def execute(self, args, app_controller): - app_controller.diff_pkg(args.pkg_1_path, args.pkg_2_path) - - def validate(self, args): - paths_that_should_exist = ( - (args.pkg_1_path, 'PKG_1_PATH'), - (args.pkg_2_path, 'PKG_2_PATH'), - ) - try: - for path, metavar in paths_that_should_exist: - msg = "%s '%s' does not exist" % (metavar, path) - assert os.path.exists(path), msg - except AssertionError as e: - self._parser.error(str(e)) - - -class DiffItemCommand(Command): - - def __init__(self, parser): - super(DiffItemCommand, self).__init__(parser) - - @staticmethod - def add_command_parser_to(subparsers): - parser = subparsers.add_parser( - 'diff-item', - help='Show differences between a specified item in two OPC ' - 'package files') - parser.add_argument( - 'pkg_1_path', metavar='PKG_1_PATH', - help='first package') - parser.add_argument( - 'pkg_2_path', metavar='PKG_2_PATH', - help='second package') - parser.add_argument( - 'filename', metavar='FILENAME', - help='Filename portion of pack URI for item to browse') - return parser - - def execute(self, args, app_controller): - app_controller.diff_item( - args.pkg_1_path, args.pkg_2_path, args.filename) - - def validate(self, args): - paths_that_should_exist = ( - (args.pkg_1_path, 'PKG_1_PATH'), - (args.pkg_2_path, 'PKG_2_PATH'), - ) - try: - for path, metavar in paths_that_should_exist: - msg = "%s '%s' does not exist" % (metavar, path) - assert os.path.exists(path), msg - except AssertionError as e: - self._parser.error(str(e)) - - -class ExtractCommand(Command): - - def __init__(self, parser): - super(ExtractCommand, self).__init__(parser) - - @staticmethod - def add_command_parser_to(subparsers): - parser = subparsers.add_parser( - 'extract', - help='Extract all items in a package to a directory') - parser.add_argument( - 'pkg_path', metavar='PKG_PATH', - help='Path to package') - parser.add_argument( - 'dirpath', metavar='DIRPATH', - help='Path to directory into which to extract package items') - return parser - - def validate(self, args): - try: - msg = "PKG_PATH '%s' does not exist" % args.pkg_path - assert os.path.exists(args.pkg_path), msg - except AssertionError as e: - self._parser.error(str(e)) - - def execute(self, args, app_controller): - app_controller.extract_package(args.pkg_path, args.dirpath) - - -class RepackageCommand(Command): - - def __init__(self, parser): - super(RepackageCommand, self).__init__(parser) - - @staticmethod - def add_command_parser_to(subparsers): - parser = subparsers.add_parser( - 'repackage', - help='Build an OPC package from the contents of a directory') - parser.add_argument( - 'dirpath', metavar='DIRPATH', - help='Directory containing expanded package files') - parser.add_argument( - 'new_package', metavar='NEW_PACKAGE', - help='Path at which to save new package file') - return parser - - def validate(self, args): - try: - msg = "DIRPATH '%s' not found or not a directory" % args.dirpath - assert os.path.isdir(args.dirpath), msg - except AssertionError as e: - self._parser.error(str(e)) - - def execute(self, args, app_controller): - app_controller.repackage(args.dirpath, args.new_package) - - -class SubstituteCommand(Command): - - def __init__(self, parser): - super(SubstituteCommand, self).__init__(parser) - - @staticmethod - def add_command_parser_to(subparsers): - parser = subparsers.add_parser( - 'substitute', - help='Substitute a part from one package into another') - parser.add_argument( - 'filename', metavar='FILENAME', - help='Filename portion of partname for part to substitute') - parser.add_argument( - 'src_pkg_path', metavar='SRC_PKG_PATH', - help='package from which to source part identified by FILENAME') - parser.add_argument( - 'tgt_pkg_path', metavar='TGT_PKG_PATH', - help='package from which to get all remaining parts') - parser.add_argument( - 'result_pkg_path', metavar='RESULT_PKG_PATH', - help='path at which to store resulting package file') - return parser - - def validate(self, args): - paths_that_should_exist = ( - (args.src_pkg_path, 'SRC_PKG_PATH'), - (args.tgt_pkg_path, 'TGT_PKG_PATH'), - ) - try: - for path, metavar in paths_that_should_exist: - msg = "%s '%s' does not exist" % (metavar, path) - assert os.path.exists(path), msg - except AssertionError as e: - self._parser.error(str(e)) - - def execute(self, args, app_controller): - app_controller.substitute( - args.filename, args.src_pkg_path, args.tgt_pkg_path, - args.result_pkg_path) - - -def main(argv=None): - command_controller = CommandController.new() - command_controller.execute(argv) diff --git a/opcdiag/model.py b/opcdiag/model.py deleted file mode 100644 index 9062bf8..0000000 --- a/opcdiag/model.py +++ /dev/null @@ -1,202 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Package and package items model -""" - -import os - -from lxml import etree - -from opcdiag.phys_pkg import BlobCollection, PhysPkg - - -_CONTENT_TYPES_URI = '[Content_Types].xml' - - -class Package(object): - """ - Root of package graph and main model API class. - """ - def __init__(self, pkg_items): - super(Package, self).__init__() - self._pkg_items = pkg_items - - @staticmethod - def read(path): - """ - Factory method to construct a new |Package| instance from package - at *path*. The package can be either a zip archive (e.g. .docx file) - or a directory containing an extracted package. - """ - phys_pkg = PhysPkg.read(path) - pkg_items = {} - for uri, blob in phys_pkg: - pkg_items[uri] = PkgItem(phys_pkg.root_uri, uri, blob) - return Package(pkg_items) - - def find_item_by_uri_tail(self, uri_tail): - """ - Return the first item in this package having a uri that ends with - *uri_tail*. Raises |KeyError| if no matching item is found. - """ - for uri in self._uris: - if uri.endswith(uri_tail): - return self._pkg_items[uri] - raise KeyError("No item with name '%s'" % uri_tail) - - def prettify_xml(self): - """ - Reformat the XML in all package items having XML content to indented, - human-readable format. If viewed after this method is called, the XML - appears "pretty printed". - """ - for pkg_item in self._pkg_items.itervalues(): - pkg_item.prettify_xml() - - @property - def rels_items(self): - """ - Return list of rels items in this package, sorted by pack URI. - """ - rels_items = [] - for uri in self._uris: - pkg_item = self._pkg_items[uri] - if pkg_item.is_rels_item: - rels_items.append(pkg_item) - return rels_items - - def save(self, path): - """ - Save this package to a zip archive at *path*. - """ - PhysPkg.write_to_zip(self._blobs, path) - - def save_to_dir(self, dirpath): - """ - Save each of the items in this package as a file in a directory at - *dirpath*, using the pack URI as the relative path of each file. If - the directory exists, it is deleted (recursively) before being - recreated. - """ - PhysPkg.write_to_dir(self._blobs, dirpath) - - def substitute_item(self, src_pkg_item): - """ - Locate the item in this package that corresponds with *src_pkg_item* - and replace its blob with that from *src_pkg_item*. - """ - tgt_pkg_item = self._pkg_items[src_pkg_item.uri] - tgt_pkg_item.blob = src_pkg_item.blob - - @property - def xml_parts(self): - """ - Return list of XML parts in this package, sorted by partname. - """ - xml_parts = [] - for uri in self._uris: - pkg_item = self._pkg_items[uri] - if pkg_item.is_xml_part: - xml_parts.append(pkg_item) - return xml_parts - - @property - def _blobs(self): - """ - A |BlobCollection| instance containing a snapshot of the blobs in the - package. - """ - blobs = BlobCollection() - for uri, pkg_item in self._pkg_items.items(): - blobs[uri] = pkg_item.blob - return blobs - - @property - def _uris(self): - """ - Return sorted list of item URIs in this package. - """ - return sorted(self._pkg_items.keys()) - - -class PkgItem(object): - """ - Individual item (file, roughly) within an OPC package. - """ - def __init__(self, root_uri, uri, blob): - super(PkgItem, self).__init__() - self._blob = blob - self._root_uri = root_uri - self._uri = uri - - @property - def blob(self): - """ - The binary contents of this package item, frequently but not always - XML text. - """ - return self._blob # pragma: no cover - - @blob.setter - def blob(self, value): - self._blob = value # pragma: no cover - - @property - def element(self): - """ - Return an lxml.etree Element obtained by parsing the XML in this - item's blob. - """ - return etree.fromstring(self._blob) - - @property - def is_content_types(self): - """ - True if this item is the ``[Content_Types].xml`` item in the package, - False otherwise. - """ - return self._uri == _CONTENT_TYPES_URI - - @property - def is_rels_item(self): - """ - True if this item is a relationships item, i.e. its uri ends with - ``.rels``, False otherwise. - """ - return self._uri.endswith('.rels') - - @property - def is_xml_part(self): - """ - True if the URI of this item ends with '.xml', except if it is the - content types item. False otherwise. - """ - return self._uri.endswith('.xml') and not self.is_content_types - - @property - def path(self): - """ - Return the path of this item as though it were extracted into a - directory at its package path. - """ - uri_part = os.path.normpath(self._uri) - return os.path.join(self._root_uri, uri_part) - - def prettify_xml(self): - """ - Reformat the XML in this package item to indented, human-readable - form. Does nothing if this package item does not contain XML. - """ - if self.is_content_types or self.is_xml_part or self.is_rels_item: - self._blob = etree.tostring( - self.element, encoding='UTF-8', standalone=True, - pretty_print=True - ) - - @property - def uri(self): - """ - The pack URI of this package item, e.g. ``'/word/document.xml'``. - """ - return self._uri # pragma: no cover diff --git a/opcdiag/phys_pkg.py b/opcdiag/phys_pkg.py deleted file mode 100644 index 3df6956..0000000 --- a/opcdiag/phys_pkg.py +++ /dev/null @@ -1,174 +0,0 @@ -# -*- coding: utf-8 -*- -# -# phys_pkg.py -# -# Copyright (C) 2013 Steve Canny scanny@cisco.com -# -# This module is part of opc-diag and is released under the MIT License: -# http://www.opensource.org/licenses/mit-license.php - -"""Interface to a physical OPC package, either a zip archive or directory""" - -import os -import shutil - -from zipfile import ZIP_DEFLATED, ZipFile - - -class BlobCollection(dict): - """ - Structures a set of blobs, like a set of files in an OPC package. - It can add and retrieve items by URI (relative path, roughly) and can - also retrieve items by uri_tail, the trailing portion of the URI. - """ - - -class PhysPkg(object): - """ - Provides read and write services for packages on the filesystem. Suitable - for use with OPC packages in either Zip or expanded directory form. - |PhysPkg| objects are iterable, generating a (uri, blob) 2-tuple for each - item in the package. - """ - def __init__(self, blobs, root_uri): - super(PhysPkg, self).__init__() - self._blobs = blobs - self._root_uri = root_uri - - def __iter__(self): - """ - Generate a (uri, blob) 2-tuple for each of the items in the package. - """ - return iter(self._blobs.items()) - - @staticmethod - def read(path): - """ - Return a |PhysPkg| instance loaded with contents of OPC package at - *path*, where *path* can be either a regular zip package or a - directory containing an expanded package. - """ - if os.path.isdir(path): - return DirPhysPkg.read(path) - else: - return ZipPhysPkg.read(path) - - @property - def root_uri(self): - return self._root_uri # pragma: no cover - - @staticmethod - def write_to_dir(blobs, dirpath): - """ - Write the contents of the |BlobCollection| instance *blobs* to a - directory at *dirpath*. If a directory already exists at *dirpath*, - it is deleted before being recreated. If a file exists at *dirpath*, - |ValueError| is raised, to prevent unintentional overwriting. - """ - PhysPkg._clear_or_make_dir(dirpath) - for uri, blob in blobs.items(): - PhysPkg._write_blob_to_dir(dirpath, uri, blob) - - @staticmethod - def write_to_zip(blobs, pkg_zip_path): - """ - Write "files" in |BlobCollection| instance *blobs* to a zip archive - at *pkg_zip_path*. - """ - zipf = ZipFile(pkg_zip_path, 'w', ZIP_DEFLATED) - for uri in sorted(blobs.keys()): - blob = blobs[uri] - zipf.writestr(uri, blob) - zipf.close() - - @staticmethod - def _clear_or_make_dir(dirpath): - """ - Create a new, empty directory at *dirpath*, removing and recreating - any directory found there. Raises |ValueError| if *dirpath* exists - but is not a directory. - """ - # raise if *dirpath* is a file - if os.path.exists(dirpath) and not os.path.isdir(dirpath): - tmpl = "target path '%s' is not a directory" - raise ValueError(tmpl % dirpath) - # remove any existing directory tree at *dirpath* - if os.path.exists(dirpath): - shutil.rmtree(dirpath) - # create dir at dirpath, as well as any intermediate-level dirs - os.makedirs(dirpath) - - @staticmethod - def _write_blob_to_dir(dirpath, uri, blob): - """ - Write *blob* to a file under *dirpath*, where the segments of *uri* - that precede the filename are created, as required, as intermediate - directories. - """ - # In general, uri will contain forward slashes as segment separators. - # This next line converts them to backslashes on Windows. - item_relpath = os.path.normpath(uri) - fullpath = os.path.join(dirpath, item_relpath) - dirpath, filename = os.path.split(fullpath) - if not os.path.exists(dirpath): - os.makedirs(dirpath) - with open(fullpath, 'wb') as f: - f.write(blob) - - -class DirPhysPkg(PhysPkg): - """ - An OPC physical package that has been expanded into individual files in - a directory structure that mirrors the pack URI. - """ - def __init__(self, blobs, root_uri): - super(DirPhysPkg, self).__init__(blobs, root_uri) - - @classmethod - def read(cls, pkg_dir): - """ - Return a |BlobCollection| instance loaded from *pkg_dir*. - """ - blobs = BlobCollection() - pfx_len = len(pkg_dir)+1 - for filepath in cls._filepaths_in_dir(pkg_dir): - uri = filepath[pfx_len:].replace('\\', '/') - with open(filepath, 'rb') as f: - blob = f.read() - blobs[uri] = blob - root_uri = pkg_dir - return cls(blobs, root_uri) - - @staticmethod - def _filepaths_in_dir(dirpath): - """ - Return a sorted list of relative paths, one for each of the files - under *dirpath*, recursively visiting all subdirectories. - """ - filepaths = [] - for root, dirnames, filenames in os.walk(dirpath): - for filename in filenames: - filepath = os.path.join(root, filename) - filepaths.append(filepath) - return sorted(filepaths) - - -class ZipPhysPkg(PhysPkg): - """ - An OPC physical package in the typically encountered form, a zip archive. - """ - def __init__(self, blobs, root_uri): - super(ZipPhysPkg, self).__init__(blobs, root_uri) - - @classmethod - def read(cls, pkg_zip_path): - """ - Return a |BlobCollection| instance loaded from *pkg_zip_path*. - """ - blobs = BlobCollection() - zipf = ZipFile(pkg_zip_path, 'r') - for name in zipf.namelist(): - blobs[name] = zipf.read(name) - zipf.close() - root_uri = os.path.splitext(pkg_zip_path)[0] - return cls(blobs, root_uri) diff --git a/opcdiag/presenter.py b/opcdiag/presenter.py deleted file mode 100644 index 4625b91..0000000 --- a/opcdiag/presenter.py +++ /dev/null @@ -1,276 +0,0 @@ -# -*- coding: utf-8 -*- -# -# presenter.py -# -# Copyright (C) 2013 Steve Canny scanny@cisco.com -# -# This module is part of opc-diag and is released under the MIT License: -# http://www.opensource.org/licenses/mit-license.php - -"""Presenter classes for opc-diag model classes""" - -from __future__ import unicode_literals - -import re - -from difflib import unified_diff -from lxml import etree - - -def diff(text_1, text_2, filename_1, filename_2): - """ - Return a ``diff`` style unified diff listing between *text_1* and - *text_2*. - """ - lines_1 = text_1.split('\n') - lines_2 = text_2.split('\n') - diff_lines = unified_diff(lines_1, lines_2, filename_1, filename_2) - # this next bit is needed to work around Python 2.6 difflib bug that left - # in a trailing space after the filename if no date was provided. The - # filename lines look like: '--- filename \n' where regular lines look - # like '+foobar'. The space needs to be removed but the linefeed preserved. - trimmed_lines = [] - for line in diff_lines: - if line.endswith(' \n'): - line = '%s\n' % line.rstrip() - trimmed_lines.append(line) - return '\n'.join(trimmed_lines) - - -def prettify_nsdecls(xml): - """ - Wrap and indent attributes on the root element so namespace declarations - don't run off the page in the text editor and can be more easily - inspected. Sort attributes such that the default namespace, if present, - appears first in the list, followed by other namespace declarations, and - then remaining attributes, both in alphabetical order. - """ - - def parse_attrs(rootline): - """ - Return 3-tuple (head, attributes, tail) looking like - (''). - """ - attr_re = re.compile(r'([-a-zA-Z0-9_:.]+="[^"]*" *)') - substrs = [substr.strip() for substr in attr_re.split(rootline) - if substr] - head = substrs[0] - attrs, tail = ((substrs[1:-1], substrs[-1]) if len(substrs) > 1 - else ([], '')) - return (head, attrs, tail) - - def sequence_attrs(attributes): - """ - Sort attributes alphabetically within the subgroups: default - namespace declaration, other namespace declarations, other - attributes. - """ - def_nsdecls, nsdecls, attrs = [], [], [] - for attr in attributes: - if attr.startswith('xmlns='): - def_nsdecls.append(attr) - elif attr.startswith('xmlns:'): - nsdecls.append(attr) - else: - attrs.append(attr) - return sorted(def_nsdecls) + sorted(nsdecls) + sorted(attrs) - - def pretty_rootline(head, attrs, tail): - """ - Return string containing prettified XML root line with *head* on the - first line, *attrs* indented on following lines, and *tail* indented - on the last line. - """ - indent = 4 * ' ' - newrootline = head - for attr in attrs: - newrootline += '\n%s%s' % (indent, attr) - newrootline += '\n%s%s' % (indent, tail) if tail else '' - return newrootline - - lines = xml.splitlines() - rootline = lines[1] - head, attributes, tail = parse_attrs(rootline) - attributes = sequence_attrs(attributes) - lines[1] = pretty_rootline(head, attributes, tail) - return '\n'.join(lines) - - -class DiffPresenter(object): - """ - Forms diffs between packages and their elements. - """ - @staticmethod - def named_item_diff(package_1, package_2, uri_tail): - """ - Return a diff between the text of the item identified by *uri_tail* - in *package_1* and that of its counterpart in *package_2*. - """ - pkg_item_1 = package_1.find_item_by_uri_tail(uri_tail) - pkg_item_2 = package_2.find_item_by_uri_tail(uri_tail) - return DiffPresenter._pkg_item_diff(pkg_item_1, pkg_item_2) - - @staticmethod - def rels_diffs(package_1, package_2): - """ - Return a list of diffs between the rels items in *package_1* against - their counterparts in *package_2*. Rels items are compared in - alphabetical order by pack URI. - """ - package_1_rels_items = package_1.rels_items - return DiffPresenter._pkg_item_diffs(package_1_rels_items, package_2) - - @staticmethod - def xml_part_diffs(package_1, package_2): - """ - Return a list of diffs between the XML parts in *package_1* and their - counterpart in *package_2*. Parts are compared in alphabetical order - by partname (pack URI). - """ - package_1_xml_parts = package_1.xml_parts - return DiffPresenter._pkg_item_diffs(package_1_xml_parts, package_2) - - @staticmethod - def _pkg_item_diff(pkg_item_1, pkg_item_2): - """ - Return a diff between the text of *pkg_item_1* and that of - *pkg_item_2*. - """ - item_presenter_1 = ItemPresenter(pkg_item_1) - item_presenter_2 = ItemPresenter(pkg_item_2) - text_1 = item_presenter_1.text - text_2 = item_presenter_2.text - filename_1 = item_presenter_1.filename - filename_2 = item_presenter_2.filename - return diff(text_1, text_2, filename_1, filename_2) - - @staticmethod - def _pkg_item_diffs(pkg_items, package_2): - """ - Return a list of diffs, one for each item in *pkg_items* that differs - from its counterpart in *package_2*. - """ - diffs = [] - for pkg_item in pkg_items: - uri = pkg_item.uri - pkg_item_2 = package_2.find_item_by_uri_tail(uri) - diff = DiffPresenter._pkg_item_diff(pkg_item, pkg_item_2) - if diff: - diffs.append(diff) - return diffs - - -class ItemPresenter(object): - """ - Base class and factory class for package item presenter classes; also - serves as presenter for binary classes, e.g. .bin and .jpg. - """ - def __new__(cls, pkg_item): - """ - Factory for package item presenter objects, choosing one of - |ContentTypes|, |RelsItem|, or |Part| based on the characteristics of - *pkg_item*. - """ - if pkg_item.is_content_types: - presenter_class = ContentTypesPresenter - elif pkg_item.is_rels_item: - presenter_class = RelsItemPresenter - elif pkg_item.is_xml_part: - presenter_class = XmlPartPresenter - else: - presenter_class = ItemPresenter - return super(ItemPresenter, cls).__new__(presenter_class) - - def __init__(self, pkg_item): - super(ItemPresenter, self).__init__() - self._pkg_item = pkg_item - - @property - def filename(self): - """ - Effective path for this package item, normalized to always use - forward slashes as the path separator. - """ - return self._pkg_item.path.replace('\\', '/') - - @property - def text(self): - """ - Raise |NotImplementedError|; all subclasses must implement a ``text`` - property, returning a text representation of the package item, - generally a formatted version of the item contents. - """ - msg = ("'.text' property must be implemented by all subclasses of It" - "emPresenter") - raise NotImplementedError(msg) - - @property - def xml(self): - """ - Return pretty-printed XML (as unicode text) from this package item's - blob. - """ - xml_bytes = etree.tostring( - self._pkg_item.element, encoding='UTF-8', pretty_print=True, - standalone=True).strip() - xml_text = xml_bytes.decode('utf-8') - return xml_text - - -class ContentTypesPresenter(ItemPresenter): - - def __init__(self, pkg_item): - super(ContentTypesPresenter, self).__init__(pkg_item) - - @property - def text(self): - """ - Return the XML for this content types item formatted for - minimal diffs. The and child elements are sorted - to remove arbitrary ordering between package saves. - """ - lines = self.xml.split('\n') - defaults = sorted([l for l in lines if l.startswith(' XML for this rels item formatted for - minimal diffs. The child elements are sorted to remove - arbitrary ordering between package saves. rId values are all set to - 'x' so internal renumbering between saves doesn't affect the - ordering. - """ - def anon(rel): - return re.sub(r' Id="[^"]+" ', r' Id="x" ', rel) - - lines = self.xml.split('\n') - relationships = [l for l in lines if l.startswith(' =61.0.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "opc-diag" +version = "1.1.0" +authors = [{name = "Steve Canny", email = "stcanny@gmail.com"}] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Office/Business :: Office Suites", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "lxml>=4", + "opc-diag", +] +description = "A command-line application for exploring Microsoft Word, Excel, and PowerPoint files from Office 2007 and later." +keywords = ["docx", "pptx", "xlsx", "office", "openxml", "word", "powerpoint", "excel"] +license = { text = "MIT" } +readme = "README.md" +requires-python = ">=3.9" + +[project.scripts] +opc = "opcdiag.cli:main" + +[project.urls] +Changelog = "https://github.com//opc-diag/blob/master/HISTORY.rst" +Documentation = "https://opc-diag.readthedocs.org/en/latest/" +Homepage = "https://github.com/python-openxml/opc-diag" +Repository = "https://github.com/python-openxml/opc-diag" + +[tool.pyright] +include = ["src/opcdiag", "tests"] +pythonPlatform = "All" +pythonVersion = "3.9" +reportImportCycles = true +reportUnnecessaryCast = true +reportUnnecessaryTypeIgnoreComment = true +stubPath = "./typings" +typeCheckingMode = "strict" +verboseOutput = true + +[tool.pytest.ini_options] +filterwarnings = [ + # -- exit on any warning not explicitly ignored here -- + "error", + + # -- pytest-xdist plugin may warn about `looponfailroots` deprecation -- + "ignore::DeprecationWarning:xdist", + + # -- pytest complains when pytest-xdist is not installed -- + "ignore:Unknown config option. looponfailroots:pytest.PytestConfigWarning", +] +looponfailroots = ["src", "tests"] +norecursedirs = [ + ".git", + ".ruff_cache", + "dist", + "docs", + "features", + "src", + "typings", + ".tox", +] +python_files = ["test_*.py"] +python_classes = ["Test", "Describe"] +python_functions = ["test_", "it_", "its_", "they_", "and_", "but_"] + +[tool.ruff] +exclude = [] +line-length = 100 +target-version = "py39" + +[tool.ruff.lint] +ignore = [ + "COM812", # -- over-aggressively insists on trailing commas where not desired -- + "PT001", # -- wants @pytest.fixture() instead of @pytest.fixture -- + "PT005", # -- wants @pytest.fixture() instead of @pytest.fixture -- +] +select = [ + "C4", # -- flake8-comprehensions -- + "COM", # -- flake8-commas -- + "E", # -- pycodestyle errors -- + "F", # -- pyflakes -- + "I", # -- isort (imports) -- + "PLR0402", # -- Name compared with itself like `foo == foo` -- + "PT", # -- flake8-pytest-style -- + "SIM", # -- flake8-simplify -- + "UP015", # -- redundant `open()` mode parameter (like "r" is default) -- + "UP018", # -- Unnecessary {literal_type} call like `str("abc")`. (rewrite as a literal) -- + "UP032", # -- Use f-string instead of `.format()` call -- + "UP034", # -- Avoid extraneous parentheses -- +] + +[tool.ruff.lint.isort] +known-first-party = ["opcdiag"] +known-local-folder = ["helpers"] + +[tool.uv] +dev-dependencies = [ + "alabaster<0.7.14", + "behave>=1.2.6", + "jinja2==2.11.3", + "markupsafe==0.23", + "pyright>=1.1.381", + "pytest-cov>=5.0.0", + "pytest>=8.3.3", + "ruff>=0.6.7", + "sphinx==1.8.6", + "tox>=4.20.0", + "twine>=5.1.1", + "types-lxml>=2024.9.16", +] + +[tool.uv.sources] +opc-diag = { workspace = true } diff --git a/setup.py b/setup.py deleted file mode 100644 index bdb4804..0000000 --- a/setup.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python - -import codecs -import os -import re -import sys - -from ez_setup import use_setuptools -use_setuptools() - -from setuptools import setup - -MAIN_PKG = 'opcdiag' - - -def _read_from_file(relpath): - """ - Return the text contained in the file at *relpath* as unicode. - """ - thisdir = os.path.dirname(__file__) - path = os.path.join(thisdir, relpath) - with codecs.open(path, encoding='utf8') as f: - text = f.read() - return text - - -NAME = 'opc-diag' - -DESCRIPTION = ( - 'Browse and diff Microsoft Office .docx, .xlsx, and .pptx files.' -) - -KEYWORDS = 'opc open xml diff docx pptx xslx office' -AUTHOR = 'Steve Canny' -AUTHOR_EMAIL = 'python-opc@googlegroups.com' -URL = 'https://github.com/python-openxml/opc-diag' - -MODULES = ['ez_setup'] -PACKAGES = [MAIN_PKG] - -ENTRY_POINTS = {'console_scripts': ['opc = opcdiag.cli:main']} - -INSTALL_REQUIRES = ['lxml >= 3.0'] -if sys.hexversion < 0x02070000: # argparse only included from Python 2.7 - INSTALL_REQUIRES.append('argparse >= 1.2') - -TESTS_REQUIRE = [ - 'behave >= 1.2.3', - 'mock >= 1.0.1', - 'pytest >= 2.3.4' -] - -CLASSIFIERS = [ - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Topic :: Office/Business :: Office Suites', - 'Topic :: Software Development :: Libraries' -] - - -# --------------------------------------------------------------------------- -# Everything below is calculated and shouldn't normally need editing -# --------------------------------------------------------------------------- - -# read version from main package __init__.py -init_py = _read_from_file(os.path.join(MAIN_PKG, '__init__.py')) -VERSION = re.search("__version__ = '([^']+)'", init_py).group(1) - -# license is documented in LICENSE file -LICENSE = _read_from_file('LICENSE') - -# compile PyPI page text from README and HISTORY -read_me = _read_from_file('README.rst') -history = _read_from_file('HISTORY.rst') -LONG_DESCRIPTION = '%s\n\n%s' % (read_me, history) - -TEST_SUITE = 'tests' - -params = { - 'name': NAME, - 'version': VERSION, - 'description': DESCRIPTION, - 'keywords': KEYWORDS, - 'long_description': LONG_DESCRIPTION, - 'author': AUTHOR, - 'author_email': AUTHOR_EMAIL, - 'url': URL, - 'license': LICENSE, - 'packages': PACKAGES, - 'py_modules': MODULES, - 'entry_points': ENTRY_POINTS, - 'install_requires': INSTALL_REQUIRES, - 'tests_require': TESTS_REQUIRE, - 'test_suite': TEST_SUITE, - 'classifiers': CLASSIFIERS, -} - -setup(**params) diff --git a/src/opcdiag/__init__.py b/src/opcdiag/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/opcdiag/cli.py b/src/opcdiag/cli.py new file mode 100644 index 0000000..0b46881 --- /dev/null +++ b/src/opcdiag/cli.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python + +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +import abc +import argparse +import os +import sys + +from opcdiag.controller import OpcController + + +class CommandController: + """Orchestrates processing of commands in the form of a list of arguments (*argv*). + + A new instance is created using the :meth:`new` staticmethod. Once instantiated, it can + process any number of commands by calling its :meth:`execute` method, once for each command. + """ + + def __init__(self, parser: argparse.ArgumentParser, app_controller: OpcController): + self._parser = parser + self._app_controller = app_controller + + @staticmethod + def new(): + """A newly created instance of |CommandController|. + + The instance is fitted with a fully configured parser and an instance of the application + controller to dispatch parsed commands to. + """ + parser = Command.parser() + app_controller = OpcController() + return CommandController(parser, app_controller) + + def execute(self, argv: list[str] | None = None): + """Interpret the command indicated by the arguments in *argv* and execute it. + + If *argv* is |None|, ``sys.argv`` is used. + """ + # print help and exit if no args + arg_count = len(argv if argv else sys.argv) + if arg_count < 2: + self._parser.print_help() + sys.exit(1) + + args = self._parser.parse_args(argv) + command = args.command + command.validate(args) + command.execute(args, self._app_controller) + + +class Command(abc.ABC): + """Base class for sub-commands.""" + + def __init__(self, parser: argparse.ArgumentParser): + super(Command, self).__init__() + self._parser = parser + + @staticmethod + @abc.abstractmethod + def add_command_parser_to( + subparsers: argparse._SubParsersAction[argparse.ArgumentParser], + ) -> argparse.ArgumentParser: ... + + @staticmethod + def parser(): + """ + Return an instance of :class:`argparse.ArgumentParser` configured + with a subcommand parser for each of the commands that are a subclass + of |Command|. + """ + desc = "Browse and diff Microsoft Office .docx, .xlsx, and .pptx files." + epilog = "'opc --help' lists command-specific help" + parser = argparse.ArgumentParser(prog="opc", description=desc, epilog=epilog) + subparsers = parser.add_subparsers(title="available commands") + for command_cls in Command.__subclasses__(): + command_parser = command_cls.add_command_parser_to(subparsers) + command = command_cls(command_parser) + command_parser.set_defaults(command=command) + return parser + + @abc.abstractmethod + def execute(self, args: argparse.Namespace, app_controller: OpcController) -> None: ... + + @abc.abstractmethod + def validate(self, args: argparse.Namespace) -> None: ... + + +class BrowseCommand(Command): + """Implements the `browse` sub-command.""" + + @staticmethod + def add_command_parser_to(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]): + parser = subparsers.add_parser( + "browse", help="List pretty-printed XML for a specified package part" + ) + parser.add_argument("pkg_path", metavar="PKG_PATH", help="Path to OPC package file") + parser.add_argument( + "filename", + metavar="FILENAME", + help="Filename portion of the pack URI for the part to browse", + ) + return parser + + def execute(self, args: argparse.Namespace, app_controller: OpcController): + app_controller.browse(args.pkg_path, args.filename) + + def validate(self, args: argparse.Namespace): + try: + msg = "PKG_PATH '%s' does not exist" % args.pkg_path + assert os.path.exists(args.pkg_path), msg + except AssertionError as e: + self._parser.error(str(e)) + + +class DiffCommand(Command): + """Implements the `diff` sub-command.""" + + @staticmethod + def add_command_parser_to(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]): + parser = subparsers.add_parser( + "diff", help="Show differences between two OPC package files" + ) + parser.add_argument("pkg_1_path", metavar="PKG_1_PATH", help="first package to compare") + parser.add_argument("pkg_2_path", metavar="PKG_2_PATH", help="second package to compare") + return parser + + def execute(self, args: argparse.Namespace, app_controller: OpcController): + app_controller.diff_pkg(args.pkg_1_path, args.pkg_2_path) + + def validate(self, args: argparse.Namespace): + paths_that_should_exist = ( + (args.pkg_1_path, "PKG_1_PATH"), + (args.pkg_2_path, "PKG_2_PATH"), + ) + try: + for path, metavar in paths_that_should_exist: + msg = "%s '%s' does not exist" % (metavar, path) + assert os.path.exists(path), msg + except AssertionError as e: + self._parser.error(str(e)) + + +class DiffItemCommand(Command): + """Implements the `diff-item` sub-command.""" + + @staticmethod + def add_command_parser_to(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]): + parser = subparsers.add_parser( + "diff-item", + help="Show differences between a specified item in two OPC " "package files", + ) + parser.add_argument("pkg_1_path", metavar="PKG_1_PATH", help="first package") + parser.add_argument("pkg_2_path", metavar="PKG_2_PATH", help="second package") + parser.add_argument( + "filename", + metavar="FILENAME", + help="Filename portion of pack URI for item to browse", + ) + return parser + + def execute(self, args: argparse.Namespace, app_controller: OpcController): + app_controller.diff_item(args.pkg_1_path, args.pkg_2_path, args.filename) + + def validate(self, args: argparse.Namespace): + paths_that_should_exist = ( + (args.pkg_1_path, "PKG_1_PATH"), + (args.pkg_2_path, "PKG_2_PATH"), + ) + try: + for path, metavar in paths_that_should_exist: + msg = "%s '%s' does not exist" % (metavar, path) + assert os.path.exists(path), msg + except AssertionError as e: + self._parser.error(str(e)) + + +class ExtractCommand(Command): + def __init__(self, parser: argparse.ArgumentParser): + super(ExtractCommand, self).__init__(parser) + + @staticmethod + def add_command_parser_to(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]): + parser = subparsers.add_parser( + "extract", help="Extract all items in a package to a directory" + ) + parser.add_argument("pkg_path", metavar="PKG_PATH", help="Path to package") + parser.add_argument( + "dirpath", + metavar="DIRPATH", + help="Path to directory into which to extract package items", + ) + return parser + + def validate(self, args: argparse.Namespace): + try: + msg = "PKG_PATH '%s' does not exist" % args.pkg_path + assert os.path.exists(args.pkg_path), msg + except AssertionError as e: + self._parser.error(str(e)) + + def execute(self, args: argparse.Namespace, app_controller: OpcController): + app_controller.extract_package(args.pkg_path, args.dirpath) + + +class RepackageCommand(Command): + def __init__(self, parser: argparse.ArgumentParser): + super(RepackageCommand, self).__init__(parser) + + @staticmethod + def add_command_parser_to(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]): + parser = subparsers.add_parser( + "repackage", help="Build an OPC package from the contents of a directory" + ) + parser.add_argument( + "dirpath", + metavar="DIRPATH", + help="Directory containing expanded package files", + ) + parser.add_argument( + "new_package", + metavar="NEW_PACKAGE", + help="Path at which to save new package file", + ) + return parser + + def validate(self, args: argparse.Namespace): + try: + msg = "DIRPATH '%s' not found or not a directory" % args.dirpath + assert os.path.isdir(args.dirpath), msg + except AssertionError as e: + self._parser.error(str(e)) + + def execute(self, args: argparse.Namespace, app_controller: OpcController): + app_controller.repackage(args.dirpath, args.new_package) + + +class SubstituteCommand(Command): + def __init__(self, parser: argparse.ArgumentParser): + super(SubstituteCommand, self).__init__(parser) + + @staticmethod + def add_command_parser_to(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]): + parser = subparsers.add_parser( + "substitute", help="Substitute a part from one package into another" + ) + parser.add_argument( + "filename", + metavar="FILENAME", + help="Filename portion of partname for part to substitute", + ) + parser.add_argument( + "src_pkg_path", + metavar="SRC_PKG_PATH", + help="package from which to source part identified by FILENAME", + ) + parser.add_argument( + "tgt_pkg_path", + metavar="TGT_PKG_PATH", + help="package from which to get all remaining parts", + ) + parser.add_argument( + "result_pkg_path", + metavar="RESULT_PKG_PATH", + help="path at which to store resulting package file", + ) + return parser + + def validate(self, args: argparse.Namespace): + paths_that_should_exist = ( + (args.src_pkg_path, "SRC_PKG_PATH"), + (args.tgt_pkg_path, "TGT_PKG_PATH"), + ) + try: + for path, metavar in paths_that_should_exist: + msg = "%s '%s' does not exist" % (metavar, path) + assert os.path.exists(path), msg + except AssertionError as e: + self._parser.error(str(e)) + + def execute(self, args: argparse.Namespace, app_controller: OpcController): + app_controller.substitute( + args.filename, args.src_pkg_path, args.tgt_pkg_path, args.result_pkg_path + ) + + +def main(argv: list[str] | None = None): + command_controller = CommandController.new() + command_controller.execute(argv) diff --git a/opcdiag/controller.py b/src/opcdiag/controller.py similarity index 70% rename from opcdiag/controller.py rename to src/opcdiag/controller.py index 40f4bd2..1073e93 100644 --- a/opcdiag/controller.py +++ b/src/opcdiag/controller.py @@ -1,35 +1,29 @@ -# -*- coding: utf-8 -*- +"""Command-line application controller for opc-diag.""" -""" -Command-line application controller for opc-diag -""" +from __future__ import annotations from opcdiag.model import Package from opcdiag.presenter import DiffPresenter, ItemPresenter from opcdiag.view import OpcView +_CONTENT_TYPES_URI = "[Content_Types].xml" -_CONTENT_TYPES_URI = '[Content_Types].xml' +class OpcController: + """Mediate between the command-line interface and the package model entities. -class OpcController(object): + Orchestrates the execution of user commands by creating entity objects, delegating work to + them, and using the appropriate view object to format the results to be displayed. """ - Mediate between the command-line interface and the package model - entities, orchestrating the execution of user commands by creating - entity objects, delegating work to them, and using the appropriate view - object to format the results to be displayed. - """ - def browse(self, pkg_path, uri_tail): - """ - Display pretty-printed XML contained in package item with URI ending - with *uri_tail* in package at *pkg_path*. - """ + + def browse(self, pkg_path: str, uri_tail: str): + """Display pretty-printed XML of part with *uri_tail* in package at `pkg_path`.""" pkg = Package.read(pkg_path) pkg_item = pkg.find_item_by_uri_tail(uri_tail) item_presenter = ItemPresenter(pkg_item) OpcView.pkg_item(item_presenter) - def diff_item(self, package_1_path, package_2_path, uri_tail): + def diff_item(self, package_1_path: str, package_2_path: str, uri_tail: str): """ Display the meaningful differences between the item identified by *uri_tail* in the package at *package_1_path* and its counterpart in @@ -42,7 +36,7 @@ def diff_item(self, package_1_path, package_2_path, uri_tail): diff = DiffPresenter.named_item_diff(package_1, package_2, uri_tail) OpcView.item_diff(diff) - def diff_pkg(self, package_1_path, package_2_path): + def diff_pkg(self, package_1_path: str, package_2_path: str): """ Display the meaningful differences between the packages at *package_1_path* and *package_2_path*. Each path can be either a @@ -51,13 +45,12 @@ def diff_pkg(self, package_1_path, package_2_path): """ package_1 = Package.read(package_1_path) package_2 = Package.read(package_2_path) - content_types_diff = DiffPresenter.named_item_diff( - package_1, package_2, _CONTENT_TYPES_URI) + content_types_diff = DiffPresenter.named_item_diff(package_1, package_2, _CONTENT_TYPES_URI) rels_diffs = DiffPresenter.rels_diffs(package_1, package_2) xml_part_diffs = DiffPresenter.xml_part_diffs(package_1, package_2) OpcView.package_diff(content_types_diff, rels_diffs, xml_part_diffs) - def extract_package(self, package_path, extract_dirpath): + def extract_package(self, package_path: str, extract_dirpath: str): """ Extract the contents of the package at *package_path* to individual files in a directory at *extract_dirpath*. @@ -66,7 +59,7 @@ def extract_package(self, package_path, extract_dirpath): package.prettify_xml() package.save_to_dir(extract_dirpath) - def repackage(self, package_path, new_package_path): + def repackage(self, package_path: str, new_package_path: str): """ Write the contents of the package found at *package_path* to a new zip package at *new_package_path*. @@ -74,7 +67,7 @@ def repackage(self, package_path, new_package_path): package = Package.read(package_path) package.save(new_package_path) - def substitute(self, uri_tail, src_pkg_path, tgt_pkg_path, new_pkg_path): + def substitute(self, uri_tail: str, src_pkg_path: str, tgt_pkg_path: str, new_pkg_path: str): """ Substitute the package item identified by *uri_tail* from the package at *src_pkg_path* into the package at *tgt_pkg_path* and save the @@ -87,5 +80,4 @@ def substitute(self, uri_tail, src_pkg_path, tgt_pkg_path, new_pkg_path): pkg_item = package_1.find_item_by_uri_tail(uri_tail) package_2.substitute_item(pkg_item) package_2.save(new_pkg_path) - OpcView.substitute(pkg_item.uri, src_pkg_path, tgt_pkg_path, - new_pkg_path) + OpcView.substitute(pkg_item.uri, src_pkg_path, tgt_pkg_path, new_pkg_path) diff --git a/src/opcdiag/model.py b/src/opcdiag/model.py new file mode 100644 index 0000000..cae8f44 --- /dev/null +++ b/src/opcdiag/model.py @@ -0,0 +1,205 @@ +"""Package and package items model.""" + +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +import os +from typing import Mapping, Protocol + +from lxml import etree + +from opcdiag.phys_pkg import BlobCollection, PhysPkg + +_CONTENT_TYPES_URI = "[Content_Types].xml" + +# ================================================================================================ +# DOMAIN MODEL +# ================================================================================================ + + +class PkgItemT(Protocol): + @property + def blob(self) -> bytes: ... + @blob.setter + def blob(self, value: bytes) -> None: ... + @property + def element(self) -> etree._Element: ... + @property + def is_content_types(self) -> bool: ... + @property + def is_rels_item(self) -> bool: ... + @property + def is_xml_part(self) -> bool: ... + @property + def path(self) -> str: ... + def prettify_xml(self) -> None: ... + @property + def uri(self) -> str: ... + + +# ================================================================================================ + + +class Package: + """Root of package graph and main model API class.""" + + def __init__(self, pkg_items: Mapping[str, PkgItemT]): + super(Package, self).__init__() + self._pkg_items = pkg_items + + @staticmethod + def read(path: str) -> Package: + """Factory method to construct a new |Package| instance from package at *path*. + + The package can be either a zip archive (e.g. .docx file) or a directory containing an + extracted package. + """ + phys_pkg = PhysPkg.read(path) + pkg_items = {uri: PkgItem(phys_pkg.root_uri, uri, blob) for uri, blob in phys_pkg} + return Package(pkg_items) + + def find_item_by_uri_tail(self, uri_tail: str) -> PkgItemT: + """ + Return the first item in this package having a uri that ends with + *uri_tail*. Raises |KeyError| if no matching item is found. + """ + for uri in self._uris: + if uri.endswith(uri_tail): + return self._pkg_items[uri] + raise KeyError("No item with name '%s'" % uri_tail) + + def prettify_xml(self): + """Reformat package XML content to human-readable format. + + All package items having XML content are mutated to a multi-line, indented + format. If viewed after this method is called, the XML appears "pretty printed". + """ + for pkg_item in self._pkg_items.values(): + pkg_item.prettify_xml() + + @property + def rels_items(self) -> list[PkgItemT]: + """Return list of rels items in this package, sorted by pack URI.""" + rels_items: list[PkgItemT] = [] + for uri in self._uris: + pkg_item = self._pkg_items[uri] + if pkg_item.is_rels_item: + rels_items.append(pkg_item) + return rels_items + + def save(self, path: str): + """Save this package to a zip archive at *path*.""" + PhysPkg.write_to_zip(self._blobs, path) + + def save_to_dir(self, dirpath: str): + """Save each of the items in this package as a file in a directory at *dirpath*. + + Uses the pack URI as the relative path of each file. If the directory exists, it is + deleted (recursively) before being recreated. + """ + PhysPkg.write_to_dir(self._blobs, dirpath) + + def substitute_item(self, src_pkg_item: PkgItemT): + """Replace corresponding pkg-item in this package with `src_pkg_item`. + + This allows a diagnotic procedure of subtituting a single package item with one from a + known working package to narrow down a problematic package part. + + `src_pkg_item` replaces the item with the same URI in this package. + """ + tgt_pkg_item = self._pkg_items[src_pkg_item.uri] + tgt_pkg_item.blob = src_pkg_item.blob + + @property + def xml_parts(self) -> list[PkgItemT]: + """Return list of XML parts in this package, sorted by partname.""" + return [ + pkg_item + for pkg_item in (self._pkg_items[uri] for uri in self._uris) + if pkg_item.is_xml_part + ] + + @property + def _blobs(self): + """ + A |BlobCollection| instance containing a snapshot of the blobs in the + package. + """ + blobs = BlobCollection() + for uri, pkg_item in self._pkg_items.items(): + blobs[uri] = pkg_item.blob + return blobs + + @property + def _uris(self): + """ + Return sorted list of item URIs in this package. + """ + return sorted(self._pkg_items.keys()) + + +class PkgItem: + """Individual item (file, roughly) within an OPC package.""" + + def __init__(self, root_uri: str, uri: str, blob: bytes): + self._blob = blob + self._root_uri = root_uri + self._uri = uri + + @property + def blob(self) -> bytes: + """The binary contents of this package item. + + Frequently but not always XML text. + """ + return self._blob + + @blob.setter + def blob(self, value: bytes): + self._blob = value + + @property + def element(self) -> etree._Element: + """Return an lxml.etree Element obtained by parsing the XML in this item's blob.""" + element = etree.fromstring(self._blob) + # -- this handles some odd cases where the XML was hand edited and some whitespace + # -- tail-text was left. + etree.indent(element) + return element + + @property + def is_content_types(self) -> bool: + """True if this item is the ``[Content_Types].xml`` item in the package.""" + return self._uri == _CONTENT_TYPES_URI + + @property + def is_rels_item(self) -> bool: + """True if this item is a relationships item, i.e. its uri ends with `.rels`.""" + return self._uri.endswith(".rels") + + @property + def is_xml_part(self) -> bool: + """True if the URI of this item ends with '.xml', except if it is the content types item.""" + return self._uri.endswith(".xml") and not self.is_content_types + + @property + def path(self) -> str: + """Path of this item as though it were extracted into a directory at its package path.""" + uri_part = os.path.normpath(self._uri) + return os.path.join(self._root_uri, uri_part) + + def prettify_xml(self) -> None: + """Reformat XML in this package item to indented, human-readable form. + + Does nothing if this package item does not contain XML. + """ + if self.is_content_types or self.is_xml_part or self.is_rels_item: + self._blob = etree.tostring( + self.element, encoding="UTF-8", standalone=True, pretty_print=True + ) + + @property + def uri(self) -> str: + """The pack URI of this package item, e.g. `'/word/document.xml'`.""" + return self._uri # pragma: no cover diff --git a/src/opcdiag/phys_pkg.py b/src/opcdiag/phys_pkg.py new file mode 100644 index 0000000..91cac03 --- /dev/null +++ b/src/opcdiag/phys_pkg.py @@ -0,0 +1,156 @@ +"""Interface to a physical OPC package, either a zip archive or directory.""" + +from __future__ import annotations + +import os +import shutil +from typing import Iterator +from zipfile import ZIP_DEFLATED, ZipFile + + +class BlobCollection(dict[str, bytes]): + """Structures a set of blobs, like a set of files in an OPC package. + + It can add and retrieve items by URI (relative path, roughly) and can also retrieve items by + uri_tail, the trailing portion of the URI. + """ + + +class PhysPkg: + """Provides read and write services for packages on the filesystem. + + Suitable for use with OPC packages in either Zip or expanded directory form. |PhysPkg| objects + are iterable, generating a (uri, blob) 2-tuple for each item in the package. + """ + + def __init__(self, blobs: dict[str, bytes], root_uri: str): + super(PhysPkg, self).__init__() + self._blobs = blobs + self._root_uri = root_uri + + def __iter__(self) -> Iterator[tuple[str, bytes]]: + """Generate a (uri, blob) 2-tuple for each of the items in the package.""" + return iter(self._blobs.items()) + + @classmethod + def read(cls, path: str, /): + """Return a |PhysPkg| instance loaded with contents of OPC package at *path*. + + *path* can be either a regular zip package or a directory containing an expanded package. + """ + if os.path.isdir(path): + return DirPhysPkg.read(path) + else: + return ZipPhysPkg.read(path) + + @property + def root_uri(self) -> str: + return self._root_uri # pragma: no cover + + @staticmethod + def write_to_dir(blobs: BlobCollection, dirpath: str): + """Write the contents of the |BlobCollection| instance *blobs* to a directory at *dirpath*. + + If a directory already exists at *dirpath*, it is deleted before being recreated. If a + file exists at *dirpath*, |ValueError| is raised, to prevent unintentional overwriting. + """ + PhysPkg._clear_or_make_dir(dirpath) + for uri, blob in blobs.items(): + PhysPkg._write_blob_to_dir(dirpath, uri, blob) + + @staticmethod + def write_to_zip(blobs: BlobCollection, pkg_zip_path: str): + """Write "files" in |BlobCollection| instance *blobs* to zip archive at *pkg_zip_path*.""" + zipf = ZipFile(pkg_zip_path, "w", ZIP_DEFLATED) + for uri in sorted(blobs.keys()): + blob = blobs[uri] + zipf.writestr(uri, blob) + zipf.close() + + @staticmethod + def _clear_or_make_dir(dirpath: str): + """Create a new, empty directory at *dirpath*. + + Removes and recreates any directory found there. Raises |ValueError| if *dirpath* exists + but is not a directory. + """ + # -- raise if *dirpath* is a file -- + if os.path.exists(dirpath) and not os.path.isdir(dirpath): + tmpl = "target path '%s' is not a directory" + raise ValueError(tmpl % dirpath) + # -- remove any existing directory tree at *dirpath* -- + if os.path.exists(dirpath): + shutil.rmtree(dirpath) + # -- create dir at dirpath, as well as any intermediate-level dirs -- + os.makedirs(dirpath) + + @staticmethod + def _write_blob_to_dir(dirpath: str, uri: str, blob: bytes): + """Write *blob* to a file under *dirpath*. + + The segments of *uri* that precede the filename are created, as required, as intermediate + directories. + """ + # -- In general, uri will contain forward slashes as segment separators. + # -- This next line converts them to backslashes on Windows. + item_relpath = os.path.normpath(uri) + fullpath = os.path.join(dirpath, item_relpath) + dirpath, _ = os.path.split(fullpath) + if not os.path.exists(dirpath): + os.makedirs(dirpath) + with open(fullpath, "wb") as f: + f.write(blob) + + +class DirPhysPkg(PhysPkg): + """An OPC physical package that has been expanded into individual files in a directory. + + The directory structure mirrors the pack URIs. + """ + + def __init__(self, blobs: dict[str, bytes], root_uri: str): + super(DirPhysPkg, self).__init__(blobs, root_uri) + + @classmethod + def read(cls, pkg_dir: str): + """Return a |BlobCollection| instance loaded from *pkg_dir*.""" + blobs = BlobCollection() + pfx_len = len(pkg_dir) + 1 + for filepath in cls._filepaths_in_dir(pkg_dir): + uri = filepath[pfx_len:].replace("\\", "/") + with open(filepath, "rb") as f: + blob = f.read() + blobs[uri] = blob + root_uri = pkg_dir + return cls(blobs, root_uri) + + @staticmethod + def _filepaths_in_dir(dirpath: str) -> list[str]: + """A sorted list of relative paths, one for each of the files under *dirpath*. + + Recursively visits all subdirectories. + """ + file_paths = [ + os.path.join(root, filename) + for root, _, filenames in os.walk(dirpath) + for filename in filenames + ] + return sorted(file_paths) + + +class ZipPhysPkg(PhysPkg): + """An OPC physical package in the typically encountered form, a zip archive.""" + + def __init__(self, blobs: dict[str, bytes], root_uri: str): + super(ZipPhysPkg, self).__init__(blobs, root_uri) + + @classmethod + def read(cls, pkg_zip_path: str): + """Return a |BlobCollection| instance loaded from *pkg_zip_path*.""" + blobs = BlobCollection() + zipf = ZipFile(pkg_zip_path, "r") + for name in zipf.namelist(): + blobs[name] = zipf.read(name) + zipf.close() + root_uri = os.path.splitext(pkg_zip_path)[0] + return cls(blobs, root_uri) diff --git a/src/opcdiag/presenter.py b/src/opcdiag/presenter.py new file mode 100644 index 0000000..7c6a343 --- /dev/null +++ b/src/opcdiag/presenter.py @@ -0,0 +1,259 @@ +"""Presenter classes for opc-diag model classes.""" + +from __future__ import annotations + +import re +from difflib import unified_diff +from typing import TYPE_CHECKING, Sequence, cast + +from lxml import etree + +if TYPE_CHECKING: + from opcdiag.model import Package, PkgItemT + + +def diff(text_1: str, text_2: str, filename_1: str, filename_2: str): + """Return a ``diff`` style unified diff listing between *text_1* and *text_2*.""" + lines_1 = text_1.split("\n") + lines_2 = text_2.split("\n") + diff_lines = unified_diff(lines_1, lines_2, filename_1, filename_2) + # this next bit is needed to work around Python 2.6 difflib bug that left + # in a trailing space after the filename if no date was provided. The + # filename lines look like: '--- filename \n' where regular lines look + # like '+foobar'. The space needs to be removed but the linefeed preserved. + trimmed_lines: list[str] = [] + for line in diff_lines: + if line.endswith(" \n"): + line = "%s\n" % line.rstrip() + trimmed_lines.append(line) + return "\n".join(trimmed_lines) + + +def prettify_nsdecls(xml: str): + """Wrap and indent attributes on the root element. + + This avoids namespace declarations running off the page in the text editor such that they can + be more easily inspected. Attributes are sorted such that the default namespace, if present, + appears first in the list, followed by other namespace declarations, and then remaining + attributes, both in alphabetical order. + """ + + def parse_attrs(rootline: str): + """ + Return 3-tuple (head, attributes, tail) looking like + (''). + """ + attr_re = re.compile(r'([-a-zA-Z0-9_:.]+="[^"]*" *)') + substrs = [substr.strip() for substr in attr_re.split(rootline) if substr] + head = substrs[0] + attrs, tail = (substrs[1:-1], substrs[-1]) if len(substrs) > 1 else ([], "") + return (head, attrs, tail) + + def sequence_attrs(attributes: Sequence[str]): + """Sort attributes alphabetically within subgroups. + + The subgroupings are default namespace declaration, other namespace declarations, and + other attributes. + """ + def_nsdecls: list[str] = [] + nsdecls: list[str] = [] + attrs: list[str] = [] + for attr in attributes: + if attr.startswith("xmlns="): + def_nsdecls.append(attr) + elif attr.startswith("xmlns:"): + nsdecls.append(attr) + else: + attrs.append(attr) + return sorted(def_nsdecls) + sorted(nsdecls) + sorted(attrs) + + def pretty_rootline(head: str, attrs: Sequence[str], tail: str): + """Return string containing prettified XML root line. + + *head* appears on the first line, *attrs* indented on following lines, and *tail* indented + on the last line. + """ + indent = 4 * " " + newrootline = head + for attr in attrs: + newrootline += "\n%s%s" % (indent, attr) + newrootline += "\n%s%s" % (indent, tail) if tail else "" + return newrootline + + lines = xml.splitlines() + rootline = lines[1] + head, attributes, tail = parse_attrs(rootline) + attributes = sequence_attrs(attributes) + lines[1] = pretty_rootline(head, attributes, tail) + return "\n".join(lines) + + +class DiffPresenter: + """Forms diffs between packages and their elements.""" + + @staticmethod + def named_item_diff(package_1: Package, package_2: Package, uri_tail: str): + """Return a diff between the corresponding text of two packages. + + The text item is identified by *uri_tail*, and the version in *package_1* is compared with + its counterpart in *package_2*. + """ + pkg_item_1 = package_1.find_item_by_uri_tail(uri_tail) + pkg_item_2 = package_2.find_item_by_uri_tail(uri_tail) + return DiffPresenter._pkg_item_diff(pkg_item_1, pkg_item_2) + + @staticmethod + def rels_diffs(package_1: Package, package_2: Package): + """Return a list of diffs between the rels items in *package_1* and *package_2*. + + Rels items are compared in alphabetical order by pack URI. + """ + package_1_rels_items = package_1.rels_items + return DiffPresenter._pkg_item_diffs(package_1_rels_items, package_2) + + @staticmethod + def xml_part_diffs(package_1: Package, package_2: Package): + """ + Return a list of diffs between the XML parts in *package_1* and their + counterpart in *package_2*. Parts are compared in alphabetical order + by partname (pack URI). + """ + package_1_xml_parts = package_1.xml_parts + return DiffPresenter._pkg_item_diffs(package_1_xml_parts, package_2) + + @staticmethod + def _pkg_item_diff(pkg_item_1: PkgItemT, pkg_item_2: PkgItemT): + """Return a diff between the text of *pkg_item_1* and that of *pkg_item_2*.""" + item_presenter_1 = ItemPresenter(pkg_item_1) + item_presenter_2 = ItemPresenter(pkg_item_2) + text_1 = item_presenter_1.text + text_2 = item_presenter_2.text + filename_1 = item_presenter_1.filename + filename_2 = item_presenter_2.filename + return diff(text_1, text_2, filename_1, filename_2) + + @staticmethod + def _pkg_item_diffs(pkg_items: list[PkgItemT], package_2: Package): + """Return a list of diffs. + + There is one diff for each item in *pkg_items* that differs from its counterpart in + *package_2*. + """ + diffs: list[str] = [] + for pkg_item in pkg_items: + uri = pkg_item.uri + pkg_item_2 = package_2.find_item_by_uri_tail(uri) + diff = DiffPresenter._pkg_item_diff(pkg_item, pkg_item_2) + if diff: + diffs.append(diff) + return diffs + + +class ItemPresenter: + """Base class and factory class for package item presenter classes. + + Also serves as presenter for binary classes, e.g. .bin and .jpg. + """ + + def __new__(cls, pkg_item: PkgItemT): + """Factory for package item presenter objects. + + Chooses one of |ContentTypes|, |RelsItem|, or |Part| based on the characteristics of + `pkg_item`. + """ + if pkg_item.is_content_types: + presenter_class = ContentTypesPresenter + elif pkg_item.is_rels_item: + presenter_class = RelsItemPresenter + elif pkg_item.is_xml_part: + presenter_class = XmlPartPresenter + else: + presenter_class = ItemPresenter + return super(ItemPresenter, cls).__new__(cast(type, presenter_class)) + + def __init__(self, pkg_item: PkgItemT): + super(ItemPresenter, self).__init__() + self._pkg_item = pkg_item + + @property + def filename(self) -> str: + """Effective path for this package item. + + Normalized to always use forward slashes as the path separator. + """ + return self._pkg_item.path.replace("\\", "/") + + @property + def text(self) -> str: + """ + Raise |NotImplementedError|; all subclasses must implement a ``text`` + property, returning a text representation of the package item, + generally a formatted version of the item contents. + """ + msg = "'.text' property must be implemented by all subclasses of It" "emPresenter" + raise NotImplementedError(msg) + + @property + def xml(self): + """ + Return pretty-printed XML (as unicode text) from this package item's + blob. + """ + xml_bytes = etree.tostring( + self._pkg_item.element, encoding="UTF-8", pretty_print=True, standalone=True + ).strip() + xml_text = xml_bytes.decode("utf-8") + return xml_text + + +class ContentTypesPresenter(ItemPresenter): + """Presenter for the `[Content_Types].xml` part.""" + + @property + def text(self): + """Return the XML for this content types item formatted for minimal diffs. + + The and child elements are sorted to remove arbitrary ordering + between package saves. + """ + lines = self.xml.split("\n") + defaults = sorted([ln for ln in lines if ln.startswith(" XML for this rels item formatted for minimal diffs. + + The child elements are sorted to remove arbitrary ordering between package + saves. rId values are all set to 'x' so internal renumbering between saves doesn't affect + the ordering. + """ + + def anon(rel: str): + return re.sub(r' Id="[^"]+" ', r' Id="x" ', rel) + + lines = self.xml.split("\n") + relationships = [ln for ln in lines if ln.startswith(" foobar" + pkg_item = PkgItem("", "", blob) assert isinstance(pkg_item.element, etree._Element) def it_can_calculate_its_effective_path(self): - pkg_item = PkgItem('root_uri', 'uri', None) - expected_path = ('root_uri\\uri' if sys.platform.startswith('win') - else 'root_uri/uri') + pkg_item = PkgItem("root_uri", "uri", b"") + expected_path = "root_uri\\uri" if sys.platform.startswith("win") else "root_uri/uri" assert pkg_item.path == expected_path def it_can_prettify_its_xml(self): - blob = '' - pkg_item = PkgItem(None, 'foo.xml', blob) + blob = b"" + pkg_item = PkgItem("", "foo.xml", blob) pkg_item.prettify_xml() assert pkg_item.blob == ( - '\n' - '\n' - ' \n' - '\n' + b"\n" + b"\n" + b" \n" + b"\n" ) diff --git a/tests/test_phys_pkg.py b/tests/test_phys_pkg.py index aadef95..fce441d 100644 --- a/tests/test_phys_pkg.py +++ b/tests/test_phys_pkg.py @@ -1,78 +1,67 @@ -# -*- coding: utf-8 -*- -# -# test_phys_pkg.py -# -# Copyright (C) 2013 Steve Canny scanny@cisco.com -# -# This module is part of opc-diag and is released under the MIT License: -# http://www.opensource.org/licenses/mit-license.php +"""Unit tests for `opcdiag.phys_pkg` module.""" -"""Unit tests for phys_pkg module""" +# pyright: reportPrivateUsage=false import os import shutil - +from unittest.mock import call from zipfile import ZIP_DEFLATED, ZipFile -from opcdiag.phys_pkg import BlobCollection, DirPhysPkg, PhysPkg, ZipPhysPkg - import pytest -from mock import call - -from .unitutil import class_mock, instance_mock, relpath +from opcdiag.phys_pkg import BlobCollection, DirPhysPkg, PhysPkg, ZipPhysPkg +from .unitutil import FixtureRequest, Mock, class_mock, instance_mock, relpath -DIRPATH = 'DIRPATH' -DELETEME_DIR = relpath('test_files/deleteme') -FOOBAR_DIR = relpath('test_files/deleteme/foobar_dir') -FOOBAR_FILEPATH = relpath('test_files/deleteme/foobar_dir/foobar_file') -MINI_DIR_PKG_PATH = relpath('test_files/mini_pkg') -MINI_ZIP_PKG_PATH = relpath('test_files/mini_pkg.zip') -ROOT_URI = relpath('test_files/mini_pkg') +DIRPATH = "DIRPATH" +DELETEME_DIR = relpath("test-files/deleteme") +FOOBAR_DIR = relpath("test-files/deleteme/foobar_dir") +FOOBAR_FILEPATH = relpath("test-files/deleteme/foobar_dir/foobar_file") +MINI_DIR_PKG_PATH = relpath("test-files/mini_pkg") +MINI_ZIP_PKG_PATH = relpath("test-files/mini_pkg.zip") +ROOT_URI = relpath("test-files/mini_pkg") @pytest.fixture -def blob_(request): +def blob_(request: FixtureRequest): return instance_mock(str, request) @pytest.fixture -def blob_2_(request): +def blob_2_(request: FixtureRequest): return instance_mock(str, request) @pytest.fixture -def PhysPkg_(request): - PhysPkg_ = class_mock('opcdiag.phys_pkg.PhysPkg', request) +def PhysPkg_(request: FixtureRequest): + PhysPkg_ = class_mock("opcdiag.phys_pkg.PhysPkg", request) return PhysPkg_ @pytest.fixture -def uri_(request): +def uri_(request: FixtureRequest): return instance_mock(str, request) @pytest.fixture -def uri_2_(request): +def uri_2_(request: FixtureRequest): return instance_mock(str, request) @pytest.fixture -def ZipFile_(request, zip_file_): - ZipFile_ = class_mock('opcdiag.phys_pkg.ZipFile', request) +def ZipFile_(request: FixtureRequest, zip_file_: Mock): + ZipFile_ = class_mock("opcdiag.phys_pkg.ZipFile", request) ZipFile_.return_value = zip_file_ return ZipFile_ @pytest.fixture -def zip_file_(request): +def zip_file_(request: FixtureRequest): zip_file_ = instance_mock(ZipFile, request) return zip_file_ -class DescribePhysPkg(object): - +class DescribePhysPkg: def it_should_construct_the_appropriate_subclass(self): pkg = PhysPkg.read(MINI_ZIP_PKG_PATH) assert isinstance(pkg, ZipPhysPkg) @@ -81,37 +70,38 @@ def it_should_construct_the_appropriate_subclass(self): def it_can_iterate_over_pkg_blobs(self): # fixture ---------------------- - blobs = BlobCollection((('foo', 'bar'), ('baz', 'zam'))) - phys_pkg = PhysPkg(blobs, None) + blobs = BlobCollection((("foo", b"bar"), ("baz", b"zam"))) + phys_pkg = PhysPkg(blobs, "") # exercise --------------------- - actual_blobs = dict([item for item in phys_pkg]) + actual_blobs = dict(item for item in phys_pkg) # verify ----------------------- assert actual_blobs == blobs - def it_can_write_a_blob_collection_to_a_zip(self, tmpdir): + def it_can_write_a_blob_collection_to_a_zip(self, tmpdir: str): # fixture ---------------------- - uri, uri_2 = 'foo/bar.xml', 'foo/_rels/bar.xml.rels' - blob, blob_2 = b'blob', b'blob_2' - blobs_in = {uri: blob, uri_2: blob_2} - zip_path = str(tmpdir.join('foobar.xml')) + uri, uri_2 = "foo/bar.xml", "foo/_rels/bar.xml.rels" + blob, blob_2 = b"blob", b"blob_2" + blobs_in = BlobCollection(((uri, blob), (uri_2, blob_2))) + zip_path = str(tmpdir.join("foobar.xml")) # exercise --------------------- PhysPkg.write_to_zip(blobs_in, zip_path) # verify ----------------------- assert os.path.isfile(zip_path) - zipf = ZipFile(zip_path, 'r') - blobs_out = dict([(name, zipf.read(name)) for name in zipf.namelist()]) + zipf = ZipFile(zip_path, "r") + blobs_out = {name: zipf.read(name) for name in zipf.namelist()} zipf.close() assert blobs_out == blobs_in - def it_should_close_zip_file_after_use(self, ZipFile_, zip_file_): + def it_should_close_zip_file_after_use(self, ZipFile_: Mock, zip_file_: Mock): # exercise --------------------- - PhysPkg.write_to_zip({}, 'foobar') + PhysPkg.write_to_zip(BlobCollection(()), "foobar") # verify ----------------------- - ZipFile_.assert_called_once_with('foobar', 'w', ZIP_DEFLATED) + ZipFile_.assert_called_once_with("foobar", "w", ZIP_DEFLATED) zip_file_.close.assert_called_with() def it_can_write_a_blob_collection_to_a_directory( - self, uri_, uri_2_, blob_, blob_2_, PhysPkg_): + self, uri_: Mock, uri_2_: Mock, blob_: Mock, blob_2_: Mock, PhysPkg_: Mock + ): # fixture ---------------------- blobs = BlobCollection(((uri_, blob_), (uri_2_, blob_2_))) # exercise --------------------- @@ -119,25 +109,25 @@ def it_can_write_a_blob_collection_to_a_directory( # verify ----------------------- PhysPkg_._clear_or_make_dir.assert_called_once_with(DIRPATH) PhysPkg_._write_blob_to_dir.assert_has_calls( - (call(DIRPATH, uri_, blob_), call(DIRPATH, uri_2_, blob_2_)), - any_order=True) + (call(DIRPATH, uri_, blob_), call(DIRPATH, uri_2_, blob_2_)), any_order=True + ) def it_can_create_a_new_empty_directory(self): """Note: tests integration with filesystem""" # case: created if does not exist # ------------------------------ - if os.path.exists(DELETEME_DIR): + if os.path.exists(DELETEME_DIR): # pragma: no cover shutil.rmtree(DELETEME_DIR) PhysPkg._clear_or_make_dir(FOOBAR_DIR) assert os.path.exists(FOOBAR_DIR) # case: re-created if exists # ------------------------------ - if os.path.exists(DELETEME_DIR): + if os.path.exists(DELETEME_DIR): # pragma: no cover shutil.rmtree(DELETEME_DIR) os.makedirs(FOOBAR_DIR) - with open(FOOBAR_FILEPATH, 'w') as f: - f.write('foobar file') + with open(FOOBAR_FILEPATH, "w") as f: + f.write("foobar file") PhysPkg._clear_or_make_dir(FOOBAR_DIR) assert os.path.exists(FOOBAR_DIR) assert not os.path.exists(FOOBAR_FILEPATH) @@ -146,29 +136,28 @@ def it_can_create_a_new_empty_directory(self): # ------------------------------ shutil.rmtree(DELETEME_DIR) os.makedirs(DELETEME_DIR) - with open(FOOBAR_DIR, 'w') as f: - f.write('foobar file at FOOBAR_DIR path') - with pytest.raises(ValueError): + with open(FOOBAR_DIR, "w") as f: + f.write("foobar file at FOOBAR_DIR path") + with pytest.raises(ValueError, match="target path .* is not a directory"): PhysPkg._clear_or_make_dir(FOOBAR_DIR) - def it_can_write_a_blob_to_a_file_in_a_directory(self, tmpdir): + def it_can_write_a_blob_to_a_file_in_a_directory(self, tmpdir: str): """Note: tests integration with filesystem""" # fixture ---------------------- - uri = 'foo/bar.xml' - blob = b'blob' + uri = "foo/bar.xml" + blob = b"blob" dirpath = str(tmpdir) - filepath = os.path.join(dirpath, 'foo', 'bar.xml') + filepath = os.path.join(dirpath, "foo", "bar.xml") # exercise --------------------- PhysPkg._write_blob_to_dir(str(tmpdir), uri, blob) # verify ----------------------- assert os.path.isfile(filepath) - with open(filepath, 'rb') as f: + with open(filepath, "rb") as f: actual_blob = f.read() assert actual_blob == blob -class DescribeDirPhysPkg(object): - +class DescribeDirPhysPkg: def it_can_construct_from_a_filesystem_package(self): """ Note: integration test, allowing PhysPkg to hit the local filesystem. @@ -176,14 +165,13 @@ def it_can_construct_from_a_filesystem_package(self): # exercise --------------------- dir_phys_pkg = DirPhysPkg.read(MINI_DIR_PKG_PATH) # verify ----------------------- - expected_blobs = {'uri_1': b'blob_1\n', 'uri_2': b'blob_2\n'} + expected_blobs = {"uri_1": b"blob_1\n", "uri_2": b"blob_2\n"} assert dir_phys_pkg._blobs == expected_blobs assert dir_phys_pkg._root_uri == ROOT_URI assert isinstance(dir_phys_pkg, DirPhysPkg) -class DescribeZipPhysPkg(object): - +class DescribeZipPhysPkg: def it_can_construct_from_a_filesystem_package(self): """ Note: integration test, allowing PhysPkg to hit ZipFile on the local @@ -192,14 +180,14 @@ def it_can_construct_from_a_filesystem_package(self): # exercise --------------------- zip_phys_pkg = ZipPhysPkg.read(MINI_ZIP_PKG_PATH) # verify ----------------------- - expected_blobs = {'uri_1': b'blob_1\n', 'uri_2': b'blob_2\n'} + expected_blobs = {"uri_1": b"blob_1\n", "uri_2": b"blob_2\n"} assert zip_phys_pkg._blobs == expected_blobs assert zip_phys_pkg._root_uri == ROOT_URI assert isinstance(zip_phys_pkg, ZipPhysPkg) - def it_should_close_zip_file_after_use(self, ZipFile_, zip_file_): + def it_should_close_zip_file_after_use(self, ZipFile_: Mock, zip_file_: Mock): # exercise --------------------- PhysPkg.read(MINI_ZIP_PKG_PATH) # verify ----------------------- - ZipFile_.assert_called_once_with(MINI_ZIP_PKG_PATH, 'r') + ZipFile_.assert_called_once_with(MINI_ZIP_PKG_PATH, "r") zip_file_.close.assert_called_with() diff --git a/tests/test_presenter.py b/tests/test_presenter.py index 85e5086..c41f678 100644 --- a/tests/test_presenter.py +++ b/tests/test_presenter.py @@ -1,35 +1,302 @@ -# -*- coding: utf-8 -*- -# -# test_presenter.py -# -# Copyright (C) 2013 Steve Canny scanny@cisco.com -# -# This module is part of opc-diag and is released under the MIT License: -# http://www.opensource.org/licenses/mit-license.php +"""Unit tests for `opcdiag.presenter` module.""" -"""Unit tests for presenter module""" +# pyright: reportPrivateUsage=false from __future__ import unicode_literals -from lxml import etree - -from opcdiag.model import Package, PkgItem -from opcdiag.presenter import DiffPresenter, diff, ItemPresenter +from unittest.mock import PropertyMock, call import pytest +from lxml import etree -from mock import call, PropertyMock +from opcdiag.model import Package, PkgItem +from opcdiag.presenter import DiffPresenter, ItemPresenter, diff from .unitutil import ( - class_mock, function_mock, instance_mock, loose_mock, property_mock + FixtureRequest, + Mock, + class_mock, + function_mock, + instance_mock, + loose_mock, + property_mock, ) +URI_TAIL = "uri_tail" + + +class Describe_diff: + """Unit-test suite for `opcdiag.presenter.diff()` function.""" + + def it_calculates_a_diff_between_two_texts(self): + """Integrates with difflib""" + # fixture ---------------------- + text = "foobar\nnoobar\nzoobar" + text_2 = "foobar\ngoobar\nnoobar" + filename, filename_2 = "filename", "filename_2" + expected_diff_text = ( + "--- filename\n" + "\n" + "+++ filename_2\n" + "\n" + "@@ -1,3 +1,3 @@\n" + "\n" + " foobar\n" + "+goobar\n" + " noobar\n" + "-zoobar" + ) + # exercise --------------------- + diff_text = diff(text, text_2, filename, filename_2) + print("actual diff text:") + for line in diff_text.splitlines(): + print("'%s'" % line) + print("\nexpected diff text:") + for line in expected_diff_text.splitlines(): + print("'%s'" % line) + assert diff_text == expected_diff_text + + +class DescribeDiffPresenter: + """Unit-test suite for `opcdiag.presenter.DiffPresenter` objects.""" + + def it_can_diff_a_named_item_between_two_packages( + self, + package_: Mock, + package_2_: Mock, + DiffPresenter_: Mock, + pkg_item_: Mock, + pkg_item_2_: Mock, + ): + # exercise --------------------- + DiffPresenter.named_item_diff(package_, package_2_, URI_TAIL) + # verify ----------------------- + package_.find_item_by_uri_tail.assert_called_once_with(URI_TAIL) + package_2_.find_item_by_uri_tail.assert_called_once_with(URI_TAIL) + DiffPresenter_._pkg_item_diff.assert_called_once_with(pkg_item_, pkg_item_2_) + + def it_can_diff_two_package_items( + self, + pkg_item_: Mock, + pkg_item_2_: Mock, + ItemPresenter_: Mock, + item_presenter_text_: Mock, + item_presenter_2_text_: Mock, + diff_: Mock, + text_: Mock, + text_2_: Mock, + filename_: Mock, + filename_2_: Mock, + diff_text_: Mock, + ): + # exercise --------------------- + item_diff = DiffPresenter._pkg_item_diff(pkg_item_, pkg_item_2_) + # expected values -------------- + expected_ItemPresenter_calls = [call(pkg_item_), call(pkg_item_2_)] + # verify ----------------------- + ItemPresenter_.assert_has_calls(expected_ItemPresenter_calls) + item_presenter_text_.assert_called_once_with() + item_presenter_2_text_.assert_called_once_with() + diff_.assert_called_once_with(text_, text_2_, filename_, filename_2_) + assert item_diff is diff_text_ + + def it_can_gather_rels_diffs_between_two_packages( + self, + package_: Mock, + package_2_: Mock, + DiffPresenter_: Mock, + rels_items_: Mock, + pkg_item_diffs_: Mock, + ): + # exercise --------------------- + rels_diffs = DiffPresenter.rels_diffs(package_, package_2_) + # verify ----------------------- + DiffPresenter_._pkg_item_diffs.assert_called_once_with(rels_items_, package_2_) + assert rels_diffs is pkg_item_diffs_ + + def it_can_gather_xml_part_diffs_between_two_packages( + self, + package_: Mock, + package_2_: Mock, + DiffPresenter_: Mock, + xml_parts_: Mock, + pkg_item_diffs_: Mock, + ): + # exercise --------------------- + xml_part_diffs = DiffPresenter.xml_part_diffs(package_, package_2_) + # verify ----------------------- + DiffPresenter_._pkg_item_diffs.assert_called_once_with(xml_parts_, package_2_) + assert xml_part_diffs is pkg_item_diffs_ + + def it_can_diff_a_list_of_pkg_items_against_another_package( + self, + pkg_items_: Mock, + package_2_: Mock, + uri_: Mock, + uri_2_: Mock, + DiffPresenter_: Mock, + pkg_item_: Mock, + pkg_item_2_: Mock, + pkg_item_diff_: Mock, + pkg_item_diff_2_: Mock, + ): + # exercise --------------------- + diffs = DiffPresenter._pkg_item_diffs(pkg_items_, package_2_) + # verify ----------------------- + assert package_2_.find_item_by_uri_tail.call_args_list == [ + call(uri_), + call(uri_2_), + ] + assert DiffPresenter_._pkg_item_diff.call_args_list == [ + call(pkg_item_, pkg_item_2_), + call(pkg_item_2_, pkg_item_), + ] + assert diffs == [pkg_item_diff_, pkg_item_diff_2_] + + +class DescribeItemPresenter: + """Unit-test suite for `opcdiag.presenter.ItemPresenter` objects.""" + + FOOBAR_XML = ( + "\n" + "\n" + ' \n' + ' \n' + "" + ) + + @pytest.fixture + def foobar_elm_(self): + foobar_xml_bytes = self.FOOBAR_XML.encode("utf-8") + return etree.fromstring(foobar_xml_bytes) + + def it_constructs_subclass_based_on_item_type( + self, content_types_item_: Mock, rels_item_: Mock, xml_part_: Mock, binary_part_: Mock + ): + cases = ( + (content_types_item_, "ContentTypesPresenter"), + (rels_item_, "RelsItemPresenter"), + (xml_part_, "XmlPartPresenter"), + (binary_part_, "ItemPresenter"), + ) + for pkg_item, expected_type_name in cases: + item_presenter = ItemPresenter(pkg_item) + assert type(item_presenter).__name__ == expected_type_name + + def it_provides_a_normalized_path_string_for_the_pkg_item(self, pkg_item_: Mock): + pkg_item_.path = "foo\\bar" + item_presenter = ItemPresenter(pkg_item_) + assert item_presenter.filename == "foo/bar" + + def it_should_raise_if_text_property_not_implemented_on_subclass(self, binary_part_: Mock): + item_presenter = object.__new__(ItemPresenter) + with pytest.raises(NotImplementedError): + item_presenter.text + + def it_can_pretty_format_the_xml_of_its_item( + self, content_types_item_: Mock, foobar_elm_: Mock + ): + """Note: tests integration with lxml.etree""" + content_types_item_.element = foobar_elm_ + item_presenter = ItemPresenter(content_types_item_) + assert item_presenter.xml == self.FOOBAR_XML + -URI_TAIL = 'uri_tail' +class DescribeContentTypesPresenter: + """Unit-test suite for `opcdiag.presenter.ContentTypesPresenter` objects.""" + + def it_can_format_cti_xml(self, ItemPresenter_xml_: Mock, content_types_item_: Mock): + # fixture ---------------------- + ItemPresenter_xml_.return_value = ( + "\n" + "\n" + ' \n' + ' \n' + ' \n' + ' \n' + "" + ) + content_types_presenter = ItemPresenter(content_types_item_) + # verify ----------------------- + expected_text = ( + "\n" + "\n" + ' \n' + ' \n' + ' \n' + ' \n' + "" + ) + assert content_types_presenter.text == expected_text + + +class DescribeRelsItemPresenter: + """Unit-test suite for `opcdiag.presenter.RelsItemPresenter` objects.""" + + def it_can_format_rels_xml(self, ItemPresenter_xml_: Mock, rels_item_: Mock): + # fixture ---------------------- + ItemPresenter_xml_.return_value = ( + "\n" + "\n" + ' \n' + ' \n' + ' \n' + "" + ) + rels_presenter = ItemPresenter(rels_item_) + # verify ----------------------- + expected_text = ( + "\n" + "\n" + ' \n' + ' \n' + ' \n' + "" + ) + assert rels_presenter.text == expected_text + + +class DescribeXmlPartPresenter: + """Unit-test suite for `opcdiag.presenter.XmlPartPresenter` objects.""" + + def it_can_format_part_xml(self, ItemPresenter_xml_: Mock, xml_part_: Mock): + # fixture ---------------------- + cases = ( + # root w/no attrs is unchanged ----------------- + (("\n" ""), ("\n" "")), + # sort order: def_ns, nsdecls, attrs ----------- + ( + ( + "\n" + '' + ), + ( + "\n" + "" + ), + ), + ) + # verify ----------------------- + for part_xml, expected_xml in cases: + ItemPresenter_xml_.return_value = part_xml + part_presenter = ItemPresenter(xml_part_) + assert part_presenter.text == expected_xml + + +# ================================================================================================ +# MODULE-LEVEL FIXTURES +# ================================================================================================ @pytest.fixture -def binary_part_(request): +def binary_part_(request: FixtureRequest): binary_part_ = instance_mock(PkgItem, request) binary_part_.is_content_types = False binary_part_.is_rels_item = False @@ -38,7 +305,7 @@ def binary_part_(request): @pytest.fixture -def content_types_item_(request): +def content_types_item_(request: FixtureRequest): content_types_item_ = instance_mock(PkgItem, request) content_types_item_.is_content_types = True content_types_item_.is_rels_item = False @@ -47,49 +314,49 @@ def content_types_item_(request): @pytest.fixture -def DiffPresenter_(request, pkg_item_diffs_): - DiffPresenter_ = class_mock('opcdiag.presenter.DiffPresenter', request) +def DiffPresenter_(request: FixtureRequest, pkg_item_diffs_: Mock): + DiffPresenter_ = class_mock("opcdiag.presenter.DiffPresenter", request) DiffPresenter_._pkg_item_diff.side_effect = pkg_item_diffs_ DiffPresenter_._pkg_item_diffs.return_value = pkg_item_diffs_ return DiffPresenter_ @pytest.fixture -def diff_(request, diff_text_): - diff_ = function_mock('opcdiag.presenter.diff', request) +def diff_(request: FixtureRequest, diff_text_: Mock): + diff_ = function_mock("opcdiag.presenter.diff", request) diff_.return_value = diff_text_ return diff_ @pytest.fixture -def diff_text_(request): +def diff_text_(request: FixtureRequest): return loose_mock(request) @pytest.fixture -def filename_(request): +def filename_(request: FixtureRequest): return loose_mock(request) @pytest.fixture -def filename_2_(request): +def filename_2_(request: FixtureRequest): return loose_mock(request) @pytest.fixture -def ItemPresenter_(request, item_presenter_, item_presenter_2_): - ItemPresenter_ = class_mock('opcdiag.presenter.ItemPresenter', request) +def ItemPresenter_(request: FixtureRequest, item_presenter_: Mock, item_presenter_2_: Mock): + ItemPresenter_ = class_mock("opcdiag.presenter.ItemPresenter", request) ItemPresenter_.side_effect = (item_presenter_, item_presenter_2_) return ItemPresenter_ @pytest.fixture -def ItemPresenter_xml_(request): - return property_mock('opcdiag.presenter.ItemPresenter.xml', request) +def ItemPresenter_xml_(request: FixtureRequest): + return property_mock("opcdiag.presenter.ItemPresenter.xml", request) @pytest.fixture -def item_presenter_(request, filename_, item_presenter_text_): +def item_presenter_(request: FixtureRequest, filename_: Mock, item_presenter_text_: Mock): item_presenter_ = instance_mock(ItemPresenter, request) item_presenter_.filename = filename_ type(item_presenter_).text = item_presenter_text_ @@ -97,7 +364,7 @@ def item_presenter_(request, filename_, item_presenter_text_): @pytest.fixture -def item_presenter_2_(request, filename_2_, item_presenter_2_text_): +def item_presenter_2_(request: FixtureRequest, filename_2_: Mock, item_presenter_2_text_: Mock): item_presenter_2_ = instance_mock(ItemPresenter, request) item_presenter_2_.filename = filename_2_ type(item_presenter_2_).text = item_presenter_2_text_ @@ -105,17 +372,17 @@ def item_presenter_2_(request, filename_2_, item_presenter_2_text_): @pytest.fixture -def item_presenter_text_(request, text_): +def item_presenter_text_(request: FixtureRequest, text_: Mock): return PropertyMock(name=request.fixturename, return_value=text_) @pytest.fixture -def item_presenter_2_text_(request, text_2_): +def item_presenter_2_text_(request: FixtureRequest, text_2_: Mock): return PropertyMock(name=request.fixturename, return_value=text_2_) @pytest.fixture -def package_(request, pkg_item_, rels_items_, xml_parts_): +def package_(request: FixtureRequest, pkg_item_: Mock, rels_items_: Mock, xml_parts_: Mock): package_ = instance_mock(Package, request) package_.find_item_by_uri_tail.return_value = pkg_item_ package_.rels_items = rels_items_ @@ -124,48 +391,48 @@ def package_(request, pkg_item_, rels_items_, xml_parts_): @pytest.fixture -def package_2_(request, pkg_item_, pkg_item_2_): +def package_2_(request: FixtureRequest, pkg_item_: Mock, pkg_item_2_: Mock): package_2_ = instance_mock(Package, request) package_2_.find_item_by_uri_tail.side_effect = (pkg_item_2_, pkg_item_) return package_2_ @pytest.fixture -def pkg_item_(request, uri_): +def pkg_item_(request: FixtureRequest, uri_: Mock): pkg_item_ = instance_mock(PkgItem, request) pkg_item_.uri = uri_ return pkg_item_ @pytest.fixture -def pkg_item_2_(request, uri_2_): +def pkg_item_2_(request: FixtureRequest, uri_2_: Mock): pkg_item_2_ = instance_mock(PkgItem, request) pkg_item_2_.uri = uri_2_ return pkg_item_2_ @pytest.fixture -def pkg_items_(request, pkg_item_, pkg_item_2_): +def pkg_items_(request: FixtureRequest, pkg_item_: Mock, pkg_item_2_: Mock): return [pkg_item_, pkg_item_2_] @pytest.fixture -def pkg_item_diff_(request): - return 'diff_' +def pkg_item_diff_(request: FixtureRequest): + return "diff_" @pytest.fixture -def pkg_item_diff_2_(request): - return 'diff_2_' +def pkg_item_diff_2_(request: FixtureRequest): + return "diff_2_" @pytest.fixture -def pkg_item_diffs_(request, pkg_item_diff_, pkg_item_diff_2_): +def pkg_item_diffs_(request: FixtureRequest, pkg_item_diff_: Mock, pkg_item_diff_2_: Mock): return [pkg_item_diff_, pkg_item_diff_2_] @pytest.fixture -def rels_item_(request): +def rels_item_(request: FixtureRequest): rels_item_ = instance_mock(PkgItem, request) rels_item_.is_content_types = False rels_item_.is_rels_item = True @@ -174,262 +441,41 @@ def rels_item_(request): @pytest.fixture -def rels_items_(request): +def rels_items_(request: FixtureRequest): rels_items_ = instance_mock(list, request) return rels_items_ @pytest.fixture -def text_(request): - return 'text_' +def text_(request: FixtureRequest): + return "text_" @pytest.fixture -def text_2_(request): - return 'text_2_' +def text_2_(request: FixtureRequest): + return "text_2_" @pytest.fixture -def uri_(request): - return '/word/document.xml' +def uri_(request: FixtureRequest): + return "/word/document.xml" @pytest.fixture -def uri_2_(request): - return '/_rels/.rels' +def uri_2_(request: FixtureRequest): + return "/_rels/.rels" @pytest.fixture -def xml_parts_(request): +def xml_parts_(request: FixtureRequest): xml_parts_ = instance_mock(list, request) return xml_parts_ @pytest.fixture -def xml_part_(request): +def xml_part_(request: FixtureRequest): xml_part_ = instance_mock(PkgItem, request) xml_part_.is_content_types = False xml_part_.is_rels_item = False xml_part_.is_xml_part = True return xml_part_ - - -class DescribeDiff(object): - - def it_calculates_a_diff_between_two_texts(self): - """Integrates with difflib""" - # fixture ---------------------- - text = 'foobar\nnoobar\nzoobar' - text_2 = 'foobar\ngoobar\nnoobar' - filename, filename_2 = 'filename', 'filename_2' - expected_diff_text = ( - '--- filename\n' - '\n' - '+++ filename_2\n' - '\n' - '@@ -1,3 +1,3 @@\n' - '\n' - ' foobar\n' - '+goobar\n' - ' noobar\n' - '-zoobar' - ) - # exercise --------------------- - diff_text = diff(text, text_2, filename, filename_2) - print("actual diff text:") - for line in diff_text.splitlines(): - print("'%s'" % line) - print("\nexpected diff text:") - for line in expected_diff_text.splitlines(): - print("'%s'" % line) - assert diff_text == expected_diff_text - - -class DescribeDiffPresenter(object): - - def it_can_diff_a_named_item_between_two_packages( - self, package_, package_2_, DiffPresenter_, pkg_item_, - pkg_item_2_): - # exercise --------------------- - DiffPresenter.named_item_diff(package_, package_2_, URI_TAIL) - # verify ----------------------- - package_.find_item_by_uri_tail.assert_called_once_with(URI_TAIL) - package_2_.find_item_by_uri_tail.assert_called_once_with(URI_TAIL) - DiffPresenter_._pkg_item_diff.assert_called_once_with( - pkg_item_, pkg_item_2_) - - def it_can_diff_two_package_items( - self, pkg_item_, pkg_item_2_, ItemPresenter_, - item_presenter_text_, item_presenter_2_text_, diff_, text_, - text_2_, filename_, filename_2_, diff_text_): - # exercise --------------------- - item_diff = DiffPresenter._pkg_item_diff(pkg_item_, pkg_item_2_) - # expected values -------------- - expected_ItemPresenter_calls = [call(pkg_item_), call(pkg_item_2_)] - # verify ----------------------- - ItemPresenter_.assert_has_calls(expected_ItemPresenter_calls) - item_presenter_text_.assert_called_once_with() - item_presenter_2_text_.assert_called_once_with() - diff_.assert_called_once_with(text_, text_2_, filename_, filename_2_) - assert item_diff is diff_text_ - - def it_can_gather_rels_diffs_between_two_packages( - self, package_, package_2_, DiffPresenter_, rels_items_, - pkg_item_diffs_): - # exercise --------------------- - rels_diffs = DiffPresenter.rels_diffs(package_, package_2_) - # verify ----------------------- - DiffPresenter_._pkg_item_diffs.assert_called_once_with( - rels_items_, package_2_) - assert rels_diffs is pkg_item_diffs_ - - def it_can_gather_xml_part_diffs_between_two_packages( - self, package_, package_2_, DiffPresenter_, xml_parts_, - pkg_item_diffs_): - # exercise --------------------- - xml_part_diffs = DiffPresenter.xml_part_diffs(package_, package_2_) - # verify ----------------------- - DiffPresenter_._pkg_item_diffs.assert_called_once_with( - xml_parts_, package_2_) - assert xml_part_diffs is pkg_item_diffs_ - - def it_can_diff_a_list_of_pkg_items_against_another_package( - self, pkg_items_, package_2_, uri_, uri_2_, DiffPresenter_, - pkg_item_, pkg_item_2_, pkg_item_diff_, pkg_item_diff_2_): - # exercise --------------------- - diffs = DiffPresenter._pkg_item_diffs(pkg_items_, package_2_) - # verify ----------------------- - assert package_2_.find_item_by_uri_tail.call_args_list == [ - call(uri_), call(uri_2_) - ] - assert DiffPresenter_._pkg_item_diff.call_args_list == [ - call(pkg_item_, pkg_item_2_), call(pkg_item_2_, pkg_item_)] - assert diffs == [pkg_item_diff_, pkg_item_diff_2_] - - -class DescribeItemPresenter(object): - - FOOBAR_XML = ( - '\n' - '\n' - ' \n' - ' \n' - '' - ) - - @pytest.fixture - def foobar_elm_(self): - foobar_xml_bytes = self.FOOBAR_XML.encode('utf-8') - return etree.fromstring(foobar_xml_bytes) - - def it_constructs_subclass_based_on_item_type( - self, content_types_item_, rels_item_, xml_part_, binary_part_): - cases = ( - (content_types_item_, 'ContentTypesPresenter'), - (rels_item_, 'RelsItemPresenter'), - (xml_part_, 'XmlPartPresenter'), - (binary_part_, 'ItemPresenter'), - ) - for pkg_item, expected_type_name in cases: - item_presenter = ItemPresenter(pkg_item) - assert type(item_presenter).__name__ == expected_type_name - - def it_provides_a_normalized_path_string_for_the_pkg_item(self, pkg_item_): - pkg_item_.path = 'foo\\bar' - item_presenter = ItemPresenter(pkg_item_) - assert item_presenter.filename == 'foo/bar' - - def it_should_raise_if_text_property_not_implemented_on_subclass( - self, binary_part_): - item_presenter = object.__new__(ItemPresenter) - with pytest.raises(NotImplementedError): - item_presenter.text - - def it_can_pretty_format_the_xml_of_its_item( - self, content_types_item_, foobar_elm_): - """Note: tests integration with lxml.etree""" - content_types_item_.element = foobar_elm_ - item_presenter = ItemPresenter(content_types_item_) - assert item_presenter.xml == self.FOOBAR_XML - - -class DescribeContentTypesPresenter(object): - - def it_can_format_cti_xml(self, ItemPresenter_xml_, content_types_item_): - # fixture ---------------------- - ItemPresenter_xml_.return_value = ( - '\n' - '\n' - ' \n' - ' \n' - ' \n' - ' \n' - '' - ) - content_types_presenter = ItemPresenter(content_types_item_) - # verify ----------------------- - expected_text = ( - '\n' - '\n' - ' \n' - ' \n' - ' \n' - ' \n' - '' - ) - assert content_types_presenter.text == expected_text - - -class DescribeRelsItemPresenter(object): - - def it_can_format_rels_xml(self, ItemPresenter_xml_, rels_item_): - # fixture ---------------------- - ItemPresenter_xml_.return_value = ( - '\n' - '\n' - ' \n' - ' \n' - ' \n' - '' - ) - rels_presenter = ItemPresenter(rels_item_) - # verify ----------------------- - expected_text = ( - '\n' - '\n' - ' \n' - ' \n' - ' \n' - '' - ) - assert rels_presenter.text == expected_text - - -class DescribeXmlPartPresenter(object): - - def it_can_format_part_xml(self, ItemPresenter_xml_, xml_part_): - # fixture ---------------------- - cases = ( - # root w/no attrs is unchanged ----------------- - (('\n' - ''), - ('\n' - '')), - # sort order: def_ns, nsdecls, attrs ----------- - (('\n' - ''), - ('\n' - '')), - ) - # verify ----------------------- - for part_xml, expected_xml in cases: - ItemPresenter_xml_.return_value = part_xml - part_presenter = ItemPresenter(xml_part_) - assert part_presenter.text == expected_xml diff --git a/tests/unitutil.py b/tests/unitutil.py index 882f1c2..30ec8cb 100644 --- a/tests/unitutil.py +++ b/tests/unitutil.py @@ -1,32 +1,28 @@ -# -*- coding: utf-8 -*- -# -# unitutil.py -# -# Copyright (C) 2013 Steve Canny scanny@cisco.com -# -# This module is part of opc-diag and is released under the MIT License: -# http://www.opensource.org/licenses/mit-license.php - """Utility functions for unit testing""" +from __future__ import annotations + import os +from typing import Any +from unittest.mock import ANY as ANY +from unittest.mock import Mock, PropertyMock, create_autospec, patch -from mock import create_autospec, Mock, patch, PropertyMock +from pytest import FixtureRequest # noqa: PT013 -def abspath(relpath): +def abspath(relpath: str): thisdir = os.path.split(__file__)[0] return os.path.abspath(os.path.join(thisdir, relpath)) -def relpath(relpath): +def relpath(relpath: str): thisdir = os.path.split(__file__)[0] return os.path.relpath(os.path.join(thisdir, relpath)) -def class_mock(q_class_name, request, autospec=True, **kwargs): - """ - Return a mock patching the class with qualified name *q_class_name*. +def class_mock(q_class_name: str, request: FixtureRequest, autospec: bool = True, **kwargs: Any): + """Return a mock patching the class with qualified name *q_class_name*. + The mock is autospec'ed based on the patched class unless the optional argument *autospec* is set to False. Any other keyword arguments are passed through to Mock(). Patch is reversed after calling test returns. @@ -36,43 +32,44 @@ def class_mock(q_class_name, request, autospec=True, **kwargs): return _patch.start() -def function_mock(q_function_name, request): - """ - Return a mock patching the function with qualified name - *q_function_name*. Patch is reversed after calling test returns. +def function_mock(q_function_name: str, request: FixtureRequest): + """Return a mock patching the function with qualified name *q_function_name*. + + Patch is reversed after calling test returns. """ _patch = patch(q_function_name) request.addfinalizer(_patch.stop) return _patch.start() -def initializer_mock(cls, request): - """ - Return a mock for the __init__ method on *cls* where the patch is - reversed after pytest uses it. - """ - _patch = patch.object(cls, '__init__', return_value=None) +def initializer_mock(Cls: type, request: FixtureRequest): + """Return a mock for the __init__ method on *Cls*.""" + _patch = patch.object(Cls, "__init__", return_value=None) request.addfinalizer(_patch.stop) return _patch.start() -def instance_mock(cls, request, name=None, spec_set=True, **kwargs): - """ - Return a mock for an instance of *cls* that draws its spec from the class - and does not allow new attributes to be set on the instance. If *name* is - missing or |None|, the name of the returned |Mock| instance is set to - *request.fixturename*. Additional keyword arguments are passed through to - the Mock() call that creates the mock. +def instance_mock( + Cls: type, + request: FixtureRequest, + name: str | None = None, + spec_set: bool = True, + **kwargs: Any, +): + """Return a mock for an instance of *Cls* that draws its spec from the class. + + The mock does not allow new attributes to be set on the instance. If *name* is missing or + |None|, the name of the returned |Mock| instance is set to *request.fixturename*. Additional + keyword arguments are passed through to the Mock() call that creates the mock. """ if name is None: name = request.fixturename - return create_autospec(cls, _name=name, spec_set=spec_set, instance=True, - **kwargs) + return create_autospec(Cls, _name=name, spec_set=spec_set, instance=True, **kwargs) -def loose_mock(request, name=None, **kwargs): - """ - Return a "loose" mock, meaning it has no spec to constrain calls on it. +def loose_mock(request: FixtureRequest, name: str | None = None, **kwargs: Any): + """Return a "loose" mock, meaning it has no spec to constrain calls on it. + Additional keyword arguments are passed through to Mock(). """ if name is None: @@ -80,31 +77,22 @@ def loose_mock(request, name=None, **kwargs): return Mock(name=name, **kwargs) -def method_mock(cls, method_name, request): - """ - Return a mock for method *method_name* on *cls* where the patch is - reversed after pytest uses it. - """ - _patch = patch.object(cls, method_name) +def method_mock(Cls: type, method_name: str, request: FixtureRequest): + """Return a mock for method `method_name` on `cls`.""" + _patch = patch.object(Cls, method_name) request.addfinalizer(_patch.stop) return _patch.start() -def property_mock(q_property_name, request): - """ - Return a mock for property with fully qualified name *q_property_name* - where the patch is reversed after pytest uses it. - """ +def property_mock(q_property_name: str, request: FixtureRequest): + """Return a mock for property with fully qualified name *q_property_name*.""" _patch = patch(q_property_name, new_callable=PropertyMock) request.addfinalizer(_patch.stop) return _patch.start() -def var_mock(q_var_name, request, **kwargs): - """ - Return a mock patching the variable with qualified name *q_var_name*. - Patch is reversed after calling test returns. - """ +def var_mock(q_var_name: str, request: FixtureRequest, **kwargs: Any): + """Return a mock patching the variable with qualified name `q_var_name`.""" _patch = patch(q_var_name, **kwargs) request.addfinalizer(_patch.stop) return _patch.start() diff --git a/tox.ini b/tox.ini index 2dbf177..98948c3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,26 +1,10 @@ -# -# tox.ini -# -# Copyright (C) 2012, 2013 Steve Canny scanny@cisco.com -# -# This module is part of python-opc and is released under the MIT License: -# http://www.opensource.org/licenses/mit-license.php -# -# Configuration for tox and pytest - -[pytest] -norecursedirs = doc *.egg-info features .git opcdiag .tox -python_classes = Test Describe -python_functions = test_ it_ they_ - [tox] -envlist = py26, py27, py33 +envlist = py39, py310, py311, py312 [testenv] deps = behave lxml - mock pytest commands = diff --git a/typings/behave/__init__.pyi b/typings/behave/__init__.pyi new file mode 100644 index 0000000..f8ffc20 --- /dev/null +++ b/typings/behave/__init__.pyi @@ -0,0 +1,17 @@ +from __future__ import annotations + +from typing import Callable + +from typing_extensions import TypeAlias + +from .runner import Context + +_ThreeArgStep: TypeAlias = Callable[[Context, str, str, str], None] +_TwoArgStep: TypeAlias = Callable[[Context, str, str], None] +_OneArgStep: TypeAlias = Callable[[Context, str], None] +_NoArgStep: TypeAlias = Callable[[Context], None] +_Step: TypeAlias = _NoArgStep | _OneArgStep | _TwoArgStep | _ThreeArgStep + +def given(phrase: str) -> Callable[[_Step], _Step]: ... +def when(phrase: str) -> Callable[[_Step], _Step]: ... +def then(phrase: str) -> Callable[[_Step], _Step]: ... diff --git a/typings/behave/runner.pyi b/typings/behave/runner.pyi new file mode 100644 index 0000000..aaea74d --- /dev/null +++ b/typings/behave/runner.pyi @@ -0,0 +1,3 @@ +from types import SimpleNamespace + +class Context(SimpleNamespace): ... diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..4c20bb1 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1115 @@ +version = 1 +requires-python = ">=3.9" + +[[package]] +name = "alabaster" +version = "0.7.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/71/a8ee96d1fd95ca04a0d2e2d9c4081dac4c2d2b12f7ddb899c8cb9bfd1532/alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2", size = 11454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/88/c7083fc61120ab661c5d0b82cb77079fc1429d3f913a456c1c82cf4658f7/alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3", size = 13857 }, +] + +[[package]] +name = "babel" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 }, +] + +[[package]] +name = "behave" +version = "1.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parse" }, + { name = "parse-type" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/4b/d0a8c23b6c8985e5544ea96d27105a273ea22051317f850c2cdbf2029fe4/behave-1.2.6.tar.gz", hash = "sha256:b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86", size = 701696 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/6c/ec9169548b6c4cb877aaa6773408ca08ae2a282805b958dbc163cb19822d/behave-1.2.6-py2.py3-none-any.whl", hash = "sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c", size = 136779 }, +] + +[[package]] +name = "cachetools" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/38/a0f315319737ecf45b4319a8cd1f3a908e29d9277b46942263292115eee7/cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a", size = 27661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/07/14f8ad37f2d12a5ce41206c21820d8cb6561b728e51fad4530dff0552a67/cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", size = 9524 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, +] + +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/61/095a0aa1a84d1481998b534177c8566fdc50bb1233ea9a0478cd3cc075bd/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", size = 194219 }, + { url = "https://files.pythonhosted.org/packages/cc/94/f7cf5e5134175de79ad2059edf2adce18e0685ebdb9227ff0139975d0e93/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", size = 122521 }, + { url = "https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", size = 120383 }, + { url = "https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", size = 138223 }, + { url = "https://files.pythonhosted.org/packages/05/8c/eb854996d5fef5e4f33ad56927ad053d04dc820e4a3d39023f35cad72617/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", size = 148101 }, + { url = "https://files.pythonhosted.org/packages/f6/93/bb6cbeec3bf9da9b2eba458c15966658d1daa8b982c642f81c93ad9b40e1/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", size = 140699 }, + { url = "https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", size = 142065 }, + { url = "https://files.pythonhosted.org/packages/3f/ba/3f5e7be00b215fa10e13d64b1f6237eb6ebea66676a41b2bcdd09fe74323/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", size = 144505 }, + { url = "https://files.pythonhosted.org/packages/33/c3/3b96a435c5109dd5b6adc8a59ba1d678b302a97938f032e3770cc84cd354/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", size = 139425 }, + { url = "https://files.pythonhosted.org/packages/43/05/3bf613e719efe68fb3a77f9c536a389f35b95d75424b96b426a47a45ef1d/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", size = 145287 }, + { url = "https://files.pythonhosted.org/packages/58/78/a0bc646900994df12e07b4ae5c713f2b3e5998f58b9d3720cce2aa45652f/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", size = 149929 }, + { url = "https://files.pythonhosted.org/packages/eb/5c/97d97248af4920bc68687d9c3b3c0f47c910e21a8ff80af4565a576bd2f0/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", size = 141605 }, + { url = "https://files.pythonhosted.org/packages/a8/31/47d018ef89f95b8aded95c589a77c072c55e94b50a41aa99c0a2008a45a4/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", size = 142646 }, + { url = "https://files.pythonhosted.org/packages/ae/d5/4fecf1d58bedb1340a50f165ba1c7ddc0400252d6832ff619c4568b36cc0/charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", size = 92846 }, + { url = "https://files.pythonhosted.org/packages/a2/a0/4af29e22cb5942488cf45630cbdd7cefd908768e69bdd90280842e4e8529/charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", size = 100343 }, + { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 }, + { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 }, + { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 }, + { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 }, + { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 }, + { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 }, + { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 }, + { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 }, + { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 }, + { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 }, + { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 }, + { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 }, + { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 }, + { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 }, + { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 }, + { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 }, + { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 }, + { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 }, + { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 }, + { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 }, + { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 }, + { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 }, + { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 }, + { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 }, + { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 }, + { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 }, + { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 }, + { url = "https://files.pythonhosted.org/packages/f7/9d/bcf4a449a438ed6f19790eee543a86a740c77508fbc5ddab210ab3ba3a9a/charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", size = 194198 }, + { url = "https://files.pythonhosted.org/packages/66/fe/c7d3da40a66a6bf2920cce0f436fa1f62ee28aaf92f412f0bf3b84c8ad6c/charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", size = 122494 }, + { url = "https://files.pythonhosted.org/packages/2a/9d/a6d15bd1e3e2914af5955c8eb15f4071997e7078419328fee93dfd497eb7/charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", size = 120393 }, + { url = "https://files.pythonhosted.org/packages/3d/85/5b7416b349609d20611a64718bed383b9251b5a601044550f0c8983b8900/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", size = 138331 }, + { url = "https://files.pythonhosted.org/packages/79/66/8946baa705c588521afe10b2d7967300e49380ded089a62d38537264aece/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", size = 148097 }, + { url = "https://files.pythonhosted.org/packages/44/80/b339237b4ce635b4af1c73742459eee5f97201bd92b2371c53e11958392e/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", size = 140711 }, + { url = "https://files.pythonhosted.org/packages/98/69/5d8751b4b670d623aa7a47bef061d69c279e9f922f6705147983aa76c3ce/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", size = 142251 }, + { url = "https://files.pythonhosted.org/packages/1f/8d/33c860a7032da5b93382cbe2873261f81467e7b37f4ed91e25fed62fd49b/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", size = 144636 }, + { url = "https://files.pythonhosted.org/packages/c2/65/52aaf47b3dd616c11a19b1052ce7fa6321250a7a0b975f48d8c366733b9f/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", size = 139514 }, + { url = "https://files.pythonhosted.org/packages/51/fd/0ee5b1c2860bb3c60236d05b6e4ac240cf702b67471138571dad91bcfed8/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", size = 145528 }, + { url = "https://files.pythonhosted.org/packages/e1/9c/60729bf15dc82e3aaf5f71e81686e42e50715a1399770bcde1a9e43d09db/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", size = 149804 }, + { url = "https://files.pythonhosted.org/packages/53/cd/aa4b8a4d82eeceb872f83237b2d27e43e637cac9ffaef19a1321c3bafb67/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", size = 141708 }, + { url = "https://files.pythonhosted.org/packages/54/7f/cad0b328759630814fcf9d804bfabaf47776816ad4ef2e9938b7e1123d04/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561", size = 142708 }, + { url = "https://files.pythonhosted.org/packages/c1/9d/254a2f1bcb0ce9acad838e94ed05ba71a7cb1e27affaa4d9e1ca3958cdb6/charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", size = 92830 }, + { url = "https://files.pythonhosted.org/packages/2f/0e/d7303ccae9735ff8ff01e36705ad6233ad2002962e8668a970fc000c5e1b/charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", size = 100376 }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690 }, + { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127 }, + { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654 }, + { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598 }, + { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732 }, + { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816 }, + { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325 }, + { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418 }, + { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343 }, + { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136 }, + { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796 }, + { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244 }, + { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279 }, + { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859 }, + { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549 }, + { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477 }, + { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134 }, + { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910 }, + { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348 }, + { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983 }, + { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221 }, + { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342 }, + { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371 }, + { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455 }, + { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924 }, + { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252 }, + { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897 }, + { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606 }, + { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373 }, + { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007 }, + { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269 }, + { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886 }, + { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037 }, + { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038 }, + { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690 }, + { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765 }, + { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611 }, + { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671 }, + { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368 }, + { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758 }, + { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035 }, + { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839 }, + { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569 }, + { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927 }, + { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401 }, + { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301 }, + { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598 }, + { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 }, + { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 }, + { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688 }, + { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120 }, + { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249 }, + { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237 }, + { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311 }, + { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453 }, + { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958 }, + { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938 }, + { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352 }, + { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153 }, + { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "43.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ba/0664727028b37e249e73879348cc46d45c5c1a2a2e81e8166462953c5755/cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d", size = 686927 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/13/1193774705783ba364121aa2a60132fa31a668b8ababd5edfa1662354ccd/cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062", size = 3794751 }, + { url = "https://files.pythonhosted.org/packages/5e/4b/39bb3c4c8cfb3e94e736b8d8859ce5c81536e91a1033b1d26770c4249000/cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962", size = 3981827 }, + { url = "https://files.pythonhosted.org/packages/ce/dc/1471d4d56608e1013237af334b8a4c35d53895694fbb73882d1c4fd3f55e/cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277", size = 3780034 }, + { url = "https://files.pythonhosted.org/packages/ad/43/7a9920135b0d5437cc2f8f529fa757431eb6a7736ddfadfdee1cc5890800/cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", size = 3993407 }, + { url = "https://files.pythonhosted.org/packages/cc/42/9ab8467af6c0b76f3d9b8f01d1cf25b9c9f3f2151f4acfab888d21c55a72/cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042", size = 3886457 }, + { url = "https://files.pythonhosted.org/packages/a4/65/430509e31700286ec02868a2457d2111d03ccefc20349d24e58d171ae0a7/cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494", size = 4081499 }, + { url = "https://files.pythonhosted.org/packages/00/0e/8217e348a1fa417ec4c78cd3cdf24154f5e76fd7597343a35bd403650dfd/cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806", size = 3794368 }, + { url = "https://files.pythonhosted.org/packages/3d/ed/38b6be7254d8f7251fde8054af597ee8afa14f911da67a9410a45f602fc3/cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85", size = 3981750 }, + { url = "https://files.pythonhosted.org/packages/64/f3/b7946c3887cf7436f002f4cbb1e6aec77b8d299b86be48eeadfefb937c4b/cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c", size = 3778925 }, + { url = "https://files.pythonhosted.org/packages/ac/7e/ebda4dd4ae098a0990753efbb4b50954f1d03003846b943ea85070782da7/cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1", size = 3993152 }, + { url = "https://files.pythonhosted.org/packages/43/f6/feebbd78a3e341e3913846a3bb2c29d0b09b1b3af1573c6baabc2533e147/cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa", size = 3886392 }, + { url = "https://files.pythonhosted.org/packages/bd/4c/ab0b9407d5247576290b4fd8abd06b7f51bd414f04eef0f2800675512d61/cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4", size = 4082606 }, + { url = "https://files.pythonhosted.org/packages/ea/45/967da50269954b993d4484bf85026c7377bd551651ebdabba94905972556/cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d", size = 3713077 }, + { url = "https://files.pythonhosted.org/packages/df/e6/ccd29a1f9a6b71294e1e9f530c4d779d5dd37c8bb736c05d5fb6d98a971b/cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289", size = 3915597 }, + { url = "https://files.pythonhosted.org/packages/3e/fd/70f3e849ad4d6cca2118ee6938e0b52326d02406f10912356151dd4b6868/cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96", size = 3713909 }, + { url = "https://files.pythonhosted.org/packages/21/b0/4ecefa99519eaa32af49a3ad002bb3e795f9e6eb32221fd87736247fa3cb/cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172", size = 3916544 }, +] + +[[package]] +name = "cssselect" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/91/d51202cc41fbfca7fa332f43a5adac4b253962588c7cc5a54824b019081c/cssselect-1.2.0.tar.gz", hash = "sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc", size = 41423 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/a9/2da08717a6862c48f1d61ef957a7bba171e7eefa6c0aa0ceb96a140c2a6b/cssselect-1.2.0-py2.py3-none-any.whl", hash = "sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e", size = 18687 }, +] + +[[package]] +name = "distlib" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/91/e2df406fb4efacdf46871c25cde65d3c6ee5e173b7e5a4547a47bae91920/distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64", size = 609931 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 }, +] + +[[package]] +name = "docutils" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/17/559b4d020f4b46e0287a2eddf2d8ebf76318fd3bd495f1625414b052fdc9/docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", size = 2016138 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5e/6003a0d1f37725ec2ebd4046b657abb9372202655f96e76795dca8c0063c/docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61", size = 575533 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 }, +] + +[[package]] +name = "jaraco-functools" +version = "4.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/b1/6ca3c2052e584e9908a2c146f00378939b3c51b839304ab8ef4de067f042/jaraco_functools-4.0.2.tar.gz", hash = "sha256:3460c74cd0d32bf82b9576bbb3527c4364d5b27a21f5158a62aed6c4b42e23f5", size = 18319 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/54/7623e24ffc63730c3a619101361b08860c6b7c7cfc1aef6edb66d80ed708/jaraco.functools-4.0.2-py3-none-any.whl", hash = "sha256:c9d16a3ed4ccb5a889ad8e0b7a343401ee5b2a71cee6ed192d3f68bc351e94e3", size = 9883 }, +] + +[[package]] +name = "jeepney" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/f4/154cf374c2daf2020e05c3c6a03c91348d59b23c5366e968feb198306fdf/jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", size = 106005 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/72/2a1e2290f1ab1e06f71f3d0f1646c9e4634e70e1d37491535e19266e8dc9/jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755", size = 48435 }, +] + +[[package]] +name = "jinja2" +version = "2.11.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/e7/65300e6b32e69768ded990494809106f87da1d436418d5f1367ed3966fd7/Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6", size = 257589 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/c2/1eece8c95ddbc9b1aeb64f5783a9e07a286de42191b7204d67b7496ddf35/Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", size = 125699 }, +] + +[[package]] +name = "keyring" +version = "25.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/1c/2bdbcfd5d59dc6274ffb175bc29aa07ecbfab196830e0cfbde7bd861a2ea/keyring-25.4.1.tar.gz", hash = "sha256:b07ebc55f3e8ed86ac81dd31ef14e81ace9dd9c3d4b5d77a6e9a2016d0d71a1b", size = 62491 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/25/e6d59e5f0a0508d0dca8bb98c7f7fd3772fc943ac3f53d5ab18a218d32c0/keyring-25.4.1-py3-none-any.whl", hash = "sha256:5426f817cf7f6f007ba5ec722b1bcad95a75b27d780343772ad76b17cb47b0bf", size = 38946 }, +] + +[[package]] +name = "lxml" +version = "5.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/6b/20c3a4b24751377aaa6307eb230b66701024012c29dd374999cc92983269/lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f", size = 3679318 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ce/2789e39eddf2b13fac29878bfa465f0910eb6b0096e29090e5176bc8cf43/lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656", size = 8124570 }, + { url = "https://files.pythonhosted.org/packages/24/a8/f4010166a25d41715527129af2675981a50d3bbf7df09c5d9ab8ca24fbf9/lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d", size = 4413042 }, + { url = "https://files.pythonhosted.org/packages/41/a4/7e45756cecdd7577ddf67a68b69c1db0f5ddbf0c9f65021ee769165ffc5a/lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a", size = 5139213 }, + { url = "https://files.pythonhosted.org/packages/02/e2/ecf845b12323c92748077e1818b64e8b4dba509a4cb12920b3762ebe7552/lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8", size = 4838814 }, + { url = "https://files.pythonhosted.org/packages/12/91/619f9fb72cf75e9ceb8700706f7276f23995f6ad757e6d400fbe35ca4990/lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330", size = 5425084 }, + { url = "https://files.pythonhosted.org/packages/25/3b/162a85a8f0fd2a3032ec3f936636911c6e9523a8e263fffcfd581ce98b54/lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965", size = 4875993 }, + { url = "https://files.pythonhosted.org/packages/43/af/dd3f58cc7d946da6ae42909629a2b1d5dd2d1b583334d4af9396697d6863/lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22", size = 5012462 }, + { url = "https://files.pythonhosted.org/packages/69/c1/5ea46b2d4c98f5bf5c83fffab8a0ad293c9bc74df9ecfbafef10f77f7201/lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b", size = 4815288 }, + { url = "https://files.pythonhosted.org/packages/1d/51/a0acca077ad35da458f4d3f729ef98effd2b90f003440d35fc36323f8ae6/lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7", size = 5472435 }, + { url = "https://files.pythonhosted.org/packages/4d/6b/0989c9368986961a6b0f55b46c80404c4b758417acdb6d87bfc3bd5f4967/lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8", size = 4976354 }, + { url = "https://files.pythonhosted.org/packages/05/9e/87492d03ff604fbf656ed2bf3e2e8d28f5d58ea1f00ff27ac27b06509079/lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32", size = 5029973 }, + { url = "https://files.pythonhosted.org/packages/f9/cc/9ae1baf5472af88e19e2c454b3710c1be9ecafb20eb474eeabcd88a055d2/lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86", size = 4888837 }, + { url = "https://files.pythonhosted.org/packages/d2/10/5594ffaec8c120d75b17e3ad23439b740a51549a9b5fd7484b2179adfe8f/lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5", size = 5530555 }, + { url = "https://files.pythonhosted.org/packages/ea/9b/de17f05377c8833343b629905571fb06cff2028f15a6f58ae2267662e341/lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03", size = 5405314 }, + { url = "https://files.pythonhosted.org/packages/8a/b4/227be0f1f3cca8255925985164c3838b8b36e441ff0cc10c1d3c6bdba031/lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7", size = 5079303 }, + { url = "https://files.pythonhosted.org/packages/5c/ee/19abcebb7fc40319bb71cd6adefa1ad94d09b5660228715854d6cc420713/lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80", size = 3475126 }, + { url = "https://files.pythonhosted.org/packages/a1/35/183d32551447e280032b2331738cd850da435a42f850b71ebeaab42c1313/lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3", size = 3805065 }, + { url = "https://files.pythonhosted.org/packages/5c/a8/449faa2a3cbe6a99f8d38dcd51a3ee8844c17862841a6f769ea7c2a9cd0f/lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b", size = 8141056 }, + { url = "https://files.pythonhosted.org/packages/ac/8a/ae6325e994e2052de92f894363b038351c50ee38749d30cc6b6d96aaf90f/lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18", size = 4425238 }, + { url = "https://files.pythonhosted.org/packages/f8/fb/128dddb7f9086236bce0eeae2bfb316d138b49b159f50bc681d56c1bdd19/lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442", size = 5095197 }, + { url = "https://files.pythonhosted.org/packages/b4/f9/a181a8ef106e41e3086629c8bdb2d21a942f14c84a0e77452c22d6b22091/lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4", size = 4809809 }, + { url = "https://files.pythonhosted.org/packages/25/2f/b20565e808f7f6868aacea48ddcdd7e9e9fb4c799287f21f1a6c7c2e8b71/lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f", size = 5407593 }, + { url = "https://files.pythonhosted.org/packages/23/0e/caac672ec246d3189a16c4d364ed4f7d6bf856c080215382c06764058c08/lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e", size = 4866657 }, + { url = "https://files.pythonhosted.org/packages/67/a4/1f5fbd3f58d4069000522196b0b776a014f3feec1796da03e495cf23532d/lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c", size = 4967017 }, + { url = "https://files.pythonhosted.org/packages/ee/73/623ecea6ca3c530dd0a4ed0d00d9702e0e85cd5624e2d5b93b005fe00abd/lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16", size = 4810730 }, + { url = "https://files.pythonhosted.org/packages/1d/ce/fb84fb8e3c298f3a245ae3ea6221c2426f1bbaa82d10a88787412a498145/lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79", size = 5455154 }, + { url = "https://files.pythonhosted.org/packages/b1/72/4d1ad363748a72c7c0411c28be2b0dc7150d91e823eadad3b91a4514cbea/lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080", size = 4969416 }, + { url = "https://files.pythonhosted.org/packages/42/07/b29571a58a3a80681722ea8ed0ba569211d9bb8531ad49b5cacf6d409185/lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654", size = 5013672 }, + { url = "https://files.pythonhosted.org/packages/b9/93/bde740d5a58cf04cbd38e3dd93ad1e36c2f95553bbf7d57807bc6815d926/lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d", size = 4878644 }, + { url = "https://files.pythonhosted.org/packages/56/b5/645c8c02721d49927c93181de4017164ec0e141413577687c3df8ff0800f/lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763", size = 5511531 }, + { url = "https://files.pythonhosted.org/packages/85/3f/6a99a12d9438316f4fc86ef88c5d4c8fb674247b17f3173ecadd8346b671/lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec", size = 5402065 }, + { url = "https://files.pythonhosted.org/packages/80/8a/df47bff6ad5ac57335bf552babfb2408f9eb680c074ec1ba412a1a6af2c5/lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be", size = 5069775 }, + { url = "https://files.pythonhosted.org/packages/08/ae/e7ad0f0fbe4b6368c5ee1e3ef0c3365098d806d42379c46c1ba2802a52f7/lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9", size = 3474226 }, + { url = "https://files.pythonhosted.org/packages/c3/b5/91c2249bfac02ee514ab135e9304b89d55967be7e53e94a879b74eec7a5c/lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1", size = 3814971 }, + { url = "https://files.pythonhosted.org/packages/eb/6d/d1f1c5e40c64bf62afd7a3f9b34ce18a586a1cccbf71e783cd0a6d8e8971/lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859", size = 8171753 }, + { url = "https://files.pythonhosted.org/packages/bd/83/26b1864921869784355459f374896dcf8b44d4af3b15d7697e9156cb2de9/lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e", size = 4441955 }, + { url = "https://files.pythonhosted.org/packages/e0/d2/e9bff9fb359226c25cda3538f664f54f2804f4b37b0d7c944639e1a51f69/lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f", size = 5050778 }, + { url = "https://files.pythonhosted.org/packages/88/69/6972bfafa8cd3ddc8562b126dd607011e218e17be313a8b1b9cc5a0ee876/lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e", size = 4748628 }, + { url = "https://files.pythonhosted.org/packages/5d/ea/a6523c7c7f6dc755a6eed3d2f6d6646617cad4d3d6d8ce4ed71bfd2362c8/lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179", size = 5322215 }, + { url = "https://files.pythonhosted.org/packages/99/37/396fbd24a70f62b31d988e4500f2068c7f3fd399d2fd45257d13eab51a6f/lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a", size = 4813963 }, + { url = "https://files.pythonhosted.org/packages/09/91/e6136f17459a11ce1757df864b213efbeab7adcb2efa63efb1b846ab6723/lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3", size = 4923353 }, + { url = "https://files.pythonhosted.org/packages/1d/7c/2eeecf87c9a1fca4f84f991067c693e67340f2b7127fc3eca8fa29d75ee3/lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1", size = 4740541 }, + { url = "https://files.pythonhosted.org/packages/3b/ed/4c38ba58defca84f5f0d0ac2480fdcd99fc7ae4b28fc417c93640a6949ae/lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d", size = 5346504 }, + { url = "https://files.pythonhosted.org/packages/a5/22/bbd3995437e5745cb4c2b5d89088d70ab19d4feabf8a27a24cecb9745464/lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c", size = 4898077 }, + { url = "https://files.pythonhosted.org/packages/0a/6e/94537acfb5b8f18235d13186d247bca478fea5e87d224644e0fe907df976/lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99", size = 4946543 }, + { url = "https://files.pythonhosted.org/packages/8d/e8/4b15df533fe8e8d53363b23a41df9be907330e1fa28c7ca36893fad338ee/lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff", size = 4816841 }, + { url = "https://files.pythonhosted.org/packages/1a/e7/03f390ea37d1acda50bc538feb5b2bda6745b25731e4e76ab48fae7106bf/lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a", size = 5417341 }, + { url = "https://files.pythonhosted.org/packages/ea/99/d1133ab4c250da85a883c3b60249d3d3e7c64f24faff494cf0fd23f91e80/lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8", size = 5327539 }, + { url = "https://files.pythonhosted.org/packages/7d/ed/e6276c8d9668028213df01f598f385b05b55a4e1b4662ee12ef05dab35aa/lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d", size = 5012542 }, + { url = "https://files.pythonhosted.org/packages/36/88/684d4e800f5aa28df2a991a6a622783fb73cf0e46235cfa690f9776f032e/lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30", size = 3486454 }, + { url = "https://files.pythonhosted.org/packages/fc/82/ace5a5676051e60355bd8fb945df7b1ba4f4fb8447f2010fb816bfd57724/lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f", size = 3816857 }, + { url = "https://files.pythonhosted.org/packages/94/6a/42141e4d373903bfea6f8e94b2f554d05506dfda522ada5343c651410dc8/lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a", size = 8156284 }, + { url = "https://files.pythonhosted.org/packages/91/5e/fa097f0f7d8b3d113fb7312c6308af702f2667f22644441715be961f2c7e/lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd", size = 4432407 }, + { url = "https://files.pythonhosted.org/packages/2d/a1/b901988aa6d4ff937f2e5cfc114e4ec561901ff00660c3e56713642728da/lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51", size = 5048331 }, + { url = "https://files.pythonhosted.org/packages/30/0f/b2a54f48e52de578b71bbe2a2f8160672a8a5e103df3a78da53907e8c7ed/lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b", size = 4744835 }, + { url = "https://files.pythonhosted.org/packages/82/9d/b000c15538b60934589e83826ecbc437a1586488d7c13f8ee5ff1f79a9b8/lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002", size = 5316649 }, + { url = "https://files.pythonhosted.org/packages/e3/ee/ffbb9eaff5e541922611d2c56b175c45893d1c0b8b11e5a497708a6a3b3b/lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4", size = 4812046 }, + { url = "https://files.pythonhosted.org/packages/15/ff/7ff89d567485c7b943cdac316087f16b2399a8b997007ed352a1248397e5/lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492", size = 4918597 }, + { url = "https://files.pythonhosted.org/packages/c6/a3/535b6ed8c048412ff51268bdf4bf1cf052a37aa7e31d2e6518038a883b29/lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3", size = 4738071 }, + { url = "https://files.pythonhosted.org/packages/7a/8f/cbbfa59cb4d4fd677fe183725a76d8c956495d7a3c7f111ab8f5e13d2e83/lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4", size = 5342213 }, + { url = "https://files.pythonhosted.org/packages/5c/fb/db4c10dd9958d4b52e34d1d1f7c1f434422aeaf6ae2bbaaff2264351d944/lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367", size = 4893749 }, + { url = "https://files.pythonhosted.org/packages/f2/38/bb4581c143957c47740de18a3281a0cab7722390a77cc6e610e8ebf2d736/lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832", size = 4945901 }, + { url = "https://files.pythonhosted.org/packages/fc/d5/18b7de4960c731e98037bd48fa9f8e6e8f2558e6fbca4303d9b14d21ef3b/lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff", size = 4815447 }, + { url = "https://files.pythonhosted.org/packages/97/a8/cd51ceaad6eb849246559a8ef60ae55065a3df550fc5fcd27014361c1bab/lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd", size = 5411186 }, + { url = "https://files.pythonhosted.org/packages/89/c3/1e3dabab519481ed7b1fdcba21dcfb8832f57000733ef0e71cf6d09a5e03/lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb", size = 5324481 }, + { url = "https://files.pythonhosted.org/packages/b6/17/71e9984cf0570cd202ac0a1c9ed5c1b8889b0fc8dc736f5ef0ffb181c284/lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b", size = 5011053 }, + { url = "https://files.pythonhosted.org/packages/69/68/9f7e6d3312a91e30829368c2b3217e750adef12a6f8eb10498249f4e8d72/lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957", size = 3485634 }, + { url = "https://files.pythonhosted.org/packages/7d/db/214290d58ad68c587bd5d6af3d34e56830438733d0d0856c0275fde43652/lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d", size = 3814417 }, + { url = "https://files.pythonhosted.org/packages/89/a9/63af38c7f42baff8251d937be91c6decfe9e4725fe16283dcee428e08d5c/lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de", size = 8129239 }, + { url = "https://files.pythonhosted.org/packages/23/b2/45e12a5b8508ee9de0af432d0dc5fcc786cd78037d692a3de7571c2db04c/lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc", size = 4415821 }, + { url = "https://files.pythonhosted.org/packages/88/88/a01dc8055d431c39859ec3806dbe4df6cf7a80b0431227a52de8428d2cf6/lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be", size = 5139927 }, + { url = "https://files.pythonhosted.org/packages/13/d9/c0f3fd5582a26ea887122feb9cfe84215642ecf10886dcb50a603a6ef448/lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a", size = 4839659 }, + { url = "https://files.pythonhosted.org/packages/64/06/290728f6fde1761c323db28ece9601018db72ecafa21b182cfea99e7cb2e/lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540", size = 5427269 }, + { url = "https://files.pythonhosted.org/packages/52/43/af104743bb733e85efc0be0e32c140e3e7be6050aca52b1e8a0b2867c382/lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70", size = 4876667 }, + { url = "https://files.pythonhosted.org/packages/d8/5f/9dea130ae3ba77848f4b93d11dfd365085620fb34c5c9d22746227b86952/lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa", size = 5013541 }, + { url = "https://files.pythonhosted.org/packages/e8/87/a089806f0327ad7f7268c3f4d22f1d76215a923bf33ea808bb665bdeacfa/lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf", size = 4818394 }, + { url = "https://files.pythonhosted.org/packages/87/63/b36ddd4a829a5de681bde7e9be4008a8b53c392dea4c8b1492c35727e150/lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229", size = 5472977 }, + { url = "https://files.pythonhosted.org/packages/99/1f/677226f48e2d1ea590c24f3ead1799584517a62a394a338b96f62d3c732e/lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe", size = 4978803 }, + { url = "https://files.pythonhosted.org/packages/9d/f8/1b96af1396f237de488b14f70b2c6ced5079b792770e6a0f7153f912124d/lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2", size = 5026166 }, + { url = "https://files.pythonhosted.org/packages/a9/42/86a09a2cabb7bed04d904e38cc09ac65e4916fc1b7eadf94bb924893988b/lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71", size = 4890234 }, + { url = "https://files.pythonhosted.org/packages/c9/0a/bf0edfe5635ed05ed69a8ae9c1e06dc28cf8becc4ea72f39d3624f20b3d9/lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3", size = 5533730 }, + { url = "https://files.pythonhosted.org/packages/00/cd/dfd8fd56415508751caac07c7ddb3b0a40aff346c11fabdd9d8aa2bfb329/lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727", size = 5406452 }, + { url = "https://files.pythonhosted.org/packages/3f/35/fcc233c86f4e59f9498cde8ad6131e1ca41dc7aa084ec982d2cccca91cd7/lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a", size = 5078114 }, + { url = "https://files.pythonhosted.org/packages/9b/55/94c9bc55ec20744a21c949138649442298cff4189067b7e0844dd0a111d0/lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff", size = 3478072 }, + { url = "https://files.pythonhosted.org/packages/bb/ab/68821837e454c4c34f40cbea8806637ec4d814b76d3d017a24a39c651a79/lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2", size = 3806100 }, + { url = "https://files.pythonhosted.org/packages/99/f7/b73a431c8500565aa500e99e60b448d305eaf7c0b4c893c7c5a8a69cc595/lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c", size = 3925431 }, + { url = "https://files.pythonhosted.org/packages/db/48/4a206623c0d093d0e3b15f415ffb4345b0bdf661a3d0b15a112948c033c7/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a", size = 4216683 }, + { url = "https://files.pythonhosted.org/packages/54/47/577820c45dd954523ae8453b632d91e76da94ca6d9ee40d8c98dd86f916b/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005", size = 4326732 }, + { url = "https://files.pythonhosted.org/packages/68/de/96cb6d3269bc994b4f5ede8ca7bf0840f5de0a278bc6e50cb317ff71cafa/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce", size = 4218377 }, + { url = "https://files.pythonhosted.org/packages/a5/43/19b1ef6cbffa4244a217f95cc5f41a6cb4720fed33510a49670b03c5f1a0/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83", size = 4351237 }, + { url = "https://files.pythonhosted.org/packages/ba/b2/6a22fb5c0885da3b00e116aee81f0b829ec9ac8f736cd414b4a09413fc7d/lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba", size = 3487557 }, + { url = "https://files.pythonhosted.org/packages/c9/ac/e8ec7b6f7d76f8b88dfe78dd547b0d8915350160a5a01cca7aceba91e87f/lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21", size = 3923032 }, + { url = "https://files.pythonhosted.org/packages/f7/b6/d94041c11aa294a09ffac7caa633114941935938eaaba159a93985283c07/lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2", size = 4214557 }, + { url = "https://files.pythonhosted.org/packages/dd/0d/ccb5e4e7a4188a9c881a3c07ee7eaf21772ae847ca5e9a3b140341f2668a/lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f", size = 4325217 }, + { url = "https://files.pythonhosted.org/packages/7a/17/9d3b43b63b0ddd77f1a680edf00de3c8c2441e8d379be17d2b712b67688b/lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab", size = 4216018 }, + { url = "https://files.pythonhosted.org/packages/19/4f/f71029b3f37f43e846b6ec0d6baaa1791c65f8c3356cc78d18076f4c5422/lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9", size = 4347893 }, + { url = "https://files.pythonhosted.org/packages/17/45/0fe53cb16a704b35b5ec93af305f77a14ec65830fc399e6634a81f17a1ea/lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c", size = 3486287 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "0.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/41/bae1254e0396c0cc8cf1751cb7d9afc90a602353695af5952530482c963f/MarkupSafe-0.23.tar.gz", hash = "sha256:a4ec1aff59b95a14b45eb2e23761a0179e98319da5a7eb76b56ea8cdc7b871c3", size = 13416 } + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "more-itertools" +version = "10.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", size = 121020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", size = 60952 }, +] + +[[package]] +name = "nh3" +version = "0.2.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/73/10df50b42ddb547a907deeb2f3c9823022580a7a47281e8eae8e003a9639/nh3-0.2.18.tar.gz", hash = "sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4", size = 15028 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/89/1daff5d9ba5a95a157c092c7c5f39b8dd2b1ddb4559966f808d31cfb67e0/nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86", size = 1374474 }, + { url = "https://files.pythonhosted.org/packages/2c/b6/42fc3c69cabf86b6b81e4c051a9b6e249c5ba9f8155590222c2622961f58/nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811", size = 694573 }, + { url = "https://files.pythonhosted.org/packages/45/b9/833f385403abaf0023c6547389ec7a7acf141ddd9d1f21573723a6eab39a/nh3-0.2.18-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200", size = 844082 }, + { url = "https://files.pythonhosted.org/packages/05/2b/85977d9e11713b5747595ee61f381bc820749daf83f07b90b6c9964cf932/nh3-0.2.18-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164", size = 782460 }, + { url = "https://files.pythonhosted.org/packages/72/f2/5c894d5265ab80a97c68ca36f25c8f6f0308abac649aaf152b74e7e854a8/nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189", size = 879827 }, + { url = "https://files.pythonhosted.org/packages/ab/a7/375afcc710dbe2d64cfbd69e31f82f3e423d43737258af01f6a56d844085/nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad", size = 841080 }, + { url = "https://files.pythonhosted.org/packages/c2/a8/3bb02d0c60a03ad3a112b76c46971e9480efa98a8946677b5a59f60130ca/nh3-0.2.18-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b", size = 924144 }, + { url = "https://files.pythonhosted.org/packages/1b/63/6ab90d0e5225ab9780f6c9fb52254fa36b52bb7c188df9201d05b647e5e1/nh3-0.2.18-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307", size = 769192 }, + { url = "https://files.pythonhosted.org/packages/a4/17/59391c28580e2c32272761629893e761442fc7666da0b1cdb479f3b67b88/nh3-0.2.18-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f", size = 791042 }, + { url = "https://files.pythonhosted.org/packages/a3/da/0c4e282bc3cff4a0adf37005fa1fb42257673fbc1bbf7d1ff639ec3d255a/nh3-0.2.18-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe", size = 1010073 }, + { url = "https://files.pythonhosted.org/packages/de/81/c291231463d21da5f8bba82c8167a6d6893cc5419b0639801ee5d3aeb8a9/nh3-0.2.18-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a", size = 1029782 }, + { url = "https://files.pythonhosted.org/packages/63/1d/842fed85cf66c973be0aed8770093d6a04741f65e2c388ddd4c07fd3296e/nh3-0.2.18-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50", size = 942504 }, + { url = "https://files.pythonhosted.org/packages/eb/61/73a007c74c37895fdf66e0edcd881f5eaa17a348ff02f4bb4bc906d61085/nh3-0.2.18-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204", size = 941541 }, + { url = "https://files.pythonhosted.org/packages/78/48/54a788fc9428e481b2f58e0cd8564f6c74ffb6e9ef73d39e8acbeae8c629/nh3-0.2.18-cp37-abi3-win32.whl", hash = "sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be", size = 573750 }, + { url = "https://files.pythonhosted.org/packages/26/8d/53c5b19c4999bdc6ba95f246f4ef35ca83d7d7423e5e38be43ad66544e5d/nh3-0.2.18-cp37-abi3-win_amd64.whl", hash = "sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844", size = 579012 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "opc-diag" +version = "1.0.1" +source = { editable = "." } +dependencies = [ + { name = "lxml" }, +] + +[package.dev-dependencies] +dev = [ + { name = "alabaster" }, + { name = "behave" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "sphinx" }, + { name = "tox" }, + { name = "twine" }, + { name = "types-lxml" }, +] + +[package.metadata] +requires-dist = [ + { name = "lxml", specifier = ">=4" }, + { name = "opc-diag", editable = "." }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "alabaster", specifier = "<0.7.14" }, + { name = "behave", specifier = ">=1.2.6" }, + { name = "jinja2", specifier = "==2.11.3" }, + { name = "markupsafe", specifier = "==0.23" }, + { name = "pyright", specifier = ">=1.1.381" }, + { name = "pytest", specifier = ">=8.3.3" }, + { name = "pytest-cov", specifier = ">=5.0.0" }, + { name = "ruff", specifier = ">=0.6.7" }, + { name = "sphinx", specifier = "==1.8.6" }, + { name = "tox", specifier = ">=4.20.0" }, + { name = "twine", specifier = ">=5.1.1" }, + { name = "types-lxml", specifier = ">=2024.9.16" }, +] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, +] + +[[package]] +name = "parse" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126 }, +] + +[[package]] +name = "parse-type" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parse" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/f4/176019f0f1aee502602d02bbe11a63211d7b031a42c0cd305a53765156c2/parse_type-0.6.3.tar.gz", hash = "sha256:8e99d2f52fab2f0f1f3d68ba9d026060140bf0e53680aada0111fb27b2f0e93a", size = 78224 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/1c/47b51f5d8e6eefaaf46027ca08586f2d96ccfd4137645d98ef9bcd79ac1a/parse_type-0.6.3-py2.py3-none-any.whl", hash = "sha256:8d94a52e0197fbad63fee8f70df16e6ed689e5e4f105b705c9afa7a30397a5aa", size = 27061 }, +] + +[[package]] +name = "pkginfo" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/72/347ec5be4adc85c182ed2823d8d1c7b51e13b9a6b0c1aae59582eca652df/pkginfo-1.10.0.tar.gz", hash = "sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297", size = 378457 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/09/054aea9b7534a15ad38a363a2bd974c20646ab1582a387a95b8df1bfea1c/pkginfo-1.10.0-py3-none-any.whl", hash = "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097", size = 30392 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pyproject-api" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/19/441e0624a8afedd15bbcce96df1b80479dd0ff0d965f5ce8fde4f2f6ffad/pyproject_api-1.8.0.tar.gz", hash = "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496", size = 22340 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/f4/3c4ddfcc0c19c217c6de513842d286de8021af2f2ab79bbb86c00342d778/pyproject_api-1.8.0-py3-none-any.whl", hash = "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228", size = 13100 }, +] + +[[package]] +name = "pyright" +version = "1.1.381" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/f4/8e2374423280cfb221a8eba3cb13d39276a05e592fea36bc06d5feb18c33/pyright-1.1.381.tar.gz", hash = "sha256:314cf0c1351c189524fb10c7ac20688ecd470e8cc505c394d642c9c80bf7c3a5", size = 17488 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/c0/fec7607edc2459816c49815cd5dac67b28c702ed497102118cdc2757cc8d/pyright-1.1.381-py3-none-any.whl", hash = "sha256:5dc0aa80a265675d36abab59c674ae01dbe476714f91845b61b841d34aa99081", size = 18221 }, +] + +[[package]] +name = "pytest" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, +] + +[[package]] +name = "readme-renderer" +version = "43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/b5/536c775084d239df6345dccf9b043419c7e3308bc31be4c7882196abc62e/readme_renderer-43.0.tar.gz", hash = "sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311", size = 31768 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/be/3ea20dc38b9db08387cf97997a85a7d51527ea2057d71118feb0aa8afa55/readme_renderer-43.0-py3-none-any.whl", hash = "sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9", size = 13301 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326 }, +] + +[[package]] +name = "rich" +version = "13.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/76/40f084cb7db51c9d1fa29a7120717892aeda9a7711f6225692c957a93535/rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a", size = 222080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/11/dadb85e2bd6b1f1ae56669c3e1f0410797f9605d752d68fb47b77f525b31/rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06", size = 241608 }, +] + +[[package]] +name = "ruff" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/7c/3045a526c57cef4b5ec4d5d154692e31429749a49810a53e785de334c4f6/ruff-0.6.7.tar.gz", hash = "sha256:44e52129d82266fa59b587e2cd74def5637b730a69c4542525dfdecfaae38bd5", size = 3073785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/c4/1c5c636f83f905c537785016e9cdd7a36df53c025a2d07940580ecb37bcf/ruff-0.6.7-py3-none-linux_armv6l.whl", hash = "sha256:08277b217534bfdcc2e1377f7f933e1c7957453e8a79764d004e44c40db923f2", size = 10336748 }, + { url = "https://files.pythonhosted.org/packages/84/d9/aa15a56be7ad796f4d7625362aff588f9fc013bbb7323a63571628a2cf2d/ruff-0.6.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c6707a32e03b791f4448dc0dce24b636cbcdee4dd5607adc24e5ee73fd86c00a", size = 9958833 }, + { url = "https://files.pythonhosted.org/packages/27/25/5dd1c32bfc3ad3136c8ebe84312d1bdd2e6c908ac7f60692ec009b7050a8/ruff-0.6.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:533d66b7774ef224e7cf91506a7dafcc9e8ec7c059263ec46629e54e7b1f90ab", size = 9633369 }, + { url = "https://files.pythonhosted.org/packages/0e/3e/01b25484f3cb08fe6fddedf1f55f3f3c0af861a5b5f5082fbe60ab4b2596/ruff-0.6.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17a86aac6f915932d259f7bec79173e356165518859f94649d8c50b81ff087e9", size = 10637415 }, + { url = "https://files.pythonhosted.org/packages/8a/c9/5bb9b849e4777e0f961de43edf95d2af0ab34999a5feee957be096887876/ruff-0.6.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3f8822defd260ae2460ea3832b24d37d203c3577f48b055590a426a722d50ef", size = 10097389 }, + { url = "https://files.pythonhosted.org/packages/52/cf/e08f1c290c7d848ddfb2ae811f24f445c18e1d3e50e01c38ffa7f5a50494/ruff-0.6.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba4efe5c6dbbb58be58dd83feedb83b5e95c00091bf09987b4baf510fee5c99", size = 10951440 }, + { url = "https://files.pythonhosted.org/packages/a2/2d/ca8aa0da5841913c302d8034c6de0ce56c401c685184d8dd23cfdd0003f9/ruff-0.6.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:525201b77f94d2b54868f0cbe5edc018e64c22563da6c5c2e5c107a4e85c1c0d", size = 11708900 }, + { url = "https://files.pythonhosted.org/packages/89/fc/9a83c57baee977c82392e19a328b52cebdaf61601af3d99498e278ef5104/ruff-0.6.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8854450839f339e1049fdbe15d875384242b8e85d5c6947bb2faad33c651020b", size = 11258892 }, + { url = "https://files.pythonhosted.org/packages/d3/a3/254cc7afef702c68ae9079290c2a1477ae0e81478589baf745026d8a4eb5/ruff-0.6.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f0b62056246234d59cbf2ea66e84812dc9ec4540518e37553513392c171cb18", size = 12367932 }, + { url = "https://files.pythonhosted.org/packages/9f/55/53f10c1bd8c3b2ae79aed18e62b22c6346f9296aa0ec80489b8442bd06a9/ruff-0.6.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b1462fa56c832dc0cea5b4041cfc9c97813505d11cce74ebc6d1aae068de36b", size = 10838629 }, + { url = "https://files.pythonhosted.org/packages/84/72/fb335c2b25432c63d15383ecbd7bfc1915e68cdf8d086a08042052144255/ruff-0.6.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:02b083770e4cdb1495ed313f5694c62808e71764ec6ee5db84eedd82fd32d8f5", size = 10648824 }, + { url = "https://files.pythonhosted.org/packages/92/a8/d57e135a8ad99b6a0c6e2a5c590bcacdd57f44340174f4409c3893368610/ruff-0.6.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c05fd37013de36dfa883a3854fae57b3113aaa8abf5dea79202675991d48624", size = 10174368 }, + { url = "https://files.pythonhosted.org/packages/a7/6f/1a30a6e81dcf2fa9ff3f7011eb87fe76c12a3c6bba74db6a1977d763de1f/ruff-0.6.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f49c9caa28d9bbfac4a637ae10327b3db00f47d038f3fbb2195c4d682e925b14", size = 10514383 }, + { url = "https://files.pythonhosted.org/packages/0b/25/df6f2575bc9fe43a6dedfd8dee12896f09a94303e2c828d5f85856bb69a0/ruff-0.6.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a0e1655868164e114ba43a908fd2d64a271a23660195017c17691fb6355d59bb", size = 10902340 }, + { url = "https://files.pythonhosted.org/packages/68/62/f2c1031e2fb7b94f9bf0603744e73db4ef90081b0eb1b9639a6feefd52ea/ruff-0.6.7-py3-none-win32.whl", hash = "sha256:a939ca435b49f6966a7dd64b765c9df16f1faed0ca3b6f16acdf7731969deb35", size = 8448033 }, + { url = "https://files.pythonhosted.org/packages/97/80/193d1604a3f7d75eb1b2a7ce6bf0fdbdbc136889a65caacea6ffb29501b1/ruff-0.6.7-py3-none-win_amd64.whl", hash = "sha256:590445eec5653f36248584579c06252ad2e110a5d1f32db5420de35fb0e1c977", size = 9273543 }, + { url = "https://files.pythonhosted.org/packages/8e/a8/4abb5a9f58f51e4b1ea386be5ab2e547035bc1ee57200d1eca2f8909a33e/ruff-0.6.7-py3-none-win_arm64.whl", hash = "sha256:b28f0d5e2f771c1fe3c7a45d3f53916fc74a480698c4b5731f0bea61e52137c8", size = 8618044 }, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221 }, +] + +[[package]] +name = "setuptools" +version = "75.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/b8/f21073fde99492b33ca357876430822e4800cdf522011f18041351dfa74b/setuptools-75.1.0.tar.gz", hash = "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538", size = 1348057 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/ae/f19306b5a221f6a436d8f2238d5b80925004093fa3edea59835b514d9057/setuptools-75.1.0-py3-none-any.whl", hash = "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2", size = 1248506 }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, +] + +[[package]] +name = "sphinx" +version = "1.8.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "six" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-websupport" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/74/5cef400220b2f22a4c85540b9ba20234525571b8b851be8a9ac219326a11/Sphinx-1.8.6.tar.gz", hash = "sha256:e096b1b369dbb0fcb95a31ba8c9e1ae98c588e601f08eada032248e1696de4b1", size = 5816141 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/da/e1b65da61267aeb92a76b6b6752430bcc076d98b723687929eb3d2e0d128/Sphinx-1.8.6-py2.py3-none-any.whl", hash = "sha256:5973adbb19a5de30e15ab394ec8bc05700317fa83f122c349dd01804d983720f", size = 3110177 }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, +] + +[[package]] +name = "sphinxcontrib-websupport" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/aa/b03a3f569a52b6f21a579d168083a27036c1f606269e34abdf5b70fe3a2c/sphinxcontrib-websupport-1.2.4.tar.gz", hash = "sha256:4edf0223a0685a7c485ae5a156b6f529ba1ee481a1417817935b20bde1956232", size = 602360 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/e5/2a547830845e6e6e5d97b3246fc1e3ec74cba879c9adc5a8e27f1291bca3/sphinxcontrib_websupport-1.2.4-py2.py3-none-any.whl", hash = "sha256:6fc9287dfc823fe9aa432463edd6cea47fa9ebbf488d7f289b322ffcfca075c7", size = 39924 }, +] + +[[package]] +name = "tomli" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, +] + +[[package]] +name = "tox" +version = "4.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "chardet" }, + { name = "colorama" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pluggy" }, + { name = "pyproject-api" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/4a/55f9dba99aad874ae54a7fb2310c940e978fd0155eb3576ddebec000fca7/tox-4.20.0.tar.gz", hash = "sha256:5b78a49b6eaaeab3ae4186415e7c97d524f762ae967c63562687c3e5f0ec23d5", size = 181364 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/ee/6f9bf37f197578f98fb450f1aeebf4570f85b24b00d846bbde6e11489bd1/tox-4.20.0-py3-none-any.whl", hash = "sha256:21a8005e3d3fe5658a8e36b8ca3ed13a4230429063c5cc2a2fdac6ee5aa0de34", size = 157087 }, +] + +[[package]] +name = "twine" +version = "5.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "keyring" }, + { name = "pkginfo" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/68/bd982e5e949ef8334e6f7dcf76ae40922a8750aa2e347291ae1477a4782b/twine-5.1.1.tar.gz", hash = "sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db", size = 225531 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/ec/00f9d5fd040ae29867355e559a94e9a8429225a0284a3f5f091a3878bfc0/twine-5.1.1-py3-none-any.whl", hash = "sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997", size = 38650 }, +] + +[[package]] +name = "types-beautifulsoup4" +version = "4.12.0.20240907" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-html5lib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/e3/138900d35df4e673935239f0259f4b984f11db32c99ceb4f6819c47a7cfc/types-beautifulsoup4-4.12.0.20240907.tar.gz", hash = "sha256:8d023b86530922070417a1d4c4d91678ab0ff2439b3b2b2cffa3b628b49ebab1", size = 11525 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/ce/bea8ea21414287b7fc0ad2fccdb571ad8e2f438d1a1d486d146e906ea041/types_beautifulsoup4-4.12.0.20240907-py3-none-any.whl", hash = "sha256:32f5ac48514b488f15241afdd7d2f73f0baf3c54e874e23b66708503dd288489", size = 12081 }, +] + +[[package]] +name = "types-html5lib" +version = "1.1.11.20240806" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/ac/a2ca5366f0337ae9c947d611c19116bd56e845976782aaa35247e2a699e8/types-html5lib-1.1.11.20240806.tar.gz", hash = "sha256:8060dc98baf63d6796a765bbbc809fff9f7a383f6e3a9add526f814c086545ef", size = 11269 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/df/ee52df5c2cb7f40f6b9d45fc11cc9256d3e237e04d57e2d797b448815fc7/types_html5lib-1.1.11.20240806-py3-none-any.whl", hash = "sha256:575c4fd84ba8eeeaa8520c7e4c7042b7791f5ec3e9c0a5d5c418124c42d9e7e4", size = 17260 }, +] + +[[package]] +name = "types-lxml" +version = "2024.9.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cssselect" }, + { name = "types-beautifulsoup4" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/f8/0773e385464af12bc047e38e15bf2c3c9b8602a85b3b6677ba44b8c72e22/types_lxml-2024.9.16.tar.gz", hash = "sha256:1005984c8da5ceb929b5f168a804b8b7217c8e0c6459fa205aa19fd8d75571ab", size = 113822 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/a9/3e28019b43f7cc621aa4133cdb72166603dadfc22afd6b54241dcec6b960/types_lxml-2024.9.16-py3-none-any.whl", hash = "sha256:bde062b76e701555aa84c23ba1cc6b22a3855dc1bf6970f48c04f2aab3ba806d", size = 79110 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "virtualenv" +version = "20.26.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/4c/66ce54c8736ff164e85117ca36b02a1e14c042a6963f85eeda82664fda4e/virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4", size = 9371932 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/1d/e1a44fdd6d30829ba21fc58b5d98a67e7aae8f4165f11d091e53aec12560/virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6", size = 5999288 }, +] + +[[package]] +name = "zipp" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200 }, +]