diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9d866e3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..fa18869 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,100 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '25 4 * * 5' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000..86ec200 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,12 @@ +name: Ruff +on: [ push, pull_request] +permissions: + contents: read +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v3 + with: + src: "./src/opc/" diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml new file mode 100644 index 0000000..5f74bb9 --- /dev/null +++ b/.github/workflows/tox.yml @@ -0,0 +1,31 @@ +name: Tox + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + python-version: [ '3.11', '3.12', '3.13'] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest lxml lxml-util tox tox-gh-actions + - name: Test with tox + run: tox diff --git a/.gitignore b/.gitignore index 23b4763..b237d20 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,5 @@ cython_debug/ #.idea/ .vscode/ +.python-version +.poetry.lock diff --git a/Makefile b/Makefile index 99aac9a..4d799a2 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ help: @echo OPTIONS: @echo " help" @echo " test" + @echo " mypy" @echo " docclean" @echo " docapidoc" @echo " dochtml" @@ -18,7 +19,10 @@ help: .PHONY = help, test, docclean, docapidoc, dochtml, clean, setup, build, testupload, upload, tox test: - pytest -s -v tests/ + poetry run pytest --cov-report term-missing --cov-report html --cov-branch --cov src + +mypy: + poetry run mypy src docclean: $(MAKE) -C docs clean @@ -51,4 +55,4 @@ upload: tox: $(MAKE) clean - tox \ No newline at end of file + tox diff --git a/README.md b/README.md index af12dee..5593c7c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ python-opc-lite is a light weight python library for Open Package Convention used for Open Office XML packages +![RUFF](https://github.com/mholschbach/python-opc-lite/actions/workflows/ruff.yml/badge.svg) +![TOX](https://github.com/mholschbach/python-opc-lite/actions/workflows/tox.yml/badge.svg) +![Dependabot](https://github.com/mholschbach/python-opc-lite/actions/workflows/dependabot/dependabot-updates/badge.svg) ## Overview diff --git a/docs/conf.py b/docs/conf.py index bdc94cf..61889ce 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,14 +4,15 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html import os import sys -sys.path.insert(0, os.path.abspath('../src')) + +sys.path.insert(0, os.path.abspath("../src")) # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = 'python-opc-lite' -copyright = '2023, Karim S' -author = 'Karim S' +project = "python-opc-lite" +copyright = "2023, Karim S" +author = "Karim S" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -25,20 +26,19 @@ "sphinx_inline_tabs", "sphinx_removed_in", "sphinxext.opengraph", - ] intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} -templates_path = ['_templates'] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'furo' -html_static_path = ['_static'] +html_theme = "furo" +html_static_path = ["_static"] pygments_style = "sphinx" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..21d9dac --- /dev/null +++ b/poetry.lock @@ -0,0 +1,836 @@ +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. + +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.7.0" +groups = ["dev"] +files = [ + {file = "beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b"}, + {file = "beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195"}, +] + +[package.dependencies] +soupsieve = ">1.2" +typing-extensions = ">=4.0.0" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "cachetools" +version = "7.0.5" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114"}, + {file = "cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.10.6" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356"}, + {file = "coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301"}, + {file = "coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460"}, + {file = "coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd"}, + {file = "coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb"}, + {file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6"}, + {file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945"}, + {file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e"}, + {file = "coverage-7.10.6-cp310-cp310-win32.whl", hash = "sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1"}, + {file = "coverage-7.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528"}, + {file = "coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f"}, + {file = "coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc"}, + {file = "coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a"}, + {file = "coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a"}, + {file = "coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62"}, + {file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153"}, + {file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5"}, + {file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619"}, + {file = "coverage-7.10.6-cp311-cp311-win32.whl", hash = "sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba"}, + {file = "coverage-7.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e"}, + {file = "coverage-7.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c"}, + {file = "coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea"}, + {file = "coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634"}, + {file = "coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6"}, + {file = "coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9"}, + {file = "coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c"}, + {file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a"}, + {file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5"}, + {file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972"}, + {file = "coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d"}, + {file = "coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629"}, + {file = "coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80"}, + {file = "coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6"}, + {file = "coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80"}, + {file = "coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003"}, + {file = "coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27"}, + {file = "coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4"}, + {file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d"}, + {file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc"}, + {file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc"}, + {file = "coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e"}, + {file = "coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32"}, + {file = "coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2"}, + {file = "coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b"}, + {file = "coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393"}, + {file = "coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27"}, + {file = "coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df"}, + {file = "coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb"}, + {file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282"}, + {file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4"}, + {file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21"}, + {file = "coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0"}, + {file = "coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5"}, + {file = "coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b"}, + {file = "coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e"}, + {file = "coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb"}, + {file = "coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034"}, + {file = "coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1"}, + {file = "coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a"}, + {file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb"}, + {file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d"}, + {file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747"}, + {file = "coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5"}, + {file = "coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713"}, + {file = "coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32"}, + {file = "coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65"}, + {file = "coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6"}, + {file = "coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0"}, + {file = "coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e"}, + {file = "coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5"}, + {file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7"}, + {file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5"}, + {file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0"}, + {file = "coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7"}, + {file = "coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930"}, + {file = "coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b"}, + {file = "coverage-7.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90558c35af64971d65fbd935c32010f9a2f52776103a259f1dee865fe8259352"}, + {file = "coverage-7.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8953746d371e5695405806c46d705a3cd170b9cc2b9f93953ad838f6c1e58612"}, + {file = "coverage-7.10.6-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c83f6afb480eae0313114297d29d7c295670a41c11b274e6bca0c64540c1ce7b"}, + {file = "coverage-7.10.6-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7eb68d356ba0cc158ca535ce1381dbf2037fa8cb5b1ae5ddfc302e7317d04144"}, + {file = "coverage-7.10.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b15a87265e96307482746d86995f4bff282f14b027db75469c446da6127433b"}, + {file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fc53ba868875bfbb66ee447d64d6413c2db91fddcfca57025a0e7ab5b07d5862"}, + {file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efeda443000aa23f276f4df973cb82beca682fd800bb119d19e80504ffe53ec2"}, + {file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9702b59d582ff1e184945d8b501ffdd08d2cee38d93a2206aa5f1365ce0b8d78"}, + {file = "coverage-7.10.6-cp39-cp39-win32.whl", hash = "sha256:2195f8e16ba1a44651ca684db2ea2b2d4b5345da12f07d9c22a395202a05b23c"}, + {file = "coverage-7.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:f32ff80e7ef6a5b5b606ea69a36e97b219cd9dc799bcf2963018a4d8f788cfbf"}, + {file = "coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3"}, + {file = "coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "cssselect" +version = "1.3.0" +description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d"}, + {file = "cssselect-1.3.0.tar.gz", hash = "sha256:57f8a99424cfab289a1b6a816a43075a4b00948c86b4dcf3ef4ee7e15f7ab0c7"}, +] + +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "filelock" +version = "3.25.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "filelock-3.25.1-py3-none-any.whl", hash = "sha256:18972df45473c4aa2c7921b609ee9ca4925910cc3a0fb226c96b92fc224ef7bf"}, + {file = "filelock-3.25.1.tar.gz", hash = "sha256:b9a2e977f794ef94d77cdf7d27129ac648a61f585bff3ca24630c1629f701aa9"}, +] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] + +[[package]] +name = "librt" +version = "0.6.3" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:45660d26569cc22ed30adf583389d8a0d1b468f8b5e518fcf9bfe2cd298f9dd1"}, + {file = "librt-0.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:54f3b2177fb892d47f8016f1087d21654b44f7fc4cf6571c1c6b3ea531ab0fcf"}, + {file = "librt-0.6.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c5b31bed2c2f2fa1fcb4815b75f931121ae210dc89a3d607fb1725f5907f1437"}, + {file = "librt-0.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f8ed5053ef9fb08d34f1fd80ff093ccbd1f67f147633a84cf4a7d9b09c0f089"}, + {file = "librt-0.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f0e4bd9bcb0ee34fa3dbedb05570da50b285f49e52c07a241da967840432513"}, + {file = "librt-0.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8f89c8d20dfa648a3f0a56861946eb00e5b00d6b00eea14bc5532b2fcfa8ef1"}, + {file = "librt-0.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecc2c526547eacd20cb9fbba19a5268611dbc70c346499656d6cf30fae328977"}, + {file = "librt-0.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fbedeb9b48614d662822ee514567d2d49a8012037fc7b4cd63f282642c2f4b7d"}, + {file = "librt-0.6.3-cp310-cp310-win32.whl", hash = "sha256:0765b0fe0927d189ee14b087cd595ae636bef04992e03fe6dfdaa383866c8a46"}, + {file = "librt-0.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:8c659f9fb8a2f16dc4131b803fa0144c1dadcb3ab24bb7914d01a6da58ae2457"}, + {file = "librt-0.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:61348cc488b18d1b1ff9f3e5fcd5ac43ed22d3e13e862489d2267c2337285c08"}, + {file = "librt-0.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64645b757d617ad5f98c08e07620bc488d4bced9ced91c6279cec418f16056fa"}, + {file = "librt-0.6.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:26b8026393920320bb9a811b691d73c5981385d537ffc5b6e22e53f7b65d4122"}, + {file = "librt-0.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d998b432ed9ffccc49b820e913c8f327a82026349e9c34fa3690116f6b70770f"}, + {file = "librt-0.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e18875e17ef69ba7dfa9623f2f95f3eda6f70b536079ee6d5763ecdfe6cc9040"}, + {file = "librt-0.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a218f85081fc3f70cddaed694323a1ad7db5ca028c379c214e3a7c11c0850523"}, + {file = "librt-0.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ef42ff4edd369e84433ce9b188a64df0837f4f69e3d34d3b34d4955c599d03f"}, + {file = "librt-0.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e0f2b79993fec23a685b3e8107ba5f8675eeae286675a216da0b09574fa1e47"}, + {file = "librt-0.6.3-cp311-cp311-win32.whl", hash = "sha256:fd98cacf4e0fabcd4005c452cb8a31750258a85cab9a59fb3559e8078da408d7"}, + {file = "librt-0.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:e17b5b42c8045867ca9d1f54af00cc2275198d38de18545edaa7833d7e9e4ac8"}, + {file = "librt-0.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:87597e3d57ec0120a3e1d857a708f80c02c42ea6b00227c728efbc860f067c45"}, + {file = "librt-0.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74418f718083009108dc9a42c21bf2e4802d49638a1249e13677585fcc9ca176"}, + {file = "librt-0.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:514f3f363d1ebc423357d36222c37e5c8e6674b6eae8d7195ac9a64903722057"}, + {file = "librt-0.6.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cf1115207a5049d1f4b7b4b72de0e52f228d6c696803d94843907111cbf80610"}, + {file = "librt-0.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad8ba80cdcea04bea7b78fcd4925bfbf408961e9d8397d2ee5d3ec121e20c08c"}, + {file = "librt-0.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4018904c83eab49c814e2494b4e22501a93cdb6c9f9425533fe693c3117126f9"}, + {file = "librt-0.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8983c5c06ac9c990eac5eb97a9f03fe41dc7e9d7993df74d9e8682a1056f596c"}, + {file = "librt-0.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7769c579663a6f8dbf34878969ac71befa42067ce6bf78e6370bf0d1194997c"}, + {file = "librt-0.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d3c9a07eafdc70556f8c220da4a538e715668c0c63cabcc436a026e4e89950bf"}, + {file = "librt-0.6.3-cp312-cp312-win32.whl", hash = "sha256:38320386a48a15033da295df276aea93a92dfa94a862e06893f75ea1d8bbe89d"}, + {file = "librt-0.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:c0ecf4786ad0404b072196b5df774b1bb23c8aacdcacb6c10b4128bc7b00bd01"}, + {file = "librt-0.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:9f2a6623057989ebc469cd9cc8fe436c40117a0147627568d03f84aef7854c55"}, + {file = "librt-0.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e716f9012148a81f02f46a04fc4c663420c6fbfeacfac0b5e128cf43b4413d3"}, + {file = "librt-0.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:669ff2495728009a96339c5ad2612569c6d8be4474e68f3f3ac85d7c3261f5f5"}, + {file = "librt-0.6.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:349b6873ebccfc24c9efd244e49da9f8a5c10f60f07575e248921aae2123fc42"}, + {file = "librt-0.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c74c26736008481c9f6d0adf1aedb5a52aff7361fea98276d1f965c0256ee70"}, + {file = "librt-0.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:408a36ddc75e91918cb15b03460bdc8a015885025d67e68c6f78f08c3a88f522"}, + {file = "librt-0.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e61ab234624c9ffca0248a707feffe6fac2343758a36725d8eb8a6efef0f8c30"}, + {file = "librt-0.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:324462fe7e3896d592b967196512491ec60ca6e49c446fe59f40743d08c97917"}, + {file = "librt-0.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36b2ec8c15030002c7f688b4863e7be42820d7c62d9c6eece3db54a2400f0530"}, + {file = "librt-0.6.3-cp313-cp313-win32.whl", hash = "sha256:25b1b60cb059471c0c0c803e07d0dfdc79e41a0a122f288b819219ed162672a3"}, + {file = "librt-0.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:10a95ad074e2a98c9e4abc7f5b7d40e5ecbfa84c04c6ab8a70fabf59bd429b88"}, + {file = "librt-0.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:17000df14f552e86877d67e4ab7966912224efc9368e998c96a6974a8d609bf9"}, + {file = "librt-0.6.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8e695f25d1a425ad7a272902af8ab8c8d66c1998b177e4b5f5e7b4e215d0c88a"}, + {file = "librt-0.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3e84a4121a7ae360ca4da436548a9c1ca8ca134a5ced76c893cc5944426164bd"}, + {file = "librt-0.6.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:05f385a414de3f950886ea0aad8f109650d4b712cf9cc14cc17f5f62a9ab240b"}, + {file = "librt-0.6.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36a8e337461150b05ca2c7bdedb9e591dfc262c5230422cea398e89d0c746cdc"}, + {file = "librt-0.6.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcbe48f6a03979384f27086484dc2a14959be1613cb173458bd58f714f2c48f3"}, + {file = "librt-0.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4bca9e4c260233fba37b15c4ec2f78aa99c1a79fbf902d19dd4a763c5c3fb751"}, + {file = "librt-0.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:760c25ed6ac968e24803eb5f7deb17ce026902d39865e83036bacbf5cf242aa8"}, + {file = "librt-0.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4a93a353ccff20df6e34fa855ae8fd788832c88f40a9070e3ddd3356a9f0e"}, + {file = "librt-0.6.3-cp314-cp314-win32.whl", hash = "sha256:cb92741c2b4ea63c09609b064b26f7f5d9032b61ae222558c55832ec3ad0bcaf"}, + {file = "librt-0.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:fdcd095b1b812d756fa5452aca93b962cf620694c0cadb192cec2bb77dcca9a2"}, + {file = "librt-0.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:822ca79e28720a76a935c228d37da6579edef048a17cd98d406a2484d10eda78"}, + {file = "librt-0.6.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:078cd77064d1640cb7b0650871a772956066174d92c8aeda188a489b58495179"}, + {file = "librt-0.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5cc22f7f5c0cc50ed69f4b15b9c51d602aabc4500b433aaa2ddd29e578f452f7"}, + {file = "librt-0.6.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:14b345eb7afb61b9fdcdfda6738946bd11b8e0f6be258666b0646af3b9bb5916"}, + {file = "librt-0.6.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d46aa46aa29b067f0b8b84f448fd9719aaf5f4c621cc279164d76a9dc9ab3e8"}, + {file = "librt-0.6.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b51ba7d9d5d9001494769eca8c0988adce25d0a970c3ba3f2eb9df9d08036fc"}, + {file = "librt-0.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ced0925a18fddcff289ef54386b2fc230c5af3c83b11558571124bfc485b8c07"}, + {file = "librt-0.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6bac97e51f66da2ca012adddbe9fd656b17f7368d439de30898f24b39512f40f"}, + {file = "librt-0.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b2922a0e8fa97395553c304edc3bd36168d8eeec26b92478e292e5d4445c1ef0"}, + {file = "librt-0.6.3-cp314-cp314t-win32.whl", hash = "sha256:f33462b19503ba68d80dac8a1354402675849259fb3ebf53b67de86421735a3a"}, + {file = "librt-0.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:04f8ce401d4f6380cfc42af0f4e67342bf34c820dae01343f58f472dbac75dcf"}, + {file = "librt-0.6.3-cp314-cp314t-win_arm64.whl", hash = "sha256:afb39550205cc5e5c935762c6bf6a2bb34f7d21a68eadb25e2db7bf3593fecc0"}, + {file = "librt-0.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09262cb2445b6f15d09141af20b95bb7030c6f13b00e876ad8fdd1a9045d6aa5"}, + {file = "librt-0.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57705e8eec76c5b77130d729c0f70190a9773366c555c5457c51eace80afd873"}, + {file = "librt-0.6.3-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3ac2a7835434b31def8ed5355dd9b895bbf41642d61967522646d1d8b9681106"}, + {file = "librt-0.6.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71f0a5918aebbea1e7db2179a8fe87e8a8732340d9e8b8107401fb407eda446e"}, + {file = "librt-0.6.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa346e202e6e1ebc01fe1c69509cffe486425884b96cb9ce155c99da1ecbe0e9"}, + {file = "librt-0.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:92267f865c7bbd12327a0d394666948b9bf4b51308b52947c0cc453bfa812f5d"}, + {file = "librt-0.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:86605d5bac340beb030cbc35859325982a79047ebdfba1e553719c7126a2389d"}, + {file = "librt-0.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:98e4bbecbef8d2a60ecf731d735602feee5ac0b32117dbbc765e28b054bac912"}, + {file = "librt-0.6.3-cp39-cp39-win32.whl", hash = "sha256:3caa0634c02d5ff0b2ae4a28052e0d8c5f20d497623dc13f629bd4a9e2a6efad"}, + {file = "librt-0.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:b47395091e7e0ece1e6ebac9b98bf0c9084d1e3d3b2739aa566be7e56e3f7bf2"}, + {file = "librt-0.6.3.tar.gz", hash = "sha256:c724a884e642aa2bbad52bb0203ea40406ad742368a5f90da1b220e970384aae"}, +] + +[[package]] +name = "lxml" +version = "5.4.0" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c"}, + {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776"}, + {file = "lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7"}, + {file = "lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250"}, + {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9"}, + {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751"}, + {file = "lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4"}, + {file = "lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539"}, + {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4"}, + {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc"}, + {file = "lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f"}, + {file = "lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2"}, + {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0"}, + {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a"}, + {file = "lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82"}, + {file = "lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f"}, + {file = "lxml-5.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7be701c24e7f843e6788353c055d806e8bd8466b52907bafe5d13ec6a6dbaecd"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb54f7c6bafaa808f27166569b1511fc42701a7713858dddc08afdde9746849e"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97dac543661e84a284502e0cf8a67b5c711b0ad5fb661d1bd505c02f8cf716d7"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:c70e93fba207106cb16bf852e421c37bbded92acd5964390aad07cb50d60f5cf"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9c886b481aefdf818ad44846145f6eaf373a20d200b5ce1a5c8e1bc2d8745410"}, + {file = "lxml-5.4.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:fa0e294046de09acd6146be0ed6727d1f42ded4ce3ea1e9a19c11b6774eea27c"}, + {file = "lxml-5.4.0-cp36-cp36m-win32.whl", hash = "sha256:61c7bbf432f09ee44b1ccaa24896d21075e533cd01477966a5ff5a71d88b2f56"}, + {file = "lxml-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121"}, + {file = "lxml-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f7f991a68d20c75cb13c5c9142b2a3f9eb161f1f12a9489c82172d1f133c0"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:ac7ba71f9561cd7d7b55e1ea5511543c0282e2b6450f122672a2694621d63b7e"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5"}, + {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:ce31158630a6ac85bddd6b830cffd46085ff90498b397bd0a259f59d27a12188"}, + {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6"}, + {file = "lxml-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063"}, + {file = "lxml-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49"}, + {file = "lxml-5.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eaf24066ad0b30917186420d51e2e3edf4b0e2ea68d8cd885b14dc8afdcf6556"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b31a3a77501d86d8ade128abb01082724c0dfd9524f542f2f07d693c9f1175f"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e108352e203c7afd0eb91d782582f00a0b16a948d204d4dec8565024fafeea5"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11a96c3b3f7551c8a8109aa65e8594e551d5a84c76bf950da33d0fb6dfafab7"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:ca755eebf0d9e62d6cb013f1261e510317a41bf4650f22963474a663fdfe02aa"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4cd915c0fb1bed47b5e6d6edd424ac25856252f09120e3e8ba5154b6b921860e"}, + {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:226046e386556a45ebc787871d6d2467b32c37ce76c2680f5c608e25823ffc84"}, + {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b108134b9667bcd71236c5a02aad5ddd073e372fb5d48ea74853e009fe38acb6"}, + {file = "lxml-5.4.0-cp38-cp38-win32.whl", hash = "sha256:1320091caa89805df7dcb9e908add28166113dcd062590668514dbd510798c88"}, + {file = "lxml-5.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:073eb6dcdf1f587d9b88c8c93528b57eccda40209cf9be549d469b942b41d70b"}, + {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e"}, + {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67ed8a40665b84d161bae3181aa2763beea3747f748bca5874b4af4d75998f87"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b4a3bd174cc9cdaa1afbc4620c049038b441d6ba07629d89a83b408e54c35cd"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:b0989737a3ba6cf2a16efb857fb0dfa20bc5c542737fddb6d893fde48be45433"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:dc0af80267edc68adf85f2a5d9be1cdf062f973db6790c1d065e45025fa26140"}, + {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:639978bccb04c42677db43c79bdaa23785dc7f9b83bfd87570da8207872f1ce5"}, + {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142"}, + {file = "lxml-5.4.0-cp39-cp39-win32.whl", hash = "sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6"}, + {file = "lxml-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5f11a1526ebd0dee85e7b1e39e39a0cc0d9d03fb527f56d8457f6df48a10dc0c"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b4afaf38bf79109bb060d9016fad014a9a48fb244e11b94f74ae366a64d252"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de6f6bb8a7840c7bf216fb83eec4e2f79f7325eca8858167b68708b929ab2172"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5cca36a194a4eb4e2ed6be36923d3cffd03dcdf477515dea687185506583d4c9"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b7c86884ad23d61b025989d99bfdd92a7351de956e01c61307cb87035960bcb1"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:53d9469ab5460402c19553b56c3648746774ecd0681b1b27ea74d5d8a3ef5590"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:56dbdbab0551532bb26c19c914848d7251d73edb507c3079d6805fa8bba5b706"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14479c2ad1cb08b62bb941ba8e0e05938524ee3c3114644df905d2331c76cd57"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32697d2ea994e0db19c1df9e40275ffe84973e4232b5c274f47e7c1ec9763cdd"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:24f6df5f24fc3385f622c0c9d63fe34604893bc1a5bdbb2dbf5870f85f9a404a"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:151d6c40bc9db11e960619d2bf2ec5829f0aaffb10b41dcf6ad2ce0f3c0b2325"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4025bf2884ac4370a3243c5aa8d66d3cb9e15d3ddd0af2d796eccc5f0244390e"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ab339536aa798b1e17750733663d272038bf28069761d5be57cb4a9b0137b4f8"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9776af1aad5a4b4a1317242ee2bea51da54b2a7b7b48674be736d463c999f37d"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987"}, + {file = "lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html-clean = ["lxml_html_clean"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=3.0.11,<3.1.0)"] + +[[package]] +name = "lxml-util" +version = "1.0.2" +description = "Utility library based on lxml" +optional = false +python-versions = ">=3" +groups = ["main"] +files = [ + {file = "lxml-util-1.0.2.tar.gz", hash = "sha256:2d45c26fe1b1af26b5f5b7c7fe31c9457fea5125822107b78e56506b44451e82"}, + {file = "lxml_util-1.0.2-py3-none-any.whl", hash = "sha256:ae322800c645f16fd7f234d97bbc170f22ca6c8c3c9ed8ed7e3927760a5b14e7"}, +] + +[package.dependencies] +lxml = "*" + +[package.extras] +dists = ["build", "twine"] +docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["pytest", "tox"] + +[[package]] +name = "mypy" +version = "1.19.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, + {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, + {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, + {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, + {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, + {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, + {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, + {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, + {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, + {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, + {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, + {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, + {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, + {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, + {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, + {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, +] + +[package.dependencies] +librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "26.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.9.4" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"}, + {file = "platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pygments" +version = "2.19.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyproject-api" +version = "1.10.0" +description = "API to interact with the python pyproject.toml based projects" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09"}, + {file = "pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330"}, +] + +[package.dependencies] +packaging = ">=25" + +[package.extras] +docs = ["furo (>=2025.9.25)", "sphinx-autodoc-typehints (>=3.5.1)"] +testing = ["covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)", "setuptools (>=80.9)"] + +[[package]] +name = "pytest" +version = "9.0.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, + {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, + {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, +] + +[package.dependencies] +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" + +[package.extras] +testing = ["process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "python-discovery" +version = "1.1.0" +description = "Python interpreter discovery" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "python_discovery-1.1.0-py3-none-any.whl", hash = "sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b"}, + {file = "python_discovery-1.1.0.tar.gz", hash = "sha256:447941ba1aed8cc2ab7ee3cb91be5fc137c5bdbb05b7e6ea62fbdcb66e50b268"}, +] + +[package.dependencies] +filelock = ">=3.15.4" +platformdirs = ">=4.3.6,<5" + +[package.extras] +docs = ["furo (>=2025.12.19)", "sphinx (>=9.1)", "sphinx-autodoc-typehints (>=3.6.3)", "sphinxcontrib-mermaid (>=2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] + +[[package]] +name = "ruff" +version = "0.15.5" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.15.5-py3-none-linux_armv6l.whl", hash = "sha256:4ae44c42281f42e3b06b988e442d344a5b9b72450ff3c892e30d11b29a96a57c"}, + {file = "ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6edd3792d408ebcf61adabc01822da687579a1a023f297618ac27a5b51ef0080"}, + {file = "ruff-0.15.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:89f463f7c8205a9f8dea9d658d59eff49db05f88f89cc3047fb1a02d9f344010"}, + {file = "ruff-0.15.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba786a8295c6574c1116704cf0b9e6563de3432ac888d8f83685654fe528fd65"}, + {file = "ruff-0.15.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd4b801e57955fe9f02b31d20375ab3a5c4415f2e5105b79fb94cf2642c91440"}, + {file = "ruff-0.15.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391f7c73388f3d8c11b794dbbc2959a5b5afe66642c142a6effa90b45f6f5204"}, + {file = "ruff-0.15.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc18f30302e379fe1e998548b0f5e9f4dff907f52f73ad6da419ea9c19d66c8"}, + {file = "ruff-0.15.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc6e7f90087e2d27f98dc34ed1b3ab7c8f0d273cc5431415454e22c0bd2a681"}, + {file = "ruff-0.15.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb7169f53c1ddb06e71a9aebd7e98fc0fea936b39afb36d8e86d36ecc2636a"}, + {file = "ruff-0.15.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9b037924500a31ee17389b5c8c4d88874cc6ea8e42f12e9c61a3d754ff72f1ca"}, + {file = "ruff-0.15.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65bb414e5b4eadd95a8c1e4804f6772bbe8995889f203a01f77ddf2d790929dd"}, + {file = "ruff-0.15.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d20aa469ae3b57033519c559e9bc9cd9e782842e39be05b50e852c7c981fa01d"}, + {file = "ruff-0.15.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:15388dd28c9161cdb8eda68993533acc870aa4e646a0a277aa166de9ad5a8752"}, + {file = "ruff-0.15.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b30da330cbd03bed0c21420b6b953158f60c74c54c5f4c1dabbdf3a57bf355d2"}, + {file = "ruff-0.15.5-py3-none-win32.whl", hash = "sha256:732e5ee1f98ba5b3679029989a06ca39a950cced52143a0ea82a2102cb592b74"}, + {file = "ruff-0.15.5-py3-none-win_amd64.whl", hash = "sha256:821d41c5fa9e19117616c35eaa3f4b75046ec76c65e7ae20a333e9a8696bc7fe"}, + {file = "ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b"}, + {file = "ruff-0.15.5.tar.gz", hash = "sha256:7c3601d3b6d76dce18c5c824fc8d06f4eef33d6df0c21ec7799510cde0f159a2"}, +] + +[[package]] +name = "soupsieve" +version = "2.7" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4"}, + {file = "soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a"}, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +description = "A lil' TOML writer" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90"}, + {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, +] + +[[package]] +name = "tox" +version = "4.49.1" +description = "tox is a generic virtualenv management and test command line tool" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "tox-4.49.1-py3-none-any.whl", hash = "sha256:6dd2d7d4e4fd5895ce4ea20e258fce0d4b81e914b697d116a5ab0365f8303bad"}, + {file = "tox-4.49.1.tar.gz", hash = "sha256:4130d02e1d53648d7107d121ed79f69a27b717817c5e9da521d50319dd261212"}, +] + +[package.dependencies] +cachetools = ">=7.0.3" +colorama = ">=0.4.6" +filelock = ">=3.25" +packaging = ">=26" +platformdirs = ">=4.9.4" +pluggy = ">=1.6" +pyproject-api = ">=1.10" +tomli-w = ">=1.2" +virtualenv = ">=21.1" + +[package.extras] +completion = ["argcomplete (>=3.6.3)"] + +[[package]] +name = "types-html5lib" +version = "1.1.11.20251117" +description = "Typing stubs for html5lib" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "types_html5lib-1.1.11.20251117-py3-none-any.whl", hash = "sha256:2a3fc935de788a4d2659f4535002a421e05bea5e172b649d33232e99d4272d08"}, + {file = "types_html5lib-1.1.11.20251117.tar.gz", hash = "sha256:1a6a3ac5394aa12bf547fae5d5eff91dceec46b6d07c4367d9b39a37f42f201a"}, +] + +[package.dependencies] +types-webencodings = "*" + +[[package]] +name = "types-lxml" +version = "2026.2.16" +description = "Complete lxml external type annotation" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "types_lxml-2026.2.16-py3-none-any.whl", hash = "sha256:5dd81ffa54830e5f361988737c5f1d6a0ae48b2742790637ec560df790ea0401"}, + {file = "types_lxml-2026.2.16.tar.gz", hash = "sha256:b3a1340cc06db98d541c785732f6f68bea438daff4e2b7809ef748d545d01406"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.13,<5.0" +cssselect = ">=1.2,<2.0" +types-html5lib = ">=1.1.11.20250809,<1.2.0.0" +typing_extensions = ">=4.15,<5.0" + +[package.extras] +dev = ["basedpyright (>=1.31.6) ; platform_machine != \"i686\"", "html5lib (==1.1)", "hypothesis (>=6.127.7)", "lxml (>=5.0)", "mypy (>=1.18.1)", "pook (>=2.0)", "pre-commit", "pyrefly (>=0.46.3)", "pyright (>=1.1.406)", "pytest (>=7.0,<9)", "pytest-revealtype-injector (>=0.9.0)", "rnc2rng", "tox (>=4.24,<5.0)", "ty (>=0.0.7)", "tzdata", "urllib3 (>=2.0)"] +mypy = ["mypy (>=1.18.1)"] + +[[package]] +name = "types-webencodings" +version = "0.5.0.20251108" +description = "Typing stubs for webencodings" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "types_webencodings-0.5.0.20251108-py3-none-any.whl", hash = "sha256:e21f81ff750795faffddaffd70a3d8bfff77d006f22c27e393eb7812586249d8"}, + {file = "types_webencodings-0.5.0.20251108.tar.gz", hash = "sha256:2378e2ceccced3d41bb5e21387586e7b5305e11519fc6b0659c629f23b2e5de4"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "virtualenv" +version = "21.1.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07"}, + {file = "virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} +platformdirs = ">=3.9.1,<5" +python-discovery = ">=1" + +[metadata] +lock-version = "2.1" +python-versions = ">=3.11" +content-hash = "328648a2a27fadb0251b4fa5136241ab2c3b29bc73e199e371e7391d4b8afa68" diff --git a/pyproject.toml b/pyproject.toml index af16421..b23b991 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,42 @@ +[project] +name = "python-opc-lite" +version = "0.0.2" +description = "" +authors = [ + {name = "Michael Holschbach", email = "mholschbach@gmail.com"} +] +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "lxml (>=5.3.2,<6.0.0)", + "lxml-util (>=1.0.2,<2.0.0)", +] + +[tool.poetry] +packages = [{include = "src"}] + +[tool.poetry.group.dev.dependencies] +pytest = "^9.0.2" +pytest-cov = "^7.0.0" +mypy = "^1.19.1" +ruff = "^0.15.5" +tox = "^4.49.1" +types-lxml = "^2026.2.16" + [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" +#requires = ["poetry-core>=2.0.0,<3.0.0"] +#build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] pythonpath = [ "src" ] +[tool.mypy] +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = [ "lxmlutil.etree" ] +follow_untyped_imports = true diff --git a/src/opc/__init__.py b/src/opc/__init__.py index 806ea18..7774afb 100644 --- a/src/opc/__init__.py +++ b/src/opc/__init__.py @@ -1,3 +1,3 @@ __version__ = "0.0.2" -from .package import Package +from .package import Package as Package diff --git a/src/opc/base.py b/src/opc/base.py index 3648895..0f26bd6 100644 --- a/src/opc/base.py +++ b/src/opc/base.py @@ -14,54 +14,61 @@ class Base(ABC): :param parent: parent object that will be returned through parent property """ - def __init__(self, parent): + def __init__(self, parent: object): self._parent = parent @property - def parent(self): + def parent(self) -> object: """Returns the parent object of current object""" return self._parent -class XmlBase(): +class XmlBase: """This is a base class for objects that has xml content. It is inherited by classes like |ct| """ - def __init__(self): + _e: etree._Element | None + + def __init__(self) -> None: self._e = None self._parser = Parser() @property - def e(self): + def e(self) -> etree._Element | None: """returns the underlying xml element object""" - return self._e + if self._e is not None: + return self._e + return None @e.setter - def e(self, value): + def e(self, value: etree._Element) -> None: self._e = value @property - def parser(self): + def parser(self) -> Parser: """returns the parser object""" return self._parser - def read(self, f): + def read(self, f) -> None: # type: ignore[no-untyped-def] """parse the given file object as xml and assigns the root element to self._e """ self._e = self._parser.parse(f).getroot() - def write(self, f): + def write(self, f) -> None: # type: ignore[no-untyped-def] """writes the xml content in self._e to the given file object. utf-8 encoding, xml_declaration and standalone properties are applied to xml """ - f.write(etree.tostring(self.e, encoding='UTF-8', - xml_declaration=True, standalone=True)) - + if self._e is not None: + f.write( + etree.tostring( + self._e, encoding="UTF-8", xml_declaration=True, standalone=True + ) + ) -class PartBase(): +class PartBase: """This is a abstract class that part and relspart classes inherits Objects of a class that inherits this class gets package property @@ -69,8 +76,10 @@ class PartBase(): """ + _parent: object + @property - def package(self): + def package(self) -> object: """property that returns the package object""" return self._parent @@ -83,13 +92,15 @@ class XmlTypeobjBase(XmlBase, Base): :param part: |part| object """ - def __init__(self, part): + _part: object + + def __init__(self, part: object): XmlBase.__init__(self) Base.__init__(self, part) self._part = part @property - def part(self): + def part(self) -> object: """property that returns the part object of say |cp| typeobj """ diff --git a/src/opc/coreprops.py b/src/opc/coreprops.py index eea72ca..4877519 100644 --- a/src/opc/coreprops.py +++ b/src/opc/coreprops.py @@ -1,5 +1,7 @@ from datetime import datetime +from lxml import etree + from .base import Base, XmlTypeobjBase from .datetime import Dt @@ -14,26 +16,26 @@ class PropertyItem(Base): :param name: name of the property """ - dt_props = ['last print date', 'creation date', 'last save time'] + dt_props = ["last print date", "creation date", "last save time"] """properties that are datetime related""" - def __init__(self, parent, name): + def __init__(self, parent: XmlTypeobjBase, name: str): super().__init__(parent) self._name = name @property - def e(self): + def e(self) -> etree._Element: """Returns the xml element of the property item""" - e = self.parent.e + e = self.parent.e # type: ignore[attr-defined] return e.find(e.qn(self.pfxname)) @property - def pfxname(self): + def pfxname(self) -> str: """Returns the prefix name of the property item eg. 'dc:title'""" - return self.parent.supported_properties[self._name] + return self.parent.supported_properties[self._name] # type: ignore[attr-defined] @property - def Value(self): + def Value(self) -> str | None: """Value of the property item :getter: Returns the value @@ -47,16 +49,16 @@ def Value(self): .Item('creation date').Value = dt_now .Item('author').Value = "New Author" """ - if self.e is not None: + if self.e is not None and self.e.text is not None: if self._name in self.dt_props: - return Dt.from_w3cdtf(self.e.text) + return str(Dt.from_w3cdtf(self.e.text)) return self.e.text + return None @Value.setter - def Value(self, newvalue): + def Value(self, newvalue: datetime) -> None: if self._name in self.dt_props and not isinstance(newvalue, datetime): - raise TypeError( - "newvalue must be a datetime object for this property") + raise TypeError("newvalue must be a datetime object for this property") if self.e is None: e = self.parent.e e.append(e.makeelement(e.qn(self.pfxname))) @@ -97,23 +99,24 @@ class CoreProperties(XmlTypeobjBase): .Item('author').Value = "New Author" """ + type = "application/vnd.openxmlformats-package.core-properties+xml" supported_properties = { - 'title': 'dc:title', - 'subject': 'dc:subject', - 'author': 'dc:creator', - 'keywords': 'cp:keywords', - 'comments': 'dc:description', - 'last author': 'cp:lastModifiedBy', - 'revision number': 'cp:revision', - 'last print date': 'cp:lastPrinted', - 'creation date': 'dcterms:created', - 'last save time': 'dcterms:modified', - 'category': 'cp:category', - 'content status': 'cp:contentStatus', + "title": "dc:title", + "subject": "dc:subject", + "author": "dc:creator", + "keywords": "cp:keywords", + "comments": "dc:description", + "last author": "cp:lastModifiedBy", + "revision number": "cp:revision", + "last print date": "cp:lastPrinted", + "creation date": "dcterms:created", + "last save time": "dcterms:modified", + "category": "cp:category", + "content status": "cp:contentStatus", } - def Item(self, prop): + def Item(self, prop: str) -> PropertyItem: """returns the |pi| object from the given property name""" prop = prop.lower() if prop not in self.supported_properties: diff --git a/src/opc/datetime.py b/src/opc/datetime.py index acc6736..0bb5455 100644 --- a/src/opc/datetime.py +++ b/src/opc/datetime.py @@ -1,24 +1,25 @@ -from datetime import datetime as _datetime, timezone as _timezone +from datetime import datetime as _datetime +from datetime import timezone as _timezone -class Dt(): +class Dt: """A utility class providing static methods for datetime conversions related to w3cdtf format. w3cdtf format is like 2023-09-26T15:01:32Z in utc """ + @staticmethod - def to_w3cdtf(dt_obj): + def to_w3cdtf(dt_obj: _datetime) -> str: """Method to convert datetime object to w3cdtf format string value :param dt_obj: timezone aware datetime.datetime object :returns: str value of dt_obj in w3cdtf format """ dt_obj = dt_obj.astimezone(_timezone.utc) - string = _datetime.isoformat( - dt_obj, timespec="seconds").replace('+00:00', 'Z') - return string if string.endswith('Z') else string+'Z' + string = _datetime.isoformat(dt_obj, timespec="seconds").replace("+00:00", "Z") + return string if string.endswith("Z") else string + "Z" @staticmethod - def from_w3cdtf(string): + def from_w3cdtf(string: str) -> _datetime: """Method to convert w3cdtf format string to datetime.datetime object :param string: str value of datetime.datetime object in w3cdtf format diff --git a/src/opc/package.py b/src/opc/package.py index b45b81e..14e2608 100644 --- a/src/opc/package.py +++ b/src/opc/package.py @@ -1,10 +1,13 @@ -from zipfile import ZipFile, ZIP_DEFLATED -from .types import Types -from .base import Base +from pathlib import Path +from typing import Self +from zipfile import ZIP_DEFLATED, ZipFile + +from .base import Base, XmlTypeobjBase +from .coreprops import CoreProperties from .part import Part -from .relspart import RelsPart from .rels import Relationships -from .coreprops import CoreProperties +from .relspart import RelsPart +from .types import Types from .uri import Uri @@ -29,7 +32,10 @@ class Package(Base): """ - def __init__(self, path, parent=None): + _parts: dict[str, Part] + _part_hooks: dict[str, type[XmlTypeobjBase]] + + def __init__(self, path: Path, parent: object | None = None) -> None: """Initiates the properties of Package object and registers hooks for |rels| and |cp| part""" super().__init__(parent) @@ -41,14 +47,14 @@ def __init__(self, path, parent=None): self.register_part_hook(CoreProperties.type, CoreProperties) @property - def path(self): + def path(self) -> Path: """Readonly property. :returns: Path of the package file""" return self._path @property - def types(self): + def types(self) -> Types: """Readonly property. :returns: |ct| object of the package @@ -56,19 +62,21 @@ def types(self): return self._types @property - def core_properties(self): + def core_properties(self) -> object | None: """Readonly property. :returns: |cp| object of the package """ - return self.get_part('/docProps/core.xml').typeobj + if (part := self.get_part("/docProps/core.xml")) is not None: + return part.typeobj + return None - def read(self): + def read(self) -> Self: """Reads the package file and constructs |ct| object and |part| of the package and then read the contents of the parts """ - with ZipFile(self.path, 'r') as zr: - with zr.open(self.types.zipname, 'r') as f: + with ZipFile(self.path, "r") as zr: + with zr.open(self.types.zipname, "r") as f: self.types.read(f) for zipname in zr.namelist(): @@ -77,25 +85,25 @@ def read(self): uri_str = Uri.zipname2str(zipname) part = self.add_part(uri_str, self.types.get_type(uri_str)) - with zr.open(zipname, 'r') as f: + with zr.open(zipname, "r") as f: part.read(f) return self - def write(self, path): + def write(self, path: Path) -> None: """Writes the package parts and content types to the given path :param path: path where package is to be written to. """ - with ZipFile(path, 'w', compression=ZIP_DEFLATED) as zw: + with ZipFile(path, "w", compression=ZIP_DEFLATED) as zw: for part in self._parts.values(): zipname = part.uri.zipname - with zw.open(zipname, 'w') as f: + with zw.open(zipname, "w") as f: part.write(f) - with zw.open(self.types.zipname, 'w') as f: + with zw.open(self.types.zipname, "w") as f: self.types.write(f) - def exists_part(self, uri_str): + def exists_part(self, uri_str: str) -> bool: """Checks if |part| exists in the |package| :param uri_str: uri string @@ -103,7 +111,7 @@ def exists_part(self, uri_str): """ return uri_str in self._parts - def add_part(self, uri_str, type_): + def add_part(self, uri_str: str, type_) -> Part: # type: ignore[no-untyped-def] """Constructs a |part| or |relspart| object and add to the parts collection of the |package|. part or relspart is decided based on the uri_str value, if uri_str ends with .rels relspart is constructed @@ -138,7 +146,7 @@ def add_part(self, uri_str, type_): return part - def get_part(self, uri_str): + def get_part(self, uri_str: str) -> Part | None: """Gets part having the given uri from the package :param uri_str: string value of the uri @@ -146,8 +154,9 @@ def get_part(self, uri_str): """ if self.exists_part(uri_str): return self._parts[uri_str] + return None - def get_parts(self, type_): + def get_parts(self, type_: str) -> list[Part]: """Gets list of parts of the given content type :param type: content type of the part @@ -155,7 +164,7 @@ def get_parts(self, type_): """ return [part for part in self._parts.values() if part.type == type_] - def register_part_hook(self, type_, callback): + def register_part_hook(self, type_: str, callback: type[XmlTypeobjBase]) -> None: """Registers a callback hook to the content type. Hooks are called when part is created and before read method is called. Any existing hook on the given type will be over written @@ -165,13 +174,14 @@ def register_part_hook(self, type_, callback): """ self._part_hooks[type_] = callback - def remove_part(self, uri_str): + def remove_part(self, uri_str: str) -> None: """Removes a |part| from the |package|. Also removes entry from types if the type is not refering to any other part. :param uri_str: string value of uri """ - type = self.get_part(uri_str).type - del self._parts[uri_str] - if len(self.get_parts(type)) == 0: - self._types.remove_type(uri_str) + if (part := self.get_part(uri_str)) is not None: + type = part.type + del self._parts[uri_str] + if len(self.get_parts(type)) == 0: + self._types.remove_type(uri_str) diff --git a/src/opc/parser.py b/src/opc/parser.py index 8a465a0..857b4ad 100644 --- a/src/opc/parser.py +++ b/src/opc/parser.py @@ -5,11 +5,10 @@ class Parser(etree.XMLParser): """Parser class that is set with default class lookup for elements""" - def __init__(self): + def __init__(self) -> None: """sets the default element class |eb| lookup for the element""" - self.set_element_class_lookup( - etree.ElementDefaultClassLookup(ElementBase)) + self.set_element_class_lookup(etree.ElementDefaultClassLookup(ElementBase)) - def parse(self, fp): + def parse(self, fp): # type: ignore[no-untyped-def] """parses the given file object and returns the xml tree""" return etree.parse(fp, self) diff --git a/src/opc/part.py b/src/opc/part.py index 7b6f710..b2a9487 100644 --- a/src/opc/part.py +++ b/src/opc/part.py @@ -1,3 +1,5 @@ +from typing import Self + from .base import Base, PartBase from .uri import Uri @@ -10,7 +12,7 @@ class Part(PartBase, Base): :param type: type of content of part """ - def __init__(self, parent, uri_str, type_): + def __init__(self, parent: Base, uri_str: str, type_: str): super().__init__(parent) self._uri = Uri(uri_str) self._type = type_ @@ -18,7 +20,7 @@ def __init__(self, parent, uri_str, type_): self._data = None @property - def uri(self): + def uri(self) -> Uri: """Readonly property :returns: |uri| object of the part @@ -26,7 +28,7 @@ def uri(self): return self._uri @property - def type(self): + def type(self) -> str: """Readonly property :returns: content type of the part @@ -34,7 +36,7 @@ def type(self): return self._type @property - def typeobj(self): + def typeobj(self): # type: ignore[no-untyped-def] """Readonly property :getter: object created as per the content of the part using hook @@ -43,11 +45,11 @@ def typeobj(self): return self._typeobj @typeobj.setter - def typeobj(self, typeobj_): + def typeobj(self, typeobj_) -> None: # type: ignore[no-untyped-def] """sets the typeobj property of Part to given value""" self._typeobj = typeobj_ - def read(self, f): + def read(self, f): # type: ignore[no-untyped-def] """reads the content from the file object. if typeobj is present then responsibility of reading the content is passed on to typeobj @@ -58,7 +60,7 @@ def read(self, f): else: self.typeobj.read(f) - def write(self, f): + def write(self, f): # type: ignore[no-untyped-def] """writes the part content to the file object. if typeobj is present then responsibility of writing the content is passed on to typeobj @@ -69,7 +71,7 @@ def write(self, f): else: self.typeobj.write(f) - def get_rels_part(self): + def get_rels_part(self): # type: ignore[no-untyped-def] """Method that gets the |relspart| of the current part :returns: |relspart| object or None @@ -82,12 +84,12 @@ def get_rels_part(self): print(presentation_part.uri.str) # '/ppt/presentation.xml' presentation_relspart = presentation_part.get_rels_part() - print(presentation_relspart.uri.str) + print(presentation_relspart.uri.str) # /ppt/_rels/presentation.xml.rels """ return self.parent.get_part(self.uri.rels) - def get_abs_uri_str(self, target_rel_uri_str): + def get_abs_uri_str(self, target_rel_uri_str: str) -> str | None: """Method to get the absolute uri string value from the given relative uri string value of target part @@ -101,8 +103,9 @@ def get_abs_uri_str(self, target_rel_uri_str): """ if target_rel_uri_str: return self.uri.get_abs(target_rel_uri_str) + return None - def get_related_part(self, rid): + def get_related_part(self, rid: str) -> str | None: """Method to get the part object from the relationship id with respect to current part. @@ -116,12 +119,12 @@ def get_related_part(self, rid): """ rels_part = self.get_rels_part() if rels_part is None: - return + return None target_rel_uri_str = rels_part.get_target_rel_uri_str(rid) related_part_uri_str = self.get_abs_uri_str(target_rel_uri_str) - return self.parent.get_part(related_part_uri_str) + return self.parent.get_part(related_part_uri_str) # type: ignore[attr-defined] - def get_related_parts_by_reltype(self, reltype): + def get_related_parts_by_reltype(self, reltype: str) -> list[Self]: """Gets list of parts that are related by given reltype wrt to current part @@ -131,14 +134,14 @@ def get_related_parts_by_reltype(self, reltype): """ rels_part = self.get_rels_part() if rels_part is None: - return + return [] lst = [] for rel_uri_str in rels_part.get_lst_target_rel_uri_str(reltype): - t = self.parent.get_part(self.get_abs_uri_str(rel_uri_str)) + t = self.parent.get_part(self.get_abs_uri_str(rel_uri_str)) # type: ignore[attr-defined] lst.append(t) return lst - def add_relspart(self): + def add_relspart(self): # type: ignore[no-untyped-def] """adds the relspart to the package for this part. It also initializes the xml for the relspart. Do not call this method if relspart already exists for the current part. @@ -146,6 +149,7 @@ def add_relspart(self): :returns: relspart object """ from .relspart import RelsPart + relspart = self.package.add_part(self.uri.rels, RelsPart.type) relspart.typeobj.init_e() return relspart diff --git a/src/opc/rels.py b/src/opc/rels.py index 420cfe3..89581b7 100644 --- a/src/opc/rels.py +++ b/src/opc/rels.py @@ -1,3 +1,5 @@ +from lxml import etree + from .base import XmlTypeobjBase from .parser import Parser @@ -6,9 +8,11 @@ class Relationships(XmlTypeobjBase): """Class for object that represents the xml of |relspart| of the package. Inherits XmlTypeobjBase class. Xml of rels part is available as self.e """ + + e: etree._Element xmlns = "http://schemas.openxmlformats.org/package/2006/relationships" - def get_target_rel_uri_str(self, rid): + def get_target_rel_uri_str(self, rid: str) -> str | None: """Method to get the target value of a relation given by rid :param rid: relation id string value @@ -26,10 +30,11 @@ def get_target_rel_uri_str(self, rid): # slides/slide1.xml """ for r in self.e: - if r.get('Id') == rid: - return r.get('Target') + if r.get("Id") == rid: + return r.get("Target") + return None - def get_lst_target_rel_uri_str(self, reltype): + def get_lst_target_rel_uri_str(self, reltype: str) -> list[str | None]: """Gets the list of relative uri of the target related by the given reltype @@ -38,17 +43,18 @@ def get_lst_target_rel_uri_str(self, reltype): """ lst = [] for r in self.e: - if r.get('Type') == reltype: - lst.append(r.get('Target')) + if r.get("Type") == reltype: + lst.append(r.get("Target")) return lst - def init_e(self): - """Add Relationships xml to the self.part object - """ + def init_e(self) -> None: + """Add Relationships xml to the self.part object""" parser = Parser() - self.e = parser.makeelement('Relationships', nsmap={None: self.xmlns}) + self.e = parser.makeelement("Relationships", nsmap={None: self.xmlns}) - def add_relation(self, type, target, target_mode=None): + def add_relation( + self, type: str, target: str, target_mode: str | None = None + ) -> str: """Adds Relationships element to the xml. Assigns new Id and sets the Type, Target, TargetMode attributes to the element. @@ -63,21 +69,23 @@ def add_relation(self, type, target, target_mode=None): parser = Parser() id = self.get_next_id() - attrib = {'Id': id, 'Type': type, 'Target': target} + attrib = {"Id": id, "Type": type, "Target": target} if target_mode: - attrib['TargetMode'] = target_mode + attrib["TargetMode"] = target_mode rel = parser.makeelement( - 'Relationship', attrib=attrib, nsmap={None: self.xmlns}) + "Relationship", attrib=attrib, nsmap={None: self.xmlns} + ) self.e.append(rel) return id - def get_next_id(self): + def get_next_id(self) -> str: """Returns the new available id for the new relationship element :returns: str value of the new available id """ used = 0 for r in self.e: - id = int(r.get('Id').replace('rId', '')) - if id > used: - used = id - return 'rId'+str(used + 1) + if (id_string := r.get("Id")) is not None: + id = int(id_string.replace("rId", "")) + if id > used: + used = id + return "rId" + str(used + 1) diff --git a/src/opc/relspart.py b/src/opc/relspart.py index 8e41684..b95585d 100644 --- a/src/opc/relspart.py +++ b/src/opc/relspart.py @@ -2,11 +2,11 @@ class RelsPart(Part): - """Class of part objects that are of type rels. Inherits the |part| class - """ + """Class of part objects that are of type rels. Inherits the |part| class""" + type = "application/vnd.openxmlformats-package.relationships+xml" - def get_target_rel_uri_str(self, rid): + def get_target_rel_uri_str(self, rid: str) -> str: """gets the target value for the relatioship with id as given rid :param rid: relation id string value @@ -14,7 +14,7 @@ def get_target_rel_uri_str(self, rid): """ return self.typeobj.get_target_rel_uri_str(rid) - def get_lst_target_rel_uri_str(self, reltype): + def get_lst_target_rel_uri_str(self, reltype: str) -> str: """Gets the list of relative uri str of the target part related by the given reltype diff --git a/src/opc/types.py b/src/opc/types.py index f53a646..7da8dd7 100644 --- a/src/opc/types.py +++ b/src/opc/types.py @@ -1,4 +1,5 @@ from .base import Base, XmlBase +from .part import Part from .uri import Uri @@ -7,64 +8,83 @@ class Types(XmlBase, Base): Inherits XmlBase and Base classes """ - zipname = '[Content_Types].xml' + zipname = "[Content_Types].xml" - def __init__(self, parent): + def __init__(self, parent: object): Base.__init__(self, parent) XmlBase.__init__(self) - def remove_type(self, uri_str): + def remove_type(self, uri_str: str) -> None: """Removes the type for given uri_str. If uri_str presents in Override elements of xml, that Override element would be removed. Otherwise Default element would be removed having the same extension as uri_str. :param uri_str: string value of the part's uri """ - for e in self.e.findall('{*}Override'): - if e.get('PartName') == uri_str: - e.getparent().remove(e) - return - extension = Uri(uri_str).ext - for e in self.e.findall('{*}Default'): - if e.get('Extension').lower() == extension.lower(): - e.getparent().remove(e) - return + if self.e is None: + return None + + for e in self.e.findall("{*}Override"): + if e.get("PartName") == uri_str: + if (parent := e.getparent()) is not None: + parent.remove(e) + return None + uri_extension = Uri(uri_str).ext + for e in self.e.findall("{*}Default"): + if (extension := e.get("Extension")) is not None: + if extension.lower() == uri_extension.lower(): + if (parent := e.getparent()) is not None: + parent.remove(e) + return None - def get_type(self, uri_str): + def get_type(self, uri_str: str) -> str | None: """Returns the type of given uri_str. First Override elements in xml are checked for match then Default elements are checked :param uri_str: string value of the part's uri :returns: string value of the type of the given uri_str """ - for e in self.e.findall('{*}Override'): - if e.get('PartName') == uri_str: - return e.get('ContentType') + if self.e is None: + return None + + for e in self.e.findall("{*}Override"): + if e.get("PartName") == uri_str: + return e.get("ContentType") + + uri_extension = Uri(uri_str).ext + for e in self.e.findall("{*}Default"): + if (extension := e.get("Extension")) is not None: + if extension.lower() == uri_extension.lower(): + return e.get("ContentType") - extension = Uri(uri_str).ext - for e in self.e.findall('{*}Default'): - if e.get('Extension').lower() == extension.lower(): - return e.get('ContentType') + return None - def add_default(self, part): + def add_default(self, part: Part) -> None: """Adds the content type and extension of the part to the xml :param part: |part| object """ - attrib = {'Extension': part.uri.ext, 'ContentType': part.type} - for e in self.e.findall('{*}Default'): + attrib = {"Extension": part.uri.ext, "ContentType": part.type} + + if self.e is None: + return None + + for e in self.e.findall("{*}Default"): if e.attrib == attrib: return - self.e.insert(0, self.parser.makeelement(self.e.qn('Default'), - **attrib)) + self.e.insert(0, self.parser.makeelement(self.e.qn("Default"), **attrib)) # type: ignore[attr-defined,arg-type] - def add_override(self, part): + def add_override(self, part: Part) -> None: """Add the uri_str and type of the part to the xml :param part: part object """ - attrib = {'PartName': part.uri.str, 'ContentType': part.type} - for e in self.e.findall('{*}Override'): + attrib = {"PartName": str(part.uri), "ContentType": part.type} + + if self.e is None: + return None + + for e in self.e.findall("{*}Override"): if e.attrib == attrib: return - self.e.append(self.parser.makeelement(self.e.qn('Override'), **attrib)) + self.e.append(self.parser.makeelement(self.e.qn("Override"), **attrib)) # type: ignore[attr-defined,arg-type] diff --git a/src/opc/uri.py b/src/opc/uri.py index cbe0251..b4ffb58 100644 --- a/src/opc/uri.py +++ b/src/opc/uri.py @@ -2,81 +2,79 @@ from pathlib import Path, PurePosixPath -class Uri(): +class Uri: """Class for uri of the |part| in a |package|. :param uri_str: string value of the part's uri """ - def __init__(self, uri_str): + def __init__(self, uri_str: str) -> None: """initializes the attributes and checks for the input correctness""" if not isinstance(uri_str, str): raise TypeError("uri_str must be str object") - if not uri_str.startswith('/'): + if not uri_str.startswith("/"): raise ValueError("uri_str must start with slash") self._uri_str = uri_str @property - def str(self): - """Readonly property. - :returns: the string value of the uri""" - return self._uri_str - - @property - def ext(self): + def ext(self) -> str: """Readonly property. :returns: string extension value of the uri""" - return self.str.split('.')[-1] + return self._uri_str.split(".")[-1] @property - def zipname(self): + def zipname(self) -> str: """Readonly property. :returns: the zipname of the uri i.e. name valid for zipfile""" - return self.str[1:] + return self._uri_str[1:] @property - def is_rels(self): + def is_rels(self) -> bool: """Readonly property. :returns: boolean if the uri is of a rels part or not""" - return self.str.endswith('.rels') + return self._uri_str.endswith(".rels") @property - def rels(self): + def rels(self) -> str: """Readonly property. :returns: the uri string of the |relspart| of current uri's part """ - p = PurePosixPath(self.str) - return str(p.parent / '_rels' / (p.name + '.rels')) + p = PurePosixPath(self._uri_str) + return str(p.parent / "_rels" / (p.name + ".rels")) @staticmethod - def zipname2str(zipname): + def zipname2str(zipname: str) -> str: """static method. :param zipname: string value of the any zipname :returns: uri string value from the given zipname """ - return '/'+zipname + return "/" + zipname - def get_abs(self, rel_target_uri_str): + def get_abs(self, rel_target_uri_str: str) -> str: """Method to get the absolute uri from the relative uri :param rel_target_uri_str: relative uri with respect to current uri :returns: absolute uri string value """ - return '/' + str(( - (Path(self.str).parent / Path(rel_target_uri_str)) - .resolve().relative_to(Path('/').resolve())) - .as_posix()) - - def get_rel(self, abs_target_uri_str): + return "/" + str( + ( + (Path(self._uri_str).parent / Path(rel_target_uri_str)) + .resolve() + .relative_to(Path("/").resolve()) + ).as_posix() + ) + + def get_rel(self, abs_target_uri_str: str) -> str: """Returns the relative uri str value from the given absolute uri str of the target part. Relative with respect to the current uri :param abs_target_uri_str: absolute uri str of target part :returns: str value of the relative uri """ - return Path(path.relpath(abs_target_uri_str, - Path(self.str).parent.as_posix())).as_posix() + return Path( + path.relpath(abs_target_uri_str, Path(self._uri_str).parent.as_posix()) + ).as_posix() - def __str__(self): - return self.str + def __str__(self) -> str: + return self._uri_str diff --git a/tests/conftest.py b/tests/conftest.py index 53f5222..618f65d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,59 +1,64 @@ -import pytest from pathlib import Path + +import pytest + from opc import Package -from opc.part import Part -from opc.parser import Parser from opc.base import XmlBase +from opc.parser import Parser +from opc.part import Part +from opc.types import Types @pytest.fixture -def data_path(): +def data_path() -> Path: return Path(__file__).parent / "data" @pytest.fixture -def pptx_path(data_path): +def pptx_path(data_path: Path) -> Path: return data_path / "blank.pptx" @pytest.fixture -def package(pptx_path): +def package(pptx_path: Path) -> Path: return Package(pptx_path).read() @pytest.fixture -def types(package): +def types(package: Package) -> Types: return package.types @pytest.fixture -def presentation_uri_str(): - return '/ppt/presentation.xml' +def presentation_uri_str() -> str: + return "/ppt/presentation.xml" @pytest.fixture -def thumbnail_url_str(): +def thumbnail_url_str() -> str: return "/docProps/thumbnail.jpeg" @pytest.fixture -def presentation_type(): - return ("application/vnd.openxmlformats-officedocument.presentationml" - ".presentation.main+xml") +def presentation_type() -> str: + return ( + "application/vnd.openxmlformats-officedocument.presentationml" + ".presentation.main+xml" + ) @pytest.fixture -def thumbnail_type(): +def thumbnail_type() -> str: return "image/jpeg" @pytest.fixture -def part(): +def part() -> Part: return Part(None, "/some/uri.def", "http://some/default/type") @pytest.fixture -def presentation_part(package, presentation_uri_str): +def presentation_part(package: Package, presentation_uri_str: str) -> Part: return package.get_part(presentation_uri_str) @@ -68,32 +73,32 @@ def presentation_relstypeobj(presentation_relspart): @pytest.fixture -def all_parts(package): +def all_parts(package: Package): return package._parts.values() @pytest.fixture -def slide_part(package): - return package.get_part('/ppt/slides/slide1.xml') +def slide_part(package: Package): + return package.get_part("/ppt/slides/slide1.xml") @pytest.fixture -def parser(): +def parser() -> Parser: return Parser() @pytest.fixture -def presentation_xml_path(data_path): +def presentation_xml_path(data_path: Path) -> Path: return data_path / "presentation.xml" @pytest.fixture -def core_properties(package): +def core_properties(package: Package): return package.core_properties @pytest.fixture -def xmlbase_presentation(presentation_xml_path): +def xmlbase_presentation(presentation_xml_path: str) -> XmlBase: x = XmlBase() with open(presentation_xml_path) as f: x.read(f) diff --git a/tests/data/package_write.pptx b/tests/data/package_write.pptx deleted file mode 100644 index aaa614c..0000000 Binary files a/tests/data/package_write.pptx and /dev/null differ diff --git a/tests/test_base.py b/tests/test_base.py index 5cbe28f..1ee3884 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,53 +1,59 @@ -from lxml import etree +from pathlib import Path from unittest import mock -from opc.base import Base, PartBase, XmlTypeobjBase, XmlBase + +from lxml import etree + +from opc.base import Base, PartBase, XmlBase, XmlTypeobjBase +from opc.package import Package from opc.parser import Parser -def test_base_parent(): +def test_base_parent() -> None: obj = object() base = Base(obj) assert base.parent is obj -def test_xmlbase_e(xmlbase_presentation): +def test_xmlbase_e(xmlbase_presentation: XmlBase) -> None: assert isinstance(xmlbase_presentation.e, etree.ElementBase) -def test_xmlbase_parser(xmlbase_presentation): +def test_xmlbase_parser(xmlbase_presentation: XmlBase) -> None: assert isinstance(xmlbase_presentation.parser, Parser) -def test_xmlbase_read(presentation_xml_path, xmlbase): +def test_xmlbase_read(presentation_xml_path: str, xmlbase: XmlBase) -> None: assert xmlbase.e is None with open(presentation_xml_path) as f: xmlbase.read(f) assert isinstance(xmlbase.e, etree.ElementBase) -def test_xmlbase_write(presentation_xml_path, xmlbase_presentation, tmp_path): +def test_xmlbase_write( + presentation_xml_path: str, xmlbase_presentation: XmlBase, tmp_path: Path +) -> None: with open(presentation_xml_path) as f: expected = etree.parse(f).getroot() path = tmp_path / "presentation.xml" - with open(path, 'wb') as f: + with open(path, "wb") as f: xmlbase_presentation.write(f) with open(path) as f: actual = etree.parse(f).getroot() assert etree.tostring(expected) == etree.tostring(actual) -def test_partbase_package_property_returns_package_object(package): +def test_partbase_package_property_returns_package_object(package: Package) -> None: for part in package._parts.values(): assert isinstance(part, PartBase) assert part.package is package -def test_xmltypeobjbase_part_property(): +def test_xmltypeobjbase_part_property() -> None: mock_part = mock.MagicMock() obj = XmlTypeobjBase(mock_part) assert obj.part is mock_part -def test_xmltypeobjbase_subclass_of_xmlbase_and_base(): +def test_xmltypeobjbase_subclass_of_xmlbase_and_base() -> None: assert issubclass(XmlTypeobjBase, (XmlBase, Base)) diff --git a/tests/test_coreprops.py b/tests/test_coreprops.py index ff58707..dc167aa 100644 --- a/tests/test_coreprops.py +++ b/tests/test_coreprops.py @@ -4,8 +4,7 @@ from opc.parser import ElementBase -def test_coreprops_raise_valueerror_for_unsupported_properties( - core_properties): +def test_coreprops_raise_valueerror_for_unsupported_properties(core_properties): with pytest.raises(ValueError): core_properties.Item("un supported") @@ -31,33 +30,35 @@ def test_coreprops_supported_properties_do_not_raise_error(core_properties): def test_coreprops_value_of_prop_absent_in_xml_is_none(core_properties): - assert None is core_properties.Item('comments').Value + assert None is core_properties.Item("comments").Value def test_property_item_property_e_is_object_of_element_base(core_properties): - assert isinstance(core_properties.Item('title').e, ElementBase) - - -@pytest.mark.parametrize("name, pfxname", [ - ('title', 'dc:title'), - ('subject', 'dc:subject'), - ('author', 'dc:creator'), - ('keywords', 'cp:keywords'), - ('comments', 'dc:description'), - ('last author', 'cp:lastModifiedBy'), - ('revision number', 'cp:revision'), - ('last print date', 'cp:lastPrinted'), - ('creation date', 'dcterms:created'), - ('last save time', 'dcterms:modified'), - ('category', 'cp:category'), - ('content status', 'cp:contentStatus'), -]) + assert isinstance(core_properties.Item("title").e, ElementBase) + + +@pytest.mark.parametrize( + "name, pfxname", + [ + ("title", "dc:title"), + ("subject", "dc:subject"), + ("author", "dc:creator"), + ("keywords", "cp:keywords"), + ("comments", "dc:description"), + ("last author", "cp:lastModifiedBy"), + ("revision number", "cp:revision"), + ("last print date", "cp:lastPrinted"), + ("creation date", "dcterms:created"), + ("last save time", "dcterms:modified"), + ("category", "cp:category"), + ("content status", "cp:contentStatus"), + ], +) def test_property_item_pfxname_values(core_properties, name, pfxname): assert core_properties.Item(name).pfxname == pfxname -def test_property_item_raises_error_if_nondt_object_given_for_dt_props( - core_properties): +def test_property_item_raises_error_if_nondt_object_given_for_dt_props(core_properties): with pytest.raises(TypeError): for prop in PropertyItem.dt_props: core_properties.Item(prop).Value = "" diff --git a/tests/test_datetime.py b/tests/test_datetime.py index c3b19ae..a31ce96 100644 --- a/tests/test_datetime.py +++ b/tests/test_datetime.py @@ -1,20 +1,28 @@ -from datetime import datetime, timezone, timedelta +from datetime import datetime, timedelta, timezone + from opc.datetime import Dt -def test_dt_to_w3cdtf_from_timezone_aware_dt_object(): +def test_dt_to_w3cdtf_from_timezone_aware_dt_object() -> None: tz = timezone(timedelta(hours=5, minutes=30)) - dt_obj = datetime(year=2023, month=9, day=26, - hour=20, minute=31, second=32, tzinfo=tz) + dt_obj = datetime( + year=2023, month=9, day=26, hour=20, minute=31, second=32, tzinfo=tz + ) assert "2023-09-26T15:01:32Z" == Dt.to_w3cdtf(dt_obj) tz = timezone(timedelta(hours=-4)) - dt_obj = datetime(year=2023, month=9, day=26, - hour=20, minute=31, second=32, tzinfo=tz) + dt_obj = datetime( + year=2023, month=9, day=26, hour=20, minute=31, second=32, tzinfo=tz + ) assert "2023-09-27T00:31:32Z" == Dt.to_w3cdtf(dt_obj) -def test_dt_from_w3cdtf_from_string(): +def test_dt_from_w3cdtf_from_string() -> None: + tz = timezone(timedelta(hours=5, minutes=30)) dt_stamp = Dt.from_w3cdtf("2023-09-26T15:01:32Z").timestamp() - assert dt_stamp == datetime(year=2023, month=9, day=26, - hour=20, minute=31, second=32).timestamp() + assert ( + dt_stamp + == datetime( + year=2023, month=9, day=26, hour=20, minute=31, second=32, tzinfo=tz + ).timestamp() + ) diff --git a/tests/test_package.py b/tests/test_package.py index 2ad31fe..5c6300a 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -1,12 +1,14 @@ -from zipfile import ZipFile -from unittest import mock import os +from unittest import mock +from zipfile import ZipFile + import pytest -from opc.types import Types -from opc.relspart import RelsPart + from opc.coreprops import CoreProperties from opc.package import Package from opc.part import Part +from opc.relspart import RelsPart +from opc.types import Types def test_package_path(package, pptx_path): @@ -34,7 +36,7 @@ def test_package_has_corepropspart_hook(package): def test_package_read_all_items_count_tally(package): parts_count = len(package._parts) total_count = parts_count + 1 # add types - with ZipFile(package.path, 'r') as zr: + with ZipFile(package.path, "r") as zr: assert len(zr.namelist()) == total_count @@ -45,66 +47,63 @@ def test_package_write_all_items_count_tally(package, data_path): os.remove(path) package.write(path) pkg = Package(path).read() + os.remove(path) assert len(package._parts) == len(pkg._parts) def test_package_exists_part(package): - assert package.exists_part('/ppt/slides/slide1.xml') - assert not package.exists_part('/ppt/slides/slide2.xml') + assert package.exists_part("/ppt/slides/slide1.xml") + assert not package.exists_part("/ppt/slides/slide2.xml") -def test_package_add_part_error_if_part_exists_already(package, - presentation_part): +def test_package_add_part_error_if_part_exists_already(package, presentation_part): with pytest.raises(ValueError): - package.add_part(presentation_part.uri.str, None) + package.add_part(str(presentation_part.uri), None) def test_package_add_part_class_of_part_used_is_part(package): - part = package.add_part('/ppt/some/part.xml', None) + part = package.add_part("/ppt/some/part.xml", None) assert isinstance(part, Part) def test_package_add_part_class_of_part_used_is_relspart(package): - part = package.add_part('/ppt/some/part.rels', None) + part = package.add_part("/ppt/some/part.rels", None) assert isinstance(part, RelsPart) def test_package_add_part_uri_is_added_to_parts(package): - part = package.add_part('/ppt/some/part.xml', None) - assert package.exists_part(part.uri.str) + part = package.add_part("/ppt/some/part.xml", None) + assert package.exists_part(str(part.uri)) def test_package_get_part_uri_match(package, presentation_uri_str): - assert package.get_part( - presentation_uri_str).uri.str == presentation_uri_str + assert str(package.get_part(presentation_uri_str).uri) == presentation_uri_str def test_package_get_parts_no_part_with_type_none(package): assert len(package.get_parts(None)) == 0 -def test_package_get_parts_with_rels_type_count(package, - presentation_relspart): +def test_package_get_parts_with_rels_type_count(package, presentation_relspart): assert 15 == len(package.get_parts(presentation_relspart.type)) def test_package_register_part_hook_if_hook_used(package): mock_callback = mock.MagicMock() - package.register_part_hook('/some/type', mock_callback) - package.add_part('/some/uri.xml', '/some/type') + package.register_part_hook("/some/type", mock_callback) + package.add_part("/some/uri.xml", "/some/type") assert mock_callback.assert_called_once def test_package_remove_part(package, presentation_part): assert len(package.get_parts(presentation_part.type)) == 1 - package.remove_part(presentation_part.uri.str) - assert not package.exists_part(presentation_part.uri.str) + package.remove_part(str(presentation_part.uri)) + assert not package.exists_part(str(presentation_part.uri)) assert package.get_parts(presentation_part.type) == [] -def test_package_remove_part_but_types_still_has_it(package, - presentation_relspart): +def test_package_remove_part_but_types_still_has_it(package, presentation_relspart): before_count = len(package.get_parts(presentation_relspart.type)) - package.remove_part(presentation_relspart.uri.str) + package.remove_part(str(presentation_relspart.uri)) after_count = len(package.get_parts(presentation_relspart.type)) assert before_count == after_count + 1 diff --git a/tests/test_parser.py b/tests/test_parser.py index 8491815..908e6d7 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,86 +1,97 @@ -import pytest from unittest import mock -from opc.parser import etree, ElementBase + +import pytest + +from opc.parser import ElementBase, Parser, etree -def test_element_base_ns_of_no_namespace_element(parser): - e = parser.makeelement('some') +def test_element_base_ns_of_no_namespace_element(parser: Parser) -> None: + e = parser.makeelement("some") assert e.ns is None -def test_element_base_ns_of_with_namespace_element(parser): - e = parser.makeelement('{somenamespace}some') +def test_element_base_ns_of_with_namespace_element(parser: Parser) -> None: + e = parser.makeelement("{somenamespace}some") assert e.ns == "somenamespace" -def test_element_base_qn_no_prefix_no_nsmap_and_e_has_no_nsmap(parser): - e = parser.makeelement('some') - assert e.qn('just') == 'just' +def test_element_base_qn_no_prefix_no_nsmap_and_e_has_no_nsmap(parser: Parser) -> None: + e = parser.makeelement("some") + assert e.qn("just") == "just" -def test_element_base_qn_no_prefix_no_nsmap_and_e_has_nsmap(parser): - e = parser.makeelement('some', nsmap={None: "somens"}) - assert e.qn('just') == '{somens}just' +def test_element_base_qn_no_prefix_no_nsmap_and_e_has_nsmap(parser: Parser) -> None: + e = parser.makeelement("some", nsmap={None: "somens"}) + assert e.qn("just") == "{somens}just" -def test_element_base_qn_no_prefix_empty_nsmap_and_e_has_nsmap(parser): - e = parser.makeelement('some', nsmap={None: "somens"}) - assert e.qn('just', nsmap={}) == 'just' +def test_element_base_qn_no_prefix_empty_nsmap_and_e_has_nsmap(parser: Parser) -> None: + e = parser.makeelement("some", nsmap={None: "somens"}) + assert e.qn("just", nsmap={}) == "just" def test_element_base_qn_no_prefix_none_in_nsmap_and_e_has_diff_ns_for_none( - parser): - e = parser.makeelement('some', nsmap={None: "somens"}) - assert e.qn('just', nsmap={None: "nsnone"}) == '{nsnone}just' + parser: Parser, +) -> None: + e = parser.makeelement("some", nsmap={None: "somens"}) + assert e.qn("just", nsmap={None: "nsnone"}) == "{nsnone}just" -def test_element_base_qn_no_prefix_no_nsmap_no_none_in_e_nsmap(parser): - e = parser.makeelement('{nssome}some') - assert e.qn('just') == 'just' +def test_element_base_qn_no_prefix_no_nsmap_no_none_in_e_nsmap(parser: Parser) -> None: + e = parser.makeelement("{nssome}some") + assert e.qn("just") == "just" def test_element_base_qn_with_prefix_no_nsmap_and_prefix_not_in_e_nsmap( - parser): - e = parser.makeelement('some', nsmap={None: "somens"}) + parser: Parser, +) -> None: + e = parser.makeelement("some", nsmap={None: "somens"}) with pytest.raises(KeyError): - e.qn('p:just') + e.qn("p:just") -def test_element_base_qn_with_prefix_no_nsmap_and_prefix_in_e_nsmap(parser): - e = parser.makeelement('some', nsmap={"p": "somens"}) - assert e.qn('p:just') == "{somens}just" +def test_element_base_qn_with_prefix_no_nsmap_and_prefix_in_e_nsmap( + parser: Parser, +) -> None: + e = parser.makeelement("some", nsmap={"p": "somens"}) + assert e.qn("p:just") == "{somens}just" def test_element_base_qn_with_prefix_nsmap_but_prefix_absent_prefix_in_e_nsmap( - parser): - e = parser.makeelement('some', nsmap={"p": "somens"}) + parser: Parser, +) -> None: + e = parser.makeelement("some", nsmap={"p": "somens"}) with pytest.raises(KeyError): - e.qn('p:just', nsmap={'q': "qnamespace"}) + e.qn("p:just", nsmap={"q": "qnamespace"}) -def test_element_base_qn_with_prefix_with_nsmap_prefix_present(parser): - e = parser.makeelement('some', ) - e.qn('p:just', nsmap={'p': "pnamespace"}) == "{pnamespace}just" +def test_element_base_qn_with_prefix_with_nsmap_prefix_present(parser: Parser) -> None: + e = parser.makeelement( + "some", + ) + e.qn("p:just", nsmap={"p": "pnamespace"}) == "{pnamespace}just" -def test_element_base_makeelement(parser): - e = parser.makeelement('some', ) - e2 = e.makeelement('somelem') +def test_element_base_makeelement(parser: Parser) -> None: + e = parser.makeelement( + "some", + ) + e2 = e.makeelement("somelem") assert isinstance(e2, e.__class__) -def test_element_base_dump(parser): - e = parser.makeelement('some') +def test_element_base_dump(parser: Parser) -> None: + e = parser.makeelement("some") etree.dump = mock.MagicMock() e.dump() etree.dump.assert_called_once_with(e) -def test_parser(parser): - assert isinstance(parser.makeelement('some'), ElementBase) +def test_parser(parser: Parser) -> None: + assert isinstance(parser.makeelement("some"), ElementBase) -def test_parser_parse(parser, presentation_xml_path): +def test_parser_parse(parser: Parser, presentation_xml_path) -> None: e = parser.parse(presentation_xml_path) for e in e.getroot(): assert isinstance(e, ElementBase) diff --git a/tests/test_part.py b/tests/test_part.py index fd8f9b0..a0c2103 100644 --- a/tests/test_part.py +++ b/tests/test_part.py @@ -24,12 +24,12 @@ def test_all_parts_have_typeobj_property(all_parts): def test_all_parts_have_read_method(all_parts): for part in all_parts: - assert hasattr(part, "read") and callable(getattr(part, 'read')) + assert hasattr(part, "read") and callable(getattr(part, "read")) def test_all_parts_have_write_method(all_parts): for part in all_parts: - assert hasattr(part, "write") and callable(getattr(part, 'write')) + assert hasattr(part, "write") and callable(getattr(part, "write")) def test_get_rels_part(presentation_part, presentation_relspart): @@ -37,9 +37,8 @@ def test_get_rels_part(presentation_part, presentation_relspart): def test_get_abs_uri_str(presentation_part, slide_part): - assert presentation_part.get_abs_uri_str('slides/slide1.xml') == \ - slide_part.uri.str + assert presentation_part.get_abs_uri_str("slides/slide1.xml") == str(slide_part.uri) def test_get_related_part(presentation_part, slide_part): - assert slide_part is presentation_part.get_related_part('rId2') + assert slide_part is presentation_part.get_related_part("rId2") diff --git a/tests/test_rels.py b/tests/test_rels.py index db7f4d5..7213fe9 100644 --- a/tests/test_rels.py +++ b/tests/test_rels.py @@ -1,4 +1,5 @@ import pytest + from opc.rels import Relationships @@ -8,12 +9,15 @@ def test_typeobj_is_initialized_for_all_rels_parts(package): assert isinstance(part.typeobj, Relationships) -@pytest.mark.parametrize("rid, expectation", [ - ("rId1", "slideMasters/slideMaster1.xml"), - ("rId2", "slides/slide1.xml"), - ("rId3", "presProps.xml"), - ("rId4", "viewProps.xml"), - ("rId5", "theme/theme1.xml"), -]) +@pytest.mark.parametrize( + "rid, expectation", + [ + ("rId1", "slideMasters/slideMaster1.xml"), + ("rId2", "slides/slide1.xml"), + ("rId3", "presProps.xml"), + ("rId4", "viewProps.xml"), + ("rId5", "theme/theme1.xml"), + ], +) def test_get_target_rel_uri_str(presentation_relstypeobj, rid, expectation): assert expectation == presentation_relstypeobj.get_target_rel_uri_str(rid) diff --git a/tests/test_relspart.py b/tests/test_relspart.py index 3f2e96d..89b4153 100644 --- a/tests/test_relspart.py +++ b/tests/test_relspart.py @@ -9,13 +9,16 @@ def test_all_rels_parts_class(package): assert isinstance(part, RelsPart) -@pytest.mark.parametrize("rid, expectation", [ - ("rId1", "slideMasters/slideMaster1.xml"), - ("rId2", "slides/slide1.xml"), - ("rId3", "presProps.xml"), - ("rId4", "viewProps.xml"), - ("rId5", "theme/theme1.xml"), -]) +@pytest.mark.parametrize( + "rid, expectation", + [ + ("rId1", "slideMasters/slideMaster1.xml"), + ("rId2", "slides/slide1.xml"), + ("rId3", "presProps.xml"), + ("rId4", "viewProps.xml"), + ("rId5", "theme/theme1.xml"), + ], +) def test_get_target_rel_uri_str(presentation_relspart, rid, expectation): assert expectation == presentation_relspart.get_target_rel_uri_str(rid) diff --git a/tests/test_types.py b/tests/test_types.py index 72c2e3b..ea03f83 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -2,11 +2,10 @@ def test_zipname(types): - assert types.zipname == '[Content_Types].xml' + assert types.zipname == "[Content_Types].xml" -@pytest.mark.parametrize("uri_str", - ["presentation_uri_str", "thumbnail_url_str"]) +@pytest.mark.parametrize("uri_str", ["presentation_uri_str", "thumbnail_url_str"]) def test_remove_type(types, uri_str, request): uri_str = request.getfixturevalue(uri_str) before_type = types.get_type(uri_str) @@ -14,9 +13,13 @@ def test_remove_type(types, uri_str, request): assert before_type != types.get_type(uri_str) -@pytest.mark.parametrize("uri_str,type", - [("presentation_uri_str", "presentation_type"), - ("thumbnail_url_str", "thumbnail_type")]) +@pytest.mark.parametrize( + "uri_str,type", + [ + ("presentation_uri_str", "presentation_type"), + ("thumbnail_url_str", "thumbnail_type"), + ], +) def test_get_type(types, uri_str, type, request): uri_str = request.getfixturevalue(uri_str) type = request.getfixturevalue(type) @@ -25,9 +28,9 @@ def test_get_type(types, uri_str, type, request): def test_add_default(types, part): types.add_default(part) - assert types.get_type(part.uri.str) == part.type + assert types.get_type(str(part.uri)) == part.type def test_add_override(types, part): types.add_override(part) - assert types.get_type(part.uri.str) == part.type + assert types.get_type(str(part.uri)) == part.type diff --git a/tests/test_uri.py b/tests/test_uri.py index 25cfe3f..0cfa36f 100644 --- a/tests/test_uri.py +++ b/tests/test_uri.py @@ -1,13 +1,14 @@ import pytest + from opc.uri import Uri @pytest.fixture def get_uri_presentation(): - return Uri('/ppt/presentation.xml') + return Uri("/ppt/presentation.xml") -@pytest.fixture(params=['/_rels/.rels', '/ppt/slides/_rels/slide1.xml.rels']) +@pytest.fixture(params=["/_rels/.rels", "/ppt/slides/_rels/slide1.xml.rels"]) def get_uri_rels(request): return Uri(request.param) @@ -19,51 +20,64 @@ def test_uri_takes_only_str(): def test_uri_str_starts_with_slash(): with pytest.raises(ValueError): - Uri('some/path') + Uri("some/path") def test_uri_str_value(get_uri_presentation): - assert get_uri_presentation.str == '/ppt/presentation.xml' + assert str(get_uri_presentation) == "/ppt/presentation.xml" def test_uri_ext_value(get_uri_rels): - assert get_uri_rels.ext == 'rels' + assert get_uri_rels.ext == "rels" def test_uri_zipname_value(get_uri_presentation): - assert get_uri_presentation.zipname == 'ppt/presentation.xml' + assert get_uri_presentation.zipname == "ppt/presentation.xml" -@pytest.mark.parametrize("uri_str,expectation", - [('/_rels/.rels', True), - ('/docProps/core.xml', False)]) +@pytest.mark.parametrize( + "uri_str,expectation", [("/_rels/.rels", True), ("/docProps/core.xml", False)] +) def test_uri_is_rels(uri_str, expectation): assert Uri(uri_str).is_rels == expectation -@pytest.mark.parametrize("uri_str, expectation", - [('/', '/_rels/.rels'), - ('/docProps/core.xml', - '/docProps/_rels/core.xml.rels')]) +@pytest.mark.parametrize( + "uri_str, expectation", + [("/", "/_rels/.rels"), ("/docProps/core.xml", "/docProps/_rels/core.xml.rels")], +) def test_uri_rels(uri_str, expectation): assert Uri(uri_str).rels == expectation -@pytest.mark.parametrize("zipname, expectation", - [('_rels/.rels', '/_rels/.rels'), - ('docProps/core.xml', '/docProps/core.xml')]) +@pytest.mark.parametrize( + "zipname, expectation", + [("_rels/.rels", "/_rels/.rels"), ("docProps/core.xml", "/docProps/core.xml")], +) def test_uri_zipname2str(zipname, expectation): assert Uri.zipname2str(zipname) == expectation -@pytest.mark.parametrize("uri_str, rel_target_uri_str, expectation", [ - ('/ppt/presentation.xml', 'slides/slide1.xml', '/ppt/slides/slide1.xml'), - ('/ppt/slides/slide1.xml', "../slideLayouts/slideLayout1.xml", - "/ppt/slideLayouts/slideLayout1.xml"), - ('/ppt/slides/slide1.xml', "../embeddings/Microsoft_Excel_Worksheet.xlsx", - "/ppt/embeddings/Microsoft_Excel_Worksheet.xlsx"), - ('/ppt/handoutMasters/handoutMaster1.xml', - "../theme/theme4.xml", "/ppt/theme/theme4.xml"), -]) +@pytest.mark.parametrize( + "uri_str, rel_target_uri_str, expectation", + [ + ("/ppt/presentation.xml", "slides/slide1.xml", "/ppt/slides/slide1.xml"), + ( + "/ppt/slides/slide1.xml", + "../slideLayouts/slideLayout1.xml", + "/ppt/slideLayouts/slideLayout1.xml", + ), + ( + "/ppt/slides/slide1.xml", + "../embeddings/Microsoft_Excel_Worksheet.xlsx", + "/ppt/embeddings/Microsoft_Excel_Worksheet.xlsx", + ), + ( + "/ppt/handoutMasters/handoutMaster1.xml", + "../theme/theme4.xml", + "/ppt/theme/theme4.xml", + ), + ], +) def test_uri_get_abs(uri_str, rel_target_uri_str, expectation): assert Uri(uri_str).get_abs(rel_target_uri_str) == expectation diff --git a/tox.ini b/tox.ini index 0ff3277..4fc16f3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,20 @@ # content of: tox.ini , put in same dir as setup.py [tox] -envlist = py311 +envlist = py311, py312, py313 isolated_build = True # ignore_base_python_conflict = False +[gh-actions] +python = + 3.11: py311 + 3.12: py312 + 3.13: py313 [testenv] +setenv = + PYTHONPATH = {toxinidir} deps = pytest # install pytest in the virtualenv where commands will be executed commands = # whatever extra steps before testing might be necessary - pytest # or any other test runner that you might use \ No newline at end of file + pytest --basetemp={envtmpdir} # or any other test runner that you might use