diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..e9b813d
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,91 @@
+# Copilot instructions for boinc-client
+
+## Build, test, and formatting commands
+
+This repository uses Poetry for dependency and environment management.
+
+```bash
+poetry install
+
+# Build distributable package
+poetry build
+```
+
+Run tests through the Makefile targets (they also run formatting first):
+
+```bash
+# Unit tests only (excludes integration + authenticated)
+make unittest
+
+# Integration tests (Docker required)
+make integration
+
+# Authenticated tests (Docker required, optional BOINC_PROJECT_KEY)
+make authenticated
+
+# Full test suite
+make test
+```
+
+Run a single test directly with pytest:
+
+```bash
+# Single test function
+poetry run pytest tests/projects/test_projects.py::test_can_attach_to_project -vv
+
+# Single integration test
+poetry run pytest tests/integration/test_boinc_client.py::test_results -m integration -vv
+```
+
+Coverage workflow used by this repo:
+
+```bash
+make coverage
+```
+
+Formatting commands:
+
+```bash
+make fmt
+make fmtxml
+```
+
+Style checks (no dedicated lint target; run tools in check mode):
+
+```bash
+poetry run black --check src/ tests/
+poetry run isort --profile=black --check-only src/ tests/
+```
+
+## High-level architecture
+
+- `src/boinc_client/boinc_client.py` exposes the `Boinc` facade class. Public methods are thin delegations to domain modules (`messages.py`, `projects.py`, `status.py`, etc.).
+- `src/boinc_client/clients/rpc_client.py` is the transport layer. It manages socket connection/authentication to BOINC RPC, wraps outgoing payloads in `...\003`, reads until `\003`, and strips `` wrappers in `make_request`.
+- Domain modules in `src/boinc_client/*.py` follow the same pipeline:
+ 1. Build XML request strings for BOINC RPC
+ 2. Call `RpcClient.make_request`
+ 3. Parse XML with `xmltodict.parse` (often with `force_list`)
+ 4. Load into marshmallow schemas under `src/boinc_client/models/`
+- Marshmallow schemas are the canonical response normalization layer. They convert variable RPC XML shapes into consistent Python dict structures returned by the public API.
+- Tests are split by marker and runtime dependency:
+ - Unit tests: mock `RpcClient.make_request`
+ - `integration`: real BOINC Docker container via `testcontainers`
+ - `authenticated`: integration flow requiring BOINC RPC password/auth operations
+
+## Key codebase conventions
+
+- Keep `Boinc` methods as wrappers; put operation logic in domain modules (`projects.py`, `status.py`, etc.), not in `boinc_client.py`.
+- New RPC operations should return normalized dicts by introducing/updating marshmallow schemas rather than returning raw `xmltodict` output.
+- Use `xmltodict.parse(..., force_list=...)` when BOINC may return one-or-many nodes; this prevents shape drift between single-item and multi-item responses.
+- Prefer shared helpers in `models/helpers.py` (`flatten_data`, `normalise_none_to_list`, `create_indexes`, `set_bools`, etc.) for schema transformations instead of duplicating normalization logic.
+- Pre/post-load transform methods in schemas use alphabetical prefixes (`_a_`, `_b_`, `_c_`) to keep transformation order explicit and stable.
+- Generic command responses use `GenericResponse` (`{"success": bool, "error": ...}`), with success inferred from presence of ``.
+- Integration fixtures in `tests/integration/conftest.py` standardize container setup (`boinc/client`, port `31416`, `--allow_remote_gui_rpc`) and should be reused for new integration/authenticated tests.
+
+## MCP server guidance
+
+- Use GitHub MCP tools for repository and CI workflows instead of manual API/scraping:
+ - PR/issue context: `list_pull_requests`, `pull_request_read`, `list_issues`, `issue_read`
+ - CI diagnostics: `actions_list` (runs/jobs/artifacts) and `get_job_logs` for failed jobs
+ - Commit/file inspection during reviews: `list_commits`, `get_commit`, `get_file_contents`
+- Prefer MCP read/list/search tools when triaging CI failures or preparing release-related changes, because this repository depends on GitHub Actions (`.github/workflows/pull-request.yml`, `test-and-release.yml`) for validation and publishing.
diff --git a/.gitignore b/.gitignore
index ca9a1a1..fcff2f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,6 @@ dist/
.coverage
cov/
-boinc_client.log
\ No newline at end of file
+old_*
+boinc_client.log
+
diff --git a/Makefile b/Makefile
index 361911a..2f6105e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,7 @@
+.PHONY: clean-cov
+clean-cov:
+ rm -f cov/*.coverage cov/.coverage
+
.PHONY: fmt
fmt:
poetry run black src/ tests/
@@ -8,7 +12,7 @@ fmtxml: fmt
find tests/test_files -type f -name "*.xml" -exec echo "- {}" \; -exec poetry run xmlformat --indent 4 --overwrite {} \;
.PHONY: unittest
-unittest: fmt
+unittest: clean-cov fmt
poetry run coverage run --data-file cov/unittest.coverage -m pytest -m "not integration and not authenticated" -vv
.PHONY: integration
diff --git a/poetry.lock b/poetry.lock
index 6e20fa7..9df1a5f 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -321,96 +321,152 @@ colors = ["colorama (>=0.4.6)"]
[[package]]
name = "lxml"
-version = "5.1.0"
+version = "6.1.1"
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"
files = [
- {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e"},
- {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da"},
- {file = "lxml-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c"},
- {file = "lxml-5.1.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb"},
- {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2befa20a13f1a75c751f47e00929fb3433d67eb9923c2c0b364de449121f447c"},
- {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22b7ee4c35f374e2c20337a95502057964d7e35b996b1c667b5c65c567d2252a"},
- {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05"},
- {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147"},
- {file = "lxml-5.1.0-cp310-cp310-win32.whl", hash = "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93"},
- {file = "lxml-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d"},
- {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"},
- {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"},
- {file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"},
- {file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"},
- {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e"},
- {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45"},
- {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2"},
- {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"},
- {file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"},
- {file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"},
- {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"},
- {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"},
- {file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"},
- {file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"},
- {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431"},
- {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1"},
- {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3"},
- {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8"},
- {file = "lxml-5.1.0-cp312-cp312-win32.whl", hash = "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01"},
- {file = "lxml-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623"},
- {file = "lxml-5.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:24ef5a4631c0b6cceaf2dbca21687e29725b7c4e171f33a8f8ce23c12558ded1"},
- {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d2900b7f5318bc7ad8631d3d40190b95ef2aa8cc59473b73b294e4a55e9f30f"},
- {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d"},
- {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95"},
- {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7"},
- {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:262bc5f512a66b527d026518507e78c2f9c2bd9eb5c8aeeb9f0eb43fcb69dc67"},
- {file = "lxml-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd"},
- {file = "lxml-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7"},
- {file = "lxml-5.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5"},
- {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5"},
- {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3898ae2b58eeafedfe99e542a17859017d72d7f6a63de0f04f99c2cb125936"},
- {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862"},
- {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3aeca824b38ca78d9ee2ab82bd9883083d0492d9d17df065ba3b94e88e4d7ee6"},
- {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764"},
- {file = "lxml-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8"},
- {file = "lxml-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b"},
- {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1"},
- {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5"},
- {file = "lxml-5.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84"},
- {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa"},
- {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45"},
- {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1"},
- {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e"},
- {file = "lxml-5.1.0-cp38-cp38-win32.whl", hash = "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a"},
- {file = "lxml-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1"},
- {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354"},
- {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969"},
- {file = "lxml-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3"},
- {file = "lxml-5.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581"},
- {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8b0c78e7aac24979ef09b7f50da871c2de2def043d468c4b41f512d831e912"},
- {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d"},
- {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:49a9b4af45e8b925e1cd6f3b15bbba2c81e7dba6dce170c677c9cda547411e14"},
- {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:280f3edf15c2a967d923bcfb1f8f15337ad36f93525828b40a0f9d6c2ad24890"},
- {file = "lxml-5.1.0-cp39-cp39-win32.whl", hash = "sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e"},
- {file = "lxml-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8d7b4beebb178e9183138f552238f7e6613162a42164233e2bda00cb3afac58f"},
- {file = "lxml-5.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae"},
- {file = "lxml-5.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa"},
- {file = "lxml-5.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372"},
- {file = "lxml-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df"},
- {file = "lxml-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea"},
- {file = "lxml-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33"},
- {file = "lxml-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324"},
- {file = "lxml-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897"},
- {file = "lxml-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6"},
- {file = "lxml-5.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3"},
- {file = "lxml-5.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f"},
- {file = "lxml-5.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f"},
- {file = "lxml-5.1.0.tar.gz", hash = "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca"},
+ {file = "lxml-6.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09dd5b7075dc2f7709654a46543ba1ea3c2e217b2ed8fbd413a8a945a0f40f60"},
+ {file = "lxml-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f6ac4ef4d82dff54670227a69c67782ae0b811b5cf6b17954f1e8f7502fc0d1d"},
+ {file = "lxml-6.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:556e94a63c9b04716f8e4de2abb65775061f846e89331b6c5be79183a24f98ea"},
+ {file = "lxml-6.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6bf403fbb3b3e348a561a5f4f0b9961835657981c802a1df03653eef8a9074"},
+ {file = "lxml-6.1.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dde6131244bba38a17c745836ba190bc753fd73c9291666287fd0a3fa3dcf30"},
+ {file = "lxml-6.1.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98fc784c2c1440667aeedf8465bdfe10208acf0ead656a2c68627299f546b315"},
+ {file = "lxml-6.1.1-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:add8cf6ddf9a65116119a28ece0f7886e30af27ba724a7594305f1d1b58a92a1"},
+ {file = "lxml-6.1.1-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cf9d57306d848218f3601fee7601fab1a327c942d56e2e97610583cb4dd74206"},
+ {file = "lxml-6.1.1-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88136950da4d13c318bde414ce10219931937851327f44328f2df4d2c4614067"},
+ {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cecdd5dfdc87b1fd87dbf81d4b037a544f47f4c744200a67013771682d67686a"},
+ {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd312b9692e831d2ffcad61eab31d91d4b4655a962e61de8fb410472cbcd37aa"},
+ {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5b7328b46d49fc9477d91ae8f6d55340347d827b7734ba3ea33faae0efef1383"},
+ {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37a58976370f36d9329d118ad0b953c5aeb9119ac9c6a4e258942a225d0573a1"},
+ {file = "lxml-6.1.1-cp310-cp310-win32.whl", hash = "sha256:cea3f4c1af79af13cdb2da0c028111d8f8522d4f22a000c82385535f24e5cf3a"},
+ {file = "lxml-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3abf332af33a74288675d936fe861fd4344da0dd6622193fbc4f2bfbb35536b5"},
+ {file = "lxml-6.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:8dadbe5b217ff35b6a8d16610dd710219b59b76d13f0e3f0d9f36786206e4485"},
+ {file = "lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2"},
+ {file = "lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d"},
+ {file = "lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510"},
+ {file = "lxml-6.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a"},
+ {file = "lxml-6.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d"},
+ {file = "lxml-6.1.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8"},
+ {file = "lxml-6.1.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009"},
+ {file = "lxml-6.1.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6"},
+ {file = "lxml-6.1.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8"},
+ {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83"},
+ {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6"},
+ {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c"},
+ {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08"},
+ {file = "lxml-6.1.1-cp311-cp311-win32.whl", hash = "sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621"},
+ {file = "lxml-6.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28"},
+ {file = "lxml-6.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b"},
+ {file = "lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7"},
+ {file = "lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1"},
+ {file = "lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc"},
+ {file = "lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc"},
+ {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383"},
+ {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b"},
+ {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818"},
+ {file = "lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740"},
+ {file = "lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f"},
+ {file = "lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2"},
+ {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635"},
+ {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf"},
+ {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc"},
+ {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955"},
+ {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a"},
+ {file = "lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a"},
+ {file = "lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77"},
+ {file = "lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f"},
+ {file = "lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736"},
+ {file = "lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9"},
+ {file = "lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354"},
+ {file = "lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca"},
+ {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099"},
+ {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6"},
+ {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085"},
+ {file = "lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e"},
+ {file = "lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f"},
+ {file = "lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c"},
+ {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b"},
+ {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2"},
+ {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5"},
+ {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785"},
+ {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947"},
+ {file = "lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca"},
+ {file = "lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660"},
+ {file = "lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc"},
+ {file = "lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0"},
+ {file = "lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840"},
+ {file = "lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14"},
+ {file = "lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909"},
+ {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00"},
+ {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955"},
+ {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13"},
+ {file = "lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7"},
+ {file = "lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245"},
+ {file = "lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5"},
+ {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462"},
+ {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465"},
+ {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a"},
+ {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590"},
+ {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb"},
+ {file = "lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603"},
+ {file = "lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137"},
+ {file = "lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf"},
+ {file = "lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee"},
+ {file = "lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c"},
+ {file = "lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef"},
+ {file = "lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a"},
+ {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c"},
+ {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525"},
+ {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca"},
+ {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e"},
+ {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038"},
+ {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e"},
+ {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072"},
+ {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52"},
+ {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b"},
+ {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2"},
+ {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e"},
+ {file = "lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1"},
+ {file = "lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e"},
+ {file = "lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c"},
+ {file = "lxml-6.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6689e828a94eee4f139408c337bb198e014724bb8a8c26d3cfac49d119ed69a6"},
+ {file = "lxml-6.1.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdebcc8a75d38c7598dfb2c9ed852d7a9eb4a10d6e2d0764b919b802bf32ac88"},
+ {file = "lxml-6.1.1-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8be8ad51249698103d24b0571df35a10990fbe93dd043b6c024172189485f5e3"},
+ {file = "lxml-6.1.1-cp38-cp38-manylinux_2_28_i686.whl", hash = "sha256:76447f65250ed2501ead1a1552f5ce8edff159a86f308348e6a9c4acb5e1f1b4"},
+ {file = "lxml-6.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ffecec8eb889b58ba9be5b95fb1cc78e22ea8eedea38e8736a1568fe1979250e"},
+ {file = "lxml-6.1.1-cp38-cp38-win32.whl", hash = "sha256:c674693f055fa2495de12292cb45e9944199d8eaef5a2dec45175c7c61cb73e3"},
+ {file = "lxml-6.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:55b03549819867ea141c0202242c4816c82e52ec36e7e648db9d8da5a3dc3ed6"},
+ {file = "lxml-6.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9f79d5325907f13e1be0b3e4dacc1049d1dffc4aeee3c995284bea5fe0fab7d"},
+ {file = "lxml-6.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:83b6b30eb131da7a75b601f28c5d6971e6ed3e887919bf6b6a1ad3c2df289080"},
+ {file = "lxml-6.1.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:441dd227fa0690eb9fc81edabc63cdcefc212bba99b906dcf6e32cc1a9d3e533"},
+ {file = "lxml-6.1.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e07c65f443c887bbcf31cc1771d932ecc192a5273943589b3c7572b749f1ffb2"},
+ {file = "lxml-6.1.1-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5bec7d03d78d853597d6107854c2310ce3f761fd218fe9fe91d5101fcf6c2efe"},
+ {file = "lxml-6.1.1-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f76acfb5f68ba982635a53fd985a8044be98a35b43232c2a1ee235ffab3e1dd"},
+ {file = "lxml-6.1.1-cp39-cp39-manylinux_2_28_i686.whl", hash = "sha256:8d43ca737b20e106e4aebc42b2f3ae19f00ba63d7eb731698ee083d72d15646f"},
+ {file = "lxml-6.1.1-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:32ab449a5486f6c758e849bb86710d0e45edc24a04e250c01555f8f5653958f8"},
+ {file = "lxml-6.1.1-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53c909b62a0532183542fed00c5a7218258c56292d409bc789886fe1cb04c438"},
+ {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:640f97d43d867bcb9c75b3af013b64850756b746cb6bce8ace83b70da3abba9d"},
+ {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:469e3618338bd7ab5beb412d2439825479fcf0dab99e394ca563dbc4eaf6c834"},
+ {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:aae97dfdb60715c164419ac2532a76d013c3918a665eb6cb7288098b5f349aaf"},
+ {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c9a4b821dc7055bf9e05ff5719e18ec501f75c0f0bbfabd573b277559780833d"},
+ {file = "lxml-6.1.1-cp39-cp39-win32.whl", hash = "sha256:639f6c857d91d9be29bd7502348d6736dab168b54b5158cd899abf11684dc186"},
+ {file = "lxml-6.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:34c2d737beabfe35baada43941ed519251e9a12e779031496bcd5d539fcfd730"},
+ {file = "lxml-6.1.1-cp39-cp39-win_arm64.whl", hash = "sha256:07a4a68e286ee7a1ed7dfb8af83e615757c0ccfe9f18c6b4ea6771388d9ba8c9"},
+ {file = "lxml-6.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e"},
+ {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004"},
+ {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e"},
+ {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2"},
+ {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf"},
+ {file = "lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84"},
+ {file = "lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40"},
]
[package.extras]
cssselect = ["cssselect (>=0.7)"]
+html-clean = ["lxml_html_clean"]
html5 = ["html5lib"]
htmlsoup = ["BeautifulSoup4"]
-source = ["Cython (>=3.0.7)"]
[[package]]
name = "marshmallow"
@@ -751,4 +807,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
-content-hash = "9eb0efd33d60fc8fc8f333f67f28a1c78b0ad01ec3550a5bc3d3890eeb2c446a"
+content-hash = "a2e01222f56ebb403031aeb50645e1049eaf617b5e1627cdff4a96851e52f7c1"
diff --git a/pyproject.toml b/pyproject.toml
index 431506b..585732b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ issues = "https://github.com/SplinterHead/boinc-client/issues"
[tool.poetry.dependencies]
python = "^3.9"
-lxml = ">=4.9.2,<6.0.0"
+lxml = ">=6.0.2,<7.0.0"
marshmallow = "^3.19.0"
xmltodict = "^0.13.0"
diff --git a/src/boinc_client/boinc_client.py b/src/boinc_client/boinc_client.py
index e8b70c3..88c5dc0 100644
--- a/src/boinc_client/boinc_client.py
+++ b/src/boinc_client/boinc_client.py
@@ -39,7 +39,7 @@
class Boinc:
- rpc_client: RpcClient
+ rpc_client: RpcClient = None
def __init__(self, rpc_client: RpcClient):
self.rpc_client = rpc_client
diff --git a/src/boinc_client/models/additional_fields.py b/src/boinc_client/models/additional_fields.py
new file mode 100644
index 0000000..b9db17d
--- /dev/null
+++ b/src/boinc_client/models/additional_fields.py
@@ -0,0 +1,26 @@
+from marshmallow import fields
+
+# Additional fields for ProjectState
+scheduler_rpc_pending = fields.Bool()
+suspended_via_gui = fields.Bool()
+dont_request_more_work = fields.Bool()
+ended = fields.Bool()
+project_files_downloaded_time = fields.Float()
+
+# Additional fields for Result
+ready_to_report = fields.Bool()
+got_server_ack = fields.Bool()
+final_cpu_time = fields.Float()
+final_elapsed_time = fields.Float()
+state = fields.Int()
+exit_status = fields.Int()
+signal = fields.Int()
+suspended_via_gui = fields.Bool()
+project_suspended_via_gui = fields.Bool()
+edf_scheduled = fields.Bool()
+scheduler_rpc_pending = fields.Bool()
+coproc_missing = fields.Bool()
+runtime_outlier = fields.Bool()
+
+# Other additional fields
+project_backend = fields.Str()
diff --git a/src/boinc_client/models/client_state.py b/src/boinc_client/models/client_state.py
index 39b4ed5..b0de66a 100644
--- a/src/boinc_client/models/client_state.py
+++ b/src/boinc_client/models/client_state.py
@@ -1,4 +1,4 @@
-from marshmallow import Schema, fields, post_load, pre_load
+from marshmallow import EXCLUDE, Schema, fields, post_load, pre_load
from boinc_client.models.helpers import (
create_lists,
@@ -43,6 +43,9 @@ class AppDetails(Schema):
class AppFileRef(Schema):
+ class Meta:
+ unknown = EXCLUDE
+
copy_file = fields.Bool()
file_name = fields.Str()
main_program = fields.Bool()
@@ -60,13 +63,21 @@ def _set_copy_file(self, data, **kwargs):
class AppVersion(Schema):
+ class Meta:
+ unknown = EXCLUDE
+
api_version = fields.Str()
app_name = fields.Str()
avg_ncpus = fields.Float()
- file_ref = fields.Nested(AppFileRef(many=True))
+ file_ref = fields.Nested(
+ AppFileRef(many=True), required=False, allow_none=True, load_default=None
+ )
flops = fields.Float()
platform = fields.Str()
version_num = fields.Int()
+ plan_class = fields.Str(allow_none=True)
+ is_wrapper = fields.Bool(allow_none=True)
+ cmdline = fields.Str(allow_none=True)
class WorkUnit(Schema):
@@ -79,6 +90,8 @@ class WorkUnit(Schema):
rsc_fpops_est = fields.Float()
rsc_memory_bound = fields.Float()
version_num = fields.Int()
+ plan_class = fields.Str(allow_none=True)
+ cmdline = fields.Str(allow_none=True)
class GlobalPreferences(Schema):
@@ -126,6 +139,9 @@ def _a_replace_none_string(self, data, **kwargs):
class State(Schema):
+ class Meta:
+ unknown = EXCLUDE
+
app_versions = fields.Nested(
AppVersion(many=True), data_key="app_version", allow_none=True
)
diff --git a/src/boinc_client/models/host_info.py b/src/boinc_client/models/host_info.py
index fe14d7b..aa7c266 100644
--- a/src/boinc_client/models/host_info.py
+++ b/src/boinc_client/models/host_info.py
@@ -4,7 +4,7 @@
class CoProc(Schema):
- name: fields.Str()
+ name = fields.Str()
class Host(Schema):
diff --git a/src/boinc_client/models/project_status.py b/src/boinc_client/models/project_status.py
index 5684231..8b03f5c 100644
--- a/src/boinc_client/models/project_status.py
+++ b/src/boinc_client/models/project_status.py
@@ -8,6 +8,27 @@
set_bools,
)
+""" from boinc_client.models.additional_fields import (
+ scheduler_rpc_pending,
+ suspended_via_gui,
+ dont_request_more_work,
+ ended,
+ project_files_downloaded_time,
+ ready_to_report,
+ got_server_ack,
+ final_cpu_time,
+ final_elapsed_time,
+ state,
+ exit_status,
+ signal,
+ project_suspended_via_gui,
+ edf_scheduled,
+ scheduler_rpc_pending,
+ coproc_missing,
+ runtime_outlier,
+ project_backend
+) """
+
class RscBackoffTime(Schema):
name = fields.Str()
@@ -76,6 +97,10 @@ class ProjectState(Schema):
user_name = fields.Str(allow_none=True)
user_total_credit = fields.Float()
userid = fields.Int()
+ no_rsc_pref = fields.Bool(allow_none=True)
+ venue = fields.Str(allow_none=True)
+ verify_files_on_app_start = fields.Bool(allow_none=True)
+ attached_via_acct_mgr = fields.Bool(allow_none=True)
@pre_load
def _a_create_keys(self, data, **kwargs):
@@ -102,6 +127,18 @@ def _c_set_bools(self, data, **kwargs):
def _a_replace_none_string(self, data, **kwargs):
return replace_none_string(data)
+ @post_load
+ def _b_drop_empty_extension_fields(self, data, **kwargs):
+ for key in (
+ "no_rsc_pref",
+ "venue",
+ "verify_files_on_app_start",
+ "attached_via_acct_mgr",
+ ):
+ if data.get(key) == "":
+ data.pop(key, None)
+ return data
+
class ProjectStatus(Schema):
project_status = fields.Nested(ProjectState(many=True), data_key="projects")
diff --git a/src/boinc_client/models/result.py b/src/boinc_client/models/result.py
index 3478fbb..ebb4930 100644
--- a/src/boinc_client/models/result.py
+++ b/src/boinc_client/models/result.py
@@ -1,4 +1,4 @@
-from marshmallow import Schema, fields, pre_load
+from marshmallow import Schema, fields, post_load, pre_load
from boinc_client.models.helpers import flatten_data, normalise_none_to_list
@@ -22,6 +22,13 @@ class ActiveTask(Schema):
swap_size = fields.Float()
working_set_size = fields.Float()
working_set_size_smoothed = fields.Float()
+ too_large = fields.Bool(required=False, allow_none=True)
+
+ @post_load
+ def _a_drop_none_optional_fields(self, data, **kwargs):
+ if data.get("too_large", None) is None:
+ data.pop("too_large", None)
+ return data
class Result(Schema):
@@ -43,6 +50,9 @@ class Result(Schema):
state = fields.Int()
version_num = fields.Int()
wu_name = fields.Str()
+ report_immediately = fields.Bool(required=False, allow_none=True)
+ resources = fields.Str(required=False, allow_none=True)
+ suspended_via_gui = fields.Bool(required=False, allow_none=True)
@pre_load
def _set_ready(self, data, **kwargs):
@@ -59,6 +69,13 @@ def _set_suspended(self, data, **kwargs):
data["project_suspended_via_gui"] = "project_suspended_via_gui" in data
return data
+ @post_load
+ def _a_drop_none_optional_fields(self, data, **kwargs):
+ for key in ("report_immediately", "resources", "suspended_via_gui"):
+ if data.get(key, None) is None:
+ data.pop(key, None)
+ return data
+
class Results(Schema):
results = fields.Nested(Result(many=True))
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
index b3a3648..9ee7886 100644
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -1,6 +1,6 @@
import os
-from pytest import fixture, mark
+from pytest import fixture
from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_for_logs
@@ -12,7 +12,6 @@
BOINC_PASSWORD = "password"
-@mark.integration
@fixture(scope="session")
def boinc_session_container() -> DockerContainer:
boinc_container = DockerContainer("boinc/client")
@@ -27,7 +26,6 @@ def boinc_session_container() -> DockerContainer:
container.stop()
-@mark.authenticated
@fixture
def boinc_test_container() -> DockerContainer:
boinc_container = DockerContainer("boinc/client")
@@ -42,7 +40,6 @@ def boinc_test_container() -> DockerContainer:
container.stop()
-@mark.integration
@fixture(scope="session")
def rpc_session_client(boinc_session_container) -> RpcClient:
client = RpcClient(hostname="localhost", port=BOINC_PORT, password=BOINC_PASSWORD)
@@ -50,7 +47,6 @@ def rpc_session_client(boinc_session_container) -> RpcClient:
return client
-@mark.authenticated
@fixture
def rpc_test_client(boinc_test_container) -> RpcClient:
client = RpcClient(hostname="localhost", port=BOINC_PORT, password=BOINC_PASSWORD)
@@ -58,13 +54,11 @@ def rpc_test_client(boinc_test_container) -> RpcClient:
return client
-@mark.integration
@fixture(scope="session")
def boinc_session_client(rpc_session_client) -> Boinc:
return Boinc(rpc_client=rpc_session_client)
-@mark.authenticated
@fixture
def boinc_test_client(rpc_test_client, project_weak_key) -> Boinc:
client = Boinc(rpc_client=rpc_test_client)
diff --git a/tests/status/test_status.py b/tests/status/test_status.py
index 559bbd5..a83c8e2 100644
--- a/tests/status/test_status.py
+++ b/tests/status/test_status.py
@@ -1,3 +1,5 @@
+from boinc_client.models.host_info import CoProc
+from boinc_client.models.project_status import ProjectState
from boinc_client.status import (
cc_status,
client_state,
@@ -300,3 +302,24 @@ def test_can_get_suspended_project_status(
return_value=suspended_project_status_xml,
)
assert project_status(client=mock_rpc_client) == suspended_project_status_dict
+
+
+def test_coproc_schema_loads_name_field():
+ assert CoProc().load({"name": "intel-gpu"}) == {"name": "intel-gpu"}
+
+
+def test_project_state_drops_empty_extension_fields():
+ data = {
+ "no_rsc_pref": "",
+ "venue": "",
+ "verify_files_on_app_start": "",
+ "attached_via_acct_mgr": "",
+ "project_name": "example",
+ }
+ cleaned = ProjectState()._b_drop_empty_extension_fields(data, many=False)
+
+ assert "no_rsc_pref" not in cleaned
+ assert "venue" not in cleaned
+ assert "verify_files_on_app_start" not in cleaned
+ assert "attached_via_acct_mgr" not in cleaned
+ assert cleaned["project_name"] == "example"
diff --git a/tests/test_boinc_client.py b/tests/test_boinc_client.py
index 42df6b4..a291e6c 100644
--- a/tests/test_boinc_client.py
+++ b/tests/test_boinc_client.py
@@ -18,3 +18,174 @@ def test_thrown_exception_output_code_is_translated(mocker):
def test_can_create_api_with_boinc_client(mock_rpc_client):
assert Boinc(rpc_client=mock_rpc_client)
+
+
+@pytest.mark.parametrize(
+ "method_name,function_name,kwargs",
+ [
+ ("get_messages", "messages", {"start": 5}),
+ ("get_message_count", "message_count", {}),
+ ("get_public_notices", "public_notices", {"start": 10}),
+ ("get_all_notices", "get_all_notices", {"start": 2}),
+ ("get_all_projects", "all_projects", {}),
+ ("get_results", "results", {"active_only": True}),
+ ("get_old_results", "old_results", {}),
+ ("get_network_stats", "daily_network_transfers", {}),
+ ("get_project_stats", "project_stats", {}),
+ ("get_client_state", "client_state", {}),
+ ("get_project_status", "project_status", {}),
+ ("get_cc_status", "cc_status", {}),
+ ("get_disk_stats", "disk_stats", {}),
+ ("get_file_transfers", "file_transfers", {}),
+ ("get_host_info", "host_info", {}),
+ ("get_simple_gui_info", "simple_gui_info", {}),
+ ("get_screensaver_tasks", "screensaver_tasks", {}),
+ ("get_client_version", "client_version", {}),
+ ("get_client_update", "client_update", {}),
+ ],
+)
+def test_boinc_delegates_keyword_calls(
+ mocker, mock_boinc_client, method_name, function_name, kwargs
+):
+ expected = {"ok": method_name}
+ call = mocker.patch(
+ f"boinc_client.boinc_client.{function_name}", return_value=expected
+ )
+
+ result = getattr(mock_boinc_client, method_name)(**kwargs)
+
+ assert result == expected
+ call.assert_called_once_with(client=mock_boinc_client.rpc_client, **kwargs)
+
+
+def test_boinc_delegates_attach_project(mocker, mock_boinc_client):
+ call = mocker.patch(
+ "boinc_client.boinc_client.attach_project", return_value={"ok": True}
+ )
+
+ result = mock_boinc_client.attach_project("name", "http://example.org", "key")
+
+ assert result == {"ok": True}
+ call.assert_called_once_with(
+ mock_boinc_client.rpc_client, "name", "http://example.org", "key"
+ )
+
+
+@pytest.mark.parametrize(
+ "method_name,function_name,arg",
+ [
+ ("update_project", "update_project", "http://example.org"),
+ ("detach_project", "detach_project", "http://example.org"),
+ ("suspend_project", "suspend_project", "http://example.org"),
+ ("resume_project", "resume_project", "http://example.org"),
+ ("reset_project", "reset_project", "http://example.org"),
+ ],
+)
+def test_boinc_delegates_project_single_arg_calls(
+ mocker, mock_boinc_client, method_name, function_name, arg
+):
+ call = mocker.patch(
+ f"boinc_client.boinc_client.{function_name}", return_value={"ok": method_name}
+ )
+
+ result = getattr(mock_boinc_client, method_name)(arg)
+
+ assert result == {"ok": method_name}
+ call.assert_called_once_with(mock_boinc_client.rpc_client, arg)
+
+
+def test_boinc_delegates_poll_attach_project(mocker, mock_boinc_client):
+ call = mocker.patch(
+ "boinc_client.boinc_client.poll_attach_project", return_value={"ok": True}
+ )
+
+ result = mock_boinc_client.poll_attach_project()
+
+ assert result == {"ok": True}
+ call.assert_called_once_with(mock_boinc_client.rpc_client)
+
+
+@pytest.mark.parametrize(
+ "method_name,function_name",
+ [
+ ("get_global_prefs_file", "get_global_prefs_file"),
+ ("get_global_prefs_override", "get_global_prefs_override"),
+ ("get_global_prefs_working", "get_global_prefs_working"),
+ ("read_global_prefs_override", "read_global_prefs_override"),
+ ("get_proxy_settings", "get_proxy_settings"),
+ ],
+)
+def test_boinc_delegates_positional_client_only_calls(
+ mocker, mock_boinc_client, method_name, function_name
+):
+ call = mocker.patch(
+ f"boinc_client.boinc_client.{function_name}", return_value={"ok": method_name}
+ )
+
+ result = getattr(mock_boinc_client, method_name)()
+
+ assert result == {"ok": method_name}
+ call.assert_called_once_with(mock_boinc_client.rpc_client)
+
+
+@pytest.mark.parametrize(
+ "method_name,function_name",
+ [
+ ("project_no_more_work", "project_no_more_work"),
+ ("project_allow_more_work", "project_allow_more_work"),
+ ],
+)
+def test_boinc_delegates_project_work_mode_calls(
+ mocker, mock_boinc_client, method_name, function_name
+):
+ call = mocker.patch(
+ f"boinc_client.boinc_client.{function_name}", return_value={"ok": method_name}
+ )
+ url = "http://example.org"
+
+ result = getattr(mock_boinc_client, method_name)(url)
+
+ assert result == {"ok": method_name}
+ call.assert_called_once_with(client=mock_boinc_client.rpc_client, project_url=url)
+
+
+@pytest.mark.parametrize(
+ "method_name,function_name",
+ [
+ ("set_global_prefs_override", "set_global_prefs_override"),
+ ("update_global_prefs_override", "update_global_prefs_override"),
+ ],
+)
+def test_boinc_delegates_preference_override_calls(
+ mocker, mock_boinc_client, method_name, function_name
+):
+ override = {"max_ncpus_pct": 10}
+ call = mocker.patch(
+ f"boinc_client.boinc_client.{function_name}", return_value={"ok": method_name}
+ )
+
+ result = getattr(mock_boinc_client, method_name)(override)
+
+ assert result == {"ok": method_name}
+ call.assert_called_once_with(mock_boinc_client.rpc_client, override)
+
+
+@pytest.mark.parametrize(
+ "method_name,function_name,run_mode,duration",
+ [
+ ("set_cpu_run_mode", "set_cpu_run_mode", "always", 60),
+ ("set_gpu_run_mode", "set_gpu_run_mode", "auto", 0),
+ ("set_network_mode", "set_network_mode", "never", 30),
+ ],
+)
+def test_boinc_delegates_run_mode_calls(
+ mocker, mock_boinc_client, method_name, function_name, run_mode, duration
+):
+ call = mocker.patch(
+ f"boinc_client.boinc_client.{function_name}", return_value={"ok": method_name}
+ )
+
+ result = getattr(mock_boinc_client, method_name)(run_mode, duration)
+
+ assert result == {"ok": method_name}
+ call.assert_called_once_with(mock_boinc_client.rpc_client, run_mode, duration)