From 1e9be05e8fd3dbf3246665e2ea29125b7ac8ab0a Mon Sep 17 00:00:00 2001 From: jtdub Date: Thu, 6 Aug 2026 09:38:00 -0500 Subject: [PATCH 1/6] Migrate to hier_config 4.0.0b1 Switch the hier-config dependency from the git checkout / 3.x range to the 4.0.0b1 beta from PyPI, using a specifier that explicitly includes the prerelease so pip and poetry resolve it. v4 removes the module-level constructor functions, so replace all get_hconfig(platform, text) calls in the service layer with the v4 classmethod HConfig.from_text(platform, text) per the official v3-to-v4 migration guide. No other v3-only APIs (config_to_get_to, dump_simple, tags_add/tags_remove, v2 compatibility helpers, negation rule models) were in use, and WorkflowRemediation is unchanged in v4, so no further code changes are required. poetry.lock is regenerated against the new pin; all 21 tests plus ruff, ruff format, and mypy strict pass. Co-Authored-By: Claude Fable 5 --- hier_config_api/services/config_service.py | 14 +- hier_config_api/services/platform_service.py | 8 +- .../services/remediation_service.py | 6 +- poetry.lock | 738 +++++++++--------- pyproject.toml | 2 +- 5 files changed, 391 insertions(+), 377 deletions(-) diff --git a/hier_config_api/services/config_service.py b/hier_config_api/services/config_service.py index 8395938..6be493e 100644 --- a/hier_config_api/services/config_service.py +++ b/hier_config_api/services/config_service.py @@ -3,7 +3,7 @@ import re from typing import Any -from hier_config import Platform, WorkflowRemediation, get_hconfig +from hier_config import HConfig, Platform, WorkflowRemediation class ConfigService: @@ -26,7 +26,7 @@ def _get_platform(platform_str: str) -> Platform: def parse_config(platform: str, config_text: str) -> dict[str, Any]: """Parse configuration text into structured format.""" platform_enum = ConfigService._get_platform(platform) - hconfig = get_hconfig(platform_enum, config_text) + hconfig = HConfig.from_text(platform_enum, config_text) # Convert HConfig tree to dictionary representation def config_to_dict(config_obj: Any) -> dict[str, Any]: @@ -47,8 +47,8 @@ def compare_configs( ) -> tuple[str, bool]: """Compare two configurations and return unified diff.""" platform_enum = ConfigService._get_platform(platform) - running_hconfig = get_hconfig(platform_enum, running_config) - intended_hconfig = get_hconfig(platform_enum, intended_config) + running_hconfig = HConfig.from_text(platform_enum, running_config) + intended_hconfig = HConfig.from_text(platform_enum, intended_config) workflow = WorkflowRemediation(running_hconfig, intended_hconfig) remediation = workflow.remediation_config @@ -100,8 +100,8 @@ def merge_configs(platform: str, configs: list[str]) -> str: # Merge each subsequent config for config in configs[1:]: - running_hconfig = get_hconfig(platform_enum, merged) - intended_hconfig = get_hconfig(platform_enum, config) + running_hconfig = HConfig.from_text(platform_enum, merged) + intended_hconfig = HConfig.from_text(platform_enum, config) workflow = WorkflowRemediation(running_hconfig, intended_hconfig) remediation = workflow.remediation_config @@ -121,7 +121,7 @@ def search_config( ) -> list[str]: """Search configuration for matching lines.""" platform_enum = ConfigService._get_platform(platform) - hconfig = get_hconfig(platform_enum, config_text) + hconfig = HConfig.from_text(platform_enum, config_text) matches = [] diff --git a/hier_config_api/services/platform_service.py b/hier_config_api/services/platform_service.py index c78e195..d89c5b0 100644 --- a/hier_config_api/services/platform_service.py +++ b/hier_config_api/services/platform_service.py @@ -2,7 +2,7 @@ from typing import Any -from hier_config import Platform, WorkflowRemediation, get_hconfig +from hier_config import HConfig, Platform, WorkflowRemediation from hier_config_api.models.platform import PlatformInfo, PlatformRules @@ -86,7 +86,7 @@ def validate_config(platform: str, config_text: str) -> dict[str, Any]: try: # Try to parse the configuration platform_enum = PlatformService._get_platform(platform) - get_hconfig(platform_enum, config_text) + HConfig.from_text(platform_enum, config_text) # Basic validation checks if not config_text.strip(): @@ -138,8 +138,8 @@ def process_batch_job(job_data: dict[str, Any]) -> dict[str, Any]: intended_config = device_config.get("intended_config", "") platform_enum = PlatformService._get_platform(platform) - running_hconfig = get_hconfig(platform_enum, running_config) - intended_hconfig = get_hconfig(platform_enum, intended_config) + running_hconfig = HConfig.from_text(platform_enum, running_config) + intended_hconfig = HConfig.from_text(platform_enum, intended_config) workflow = WorkflowRemediation(running_hconfig, intended_hconfig) remediation = workflow.remediation_config diff --git a/hier_config_api/services/remediation_service.py b/hier_config_api/services/remediation_service.py index 56fc3b0..b027c57 100644 --- a/hier_config_api/services/remediation_service.py +++ b/hier_config_api/services/remediation_service.py @@ -2,7 +2,7 @@ from typing import Any -from hier_config import Platform, WorkflowRemediation, get_hconfig +from hier_config import HConfig, Platform, WorkflowRemediation from hier_config_api.models.remediation import RemediationSummary, TagRule @@ -34,8 +34,8 @@ def generate_remediation( ) -> dict[str, Any]: """Generate remediation and rollback configurations.""" platform_enum = RemediationService._get_platform(platform) - running_hconfig = get_hconfig(platform_enum, running_config) - intended_hconfig = get_hconfig(platform_enum, intended_config) + running_hconfig = HConfig.from_text(platform_enum, running_config) + intended_hconfig = HConfig.from_text(platform_enum, intended_config) # Load tag rules if provided if tag_rules: diff --git a/poetry.lock b/poetry.lock index 2cdf02a..3fc5d57 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "annotated-doc" @@ -46,14 +46,14 @@ trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python [[package]] name = "astroid" -version = "4.0.3" +version = "4.0.4" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.10.0" groups = ["dev"] files = [ - {file = "astroid-4.0.3-py3-none-any.whl", hash = "sha256:864a0a34af1bd70e1049ba1e61cee843a7252c826d97825fcee9b2fcbd9e1b14"}, - {file = "astroid-4.0.3.tar.gz", hash = "sha256:08d1de40d251cc3dc4a7a12726721d475ac189e4e583d596ece7422bc176bda3"}, + {file = "astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753"}, + {file = "astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0"}, ] [package.dependencies] @@ -61,14 +61,14 @@ typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} [[package]] name = "babel" -version = "2.17.0" +version = "2.18.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, + {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, + {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, ] [package.extras] @@ -89,19 +89,19 @@ files = [ [[package]] name = "backrefs" -version = "6.1" +version = "6.2" description = "A wrapper around re and regex that adds additional back references." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1"}, - {file = "backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7"}, - {file = "backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a"}, - {file = "backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05"}, - {file = "backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853"}, - {file = "backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0"}, - {file = "backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231"}, + {file = "backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8"}, + {file = "backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be"}, + {file = "backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90"}, + {file = "backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b"}, + {file = "backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7"}, + {file = "backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7"}, + {file = "backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49"}, ] [package.extras] @@ -109,137 +109,137 @@ extras = ["regex"] [[package]] name = "certifi" -version = "2026.1.4" +version = "2026.2.25" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, - {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, ] [[package]] name = "charset-normalizer" -version = "3.4.4" +version = "3.4.5" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, - {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, - {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-win32.whl", hash = "sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e22d1059b951e7ae7c20ef6b06afd10fb95e3c41bf3c4fbc874dba113321c193"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afca7f78067dd27c2b848f1b234623d26b87529296c6c5652168cc1954f2f3b2"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec56a2266f32bc06ed3c3e2a8f58417ce02f7e0356edc89786e52db13c593c98"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b970382e4a36bed897c19f310f31d7d13489c11b4f468ddfba42d41cddfb918"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:573ef5814c4b7c0d59a7710aa920eaaaef383bd71626aa420fba27b5cab92e8d"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:50bcbca6603c06a1dcc7b056ed45c37715fb5d2768feb3bcd37d2313c587a5b9"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1f2da5cbb9becfcd607757a169e38fb82aa5fd86fae6653dea716e7b613fe2cf"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc1c64934b8faf7584924143eb9db4770bbdb16659626e1a1a4d9efbcb68d947"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ae8b03427410731469c4033934cf473426faff3e04b69d2dfb64a4281a3719f8"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:b3e71afc578b98512bfe7bdb822dd6bc57d4b0093b4b6e5487c1e96ad4ace242"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:4b8551b6e6531e156db71193771c93bda78ffc4d1e6372517fe58ad3b91e4659"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:65b3c403a5b6b8034b655e7385de4f72b7b244869a22b32d4030b99a60593eca"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8ce11cd4d62d11166f2b441e30ace226c19a3899a7cf0796f668fba49a9fb123"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-win32.whl", hash = "sha256:66dee73039277eb35380d1b82cccc69cc82b13a66f9f4a18da32d573acf02b7c"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:d29dd9c016f2078b43d0c357511e87eee5b05108f3dd603423cb389b89813969"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:259cd1ca995ad525f638e131dbcc2353a586564c038fc548a3fe450a91882139"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a28afb04baa55abf26df544e3e5c6534245d3daa5178bc4a8eeb48202060d0e"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff95a9283de8a457e6b12989de3f9f5193430f375d64297d323a615ea52cbdb3"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:708c7acde173eedd4bfa4028484426ba689d2103b28588c513b9db2cd5ecde9c"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa92ec1102eaff840ccd1021478af176a831f1bccb08e526ce844b7ddda85c22"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:5fea359734b140d0d6741189fea5478c6091b54ffc69d7ce119e0a05637d8c99"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e545b51da9f9af5c67815ca0eb40676c0f016d0b0381c86f20451e35696c5f95"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:30987f4a8ed169983f93e1be8ffeea5214a779e27ed0b059835c7afe96550ad7"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:149ec69866c3d6c2fb6f758dbc014ecb09f30b35a5ca90b6a8a2d4e54e18fdfe"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:530beedcec9b6e027e7a4b6ce26eed36678aa39e17da85e6e03d7bd9e8e9d7c9"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:14498a429321de554b140013142abe7608f9d8ccc04d7baf2ad60498374aefa2"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2820a98460c83663dd8ec015d9ddfd1e4879f12e06bb7d0500f044fb477d2770"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:aa2f963b4da26daf46231d9b9e0e2c9408a751f8f0d0f44d2de56d3caf51d294"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-win32.whl", hash = "sha256:82cc7c2ad42faec8b574351f8bc2a0c049043893853317bd9bb309f5aba6cb5a"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:92263f7eca2f4af326cd20de8d16728d2602f7cfea02e790dcde9d83c365d7cc"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-win_arm64.whl", hash = "sha256:014837af6fabf57121b6254fa8ade10dceabc3528b27b721a64bbc7b8b1d4eb4"}, + {file = "charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0"}, + {file = "charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644"}, ] [[package]] @@ -272,104 +272,118 @@ markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "coverage" -version = "7.13.2" +version = "7.13.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4af3b01763909f477ea17c962e2cca8f39b350a4e46e3a30838b2c12e31b81b"}, - {file = "coverage-7.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36393bd2841fa0b59498f75466ee9bdec4f770d3254f031f23e8fd8e140ffdd2"}, - {file = "coverage-7.13.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cc7573518b7e2186bd229b1a0fe24a807273798832c27032c4510f47ffdb896"}, - {file = "coverage-7.13.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca9566769b69a5e216a4e176d54b9df88f29d750c5b78dbb899e379b4e14b30c"}, - {file = "coverage-7.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c9bdea644e94fd66d75a6f7e9a97bb822371e1fe7eadae2cacd50fcbc28e4dc"}, - {file = "coverage-7.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5bd447332ec4f45838c1ad42268ce21ca87c40deb86eabd59888859b66be22a5"}, - {file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7c79ad5c28a16a1277e1187cf83ea8dafdcc689a784228a7d390f19776db7c31"}, - {file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:76e06ccacd1fb6ada5d076ed98a8c6f66e2e6acd3df02819e2ee29fd637b76ad"}, - {file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:49d49e9a5e9f4dc3d3dac95278a020afa6d6bdd41f63608a76fa05a719d5b66f"}, - {file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed2bce0e7bfa53f7b0b01c722da289ef6ad4c18ebd52b1f93704c21f116360c8"}, - {file = "coverage-7.13.2-cp310-cp310-win32.whl", hash = "sha256:1574983178b35b9af4db4a9f7328a18a14a0a0ce76ffaa1c1bacb4cc82089a7c"}, - {file = "coverage-7.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:a360a8baeb038928ceb996f5623a4cd508728f8f13e08d4e96ce161702f3dd99"}, - {file = "coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e"}, - {file = "coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e"}, - {file = "coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508"}, - {file = "coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b"}, - {file = "coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b"}, - {file = "coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f"}, - {file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3"}, - {file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b"}, - {file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1"}, - {file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059"}, - {file = "coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031"}, - {file = "coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e"}, - {file = "coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28"}, - {file = "coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d"}, - {file = "coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3"}, - {file = "coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99"}, - {file = "coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f"}, - {file = "coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f"}, - {file = "coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa"}, - {file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce"}, - {file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94"}, - {file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5"}, - {file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b"}, - {file = "coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41"}, - {file = "coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e"}, - {file = "coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894"}, - {file = "coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6"}, - {file = "coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc"}, - {file = "coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f"}, - {file = "coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1"}, - {file = "coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9"}, - {file = "coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c"}, - {file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5"}, - {file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4"}, - {file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c"}, - {file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31"}, - {file = "coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8"}, - {file = "coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb"}, - {file = "coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557"}, - {file = "coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e"}, - {file = "coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7"}, - {file = "coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3"}, - {file = "coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3"}, - {file = "coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421"}, - {file = "coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5"}, - {file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23"}, - {file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c"}, - {file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f"}, - {file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573"}, - {file = "coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343"}, - {file = "coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47"}, - {file = "coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7"}, - {file = "coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef"}, - {file = "coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f"}, - {file = "coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5"}, - {file = "coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4"}, - {file = "coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27"}, - {file = "coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548"}, - {file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660"}, - {file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92"}, - {file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82"}, - {file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892"}, - {file = "coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe"}, - {file = "coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859"}, - {file = "coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6"}, - {file = "coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b"}, - {file = "coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417"}, - {file = "coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee"}, - {file = "coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1"}, - {file = "coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d"}, - {file = "coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6"}, - {file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a"}, - {file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04"}, - {file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f"}, - {file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f"}, - {file = "coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3"}, - {file = "coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba"}, - {file = "coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c"}, - {file = "coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5"}, - {file = "coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3"}, + {file = "coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415"}, + {file = "coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9"}, + {file = "coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf"}, + {file = "coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95"}, + {file = "coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053"}, + {file = "coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9"}, + {file = "coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9"}, + {file = "coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f"}, + {file = "coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f"}, + {file = "coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459"}, + {file = "coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0"}, + {file = "coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246"}, + {file = "coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126"}, + {file = "coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d"}, + {file = "coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9"}, + {file = "coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a"}, + {file = "coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d"}, + {file = "coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd"}, + {file = "coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af"}, + {file = "coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d"}, + {file = "coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b"}, + {file = "coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9"}, + {file = "coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd"}, + {file = "coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997"}, + {file = "coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601"}, + {file = "coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0"}, + {file = "coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb"}, + {file = "coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505"}, + {file = "coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2"}, + {file = "coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056"}, + {file = "coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0"}, + {file = "coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea"}, + {file = "coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932"}, + {file = "coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b"}, + {file = "coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0"}, + {file = "coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91"}, ] [package.dependencies] @@ -455,20 +469,16 @@ python-dateutil = ">=2.8.1" dev = ["flake8", "markdown", "twine", "wheel"] [[package]] -name = "griffe" -version = "1.15.0" +name = "griffelib" +version = "2.0.0" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3"}, - {file = "griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea"}, + {file = "griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f"}, ] -[package.dependencies] -colorama = ">=0.4" - [package.extras] pypi = ["pip (>=24.0)", "platformdirs (>=4.2)", "wheel (>=0.42)"] @@ -486,22 +496,18 @@ files = [ [[package]] name = "hier-config" -version = "3.4.0" +version = "4.0.0b1" description = "A network configuration query and comparison library, used to build remediation configurations." optional = false -python-versions = ">=3.10.0,<4.0" +python-versions = "<4.0,>=3.10.0" groups = ["main"] -files = [] -develop = false +files = [ + {file = "hier_config-4.0.0b1-py3-none-any.whl", hash = "sha256:8beb63844c5ad832e03b88e763be7789e20c522aecd3ebde25ad0822ee5b9326"}, + {file = "hier_config-4.0.0b1.tar.gz", hash = "sha256:238da00c357ebc638972398fb65c2473b0f3c99db8dcf3e7b6bb62bfac630cd6"}, +] [package.dependencies] -pydantic = "^2.9" - -[package.source] -type = "git" -url = "https://github.com/netdevops/hier_config.git" -reference = "HEAD" -resolved_reference = "aa119e54d716e6aaedc9035d10e370a14e929ae9" +pydantic = ">=2.9,<3.0" [[package]] name = "httpcore" @@ -632,19 +638,18 @@ files = [ [[package]] name = "isort" -version = "7.0.0" +version = "8.0.1" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.10.0" groups = ["dev"] files = [ - {file = "isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1"}, - {file = "isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187"}, + {file = "isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75"}, + {file = "isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d"}, ] [package.extras] colors = ["colorama"] -plugins = ["setuptools"] [[package]] name = "jinja2" @@ -666,101 +671,115 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "librt" -version = "0.7.8" +version = "0.8.1" description = "Mypyc runtime library" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "platform_python_implementation != \"PyPy\"" files = [ - {file = "librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d"}, - {file = "librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b"}, - {file = "librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d"}, - {file = "librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d"}, - {file = "librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c"}, - {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c"}, - {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d"}, - {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0"}, - {file = "librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85"}, - {file = "librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c"}, - {file = "librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f"}, - {file = "librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac"}, - {file = "librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c"}, - {file = "librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8"}, - {file = "librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff"}, - {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3"}, - {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75"}, - {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873"}, - {file = "librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7"}, - {file = "librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c"}, - {file = "librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232"}, - {file = "librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63"}, - {file = "librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93"}, - {file = "librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592"}, - {file = "librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850"}, - {file = "librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62"}, - {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b"}, - {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714"}, - {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449"}, - {file = "librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac"}, - {file = "librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708"}, - {file = "librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0"}, - {file = "librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc"}, - {file = "librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2"}, - {file = "librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3"}, - {file = "librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6"}, - {file = "librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d"}, - {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e"}, - {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca"}, - {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93"}, - {file = "librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951"}, - {file = "librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34"}, - {file = "librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09"}, - {file = "librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418"}, - {file = "librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611"}, - {file = "librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758"}, - {file = "librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea"}, - {file = "librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac"}, - {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398"}, - {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81"}, - {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83"}, - {file = "librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d"}, - {file = "librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44"}, - {file = "librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce"}, - {file = "librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f"}, - {file = "librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde"}, - {file = "librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e"}, - {file = "librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b"}, - {file = "librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666"}, - {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581"}, - {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a"}, - {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca"}, - {file = "librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365"}, - {file = "librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32"}, - {file = "librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06"}, - {file = "librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6"}, - {file = "librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b"}, - {file = "librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c"}, - {file = "librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5"}, - {file = "librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71"}, - {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e"}, - {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63"}, - {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94"}, - {file = "librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb"}, - {file = "librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be"}, - {file = "librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862"}, + {file = "librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc"}, + {file = "librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7"}, + {file = "librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6"}, + {file = "librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0"}, + {file = "librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b"}, + {file = "librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891"}, + {file = "librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7"}, + {file = "librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2"}, + {file = "librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd"}, + {file = "librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965"}, + {file = "librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da"}, + {file = "librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0"}, + {file = "librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e"}, + {file = "librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe"}, + {file = "librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb"}, + {file = "librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b"}, + {file = "librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9"}, + {file = "librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a"}, + {file = "librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9"}, + {file = "librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb"}, + {file = "librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d"}, + {file = "librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7"}, + {file = "librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0"}, + {file = "librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a"}, + {file = "librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444"}, + {file = "librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d"}, + {file = "librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35"}, + {file = "librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583"}, + {file = "librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c"}, + {file = "librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04"}, + {file = "librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363"}, + {file = "librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d"}, + {file = "librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a"}, + {file = "librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79"}, + {file = "librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0"}, + {file = "librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f"}, + {file = "librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c"}, + {file = "librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc"}, + {file = "librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c"}, + {file = "librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3"}, + {file = "librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78"}, + {file = "librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023"}, + {file = "librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730"}, + {file = "librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3"}, + {file = "librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1"}, + {file = "librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994"}, + {file = "librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a"}, + {file = "librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4"}, + {file = "librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61"}, + {file = "librt-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac"}, + {file = "librt-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed"}, + {file = "librt-0.8.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd"}, + {file = "librt-0.8.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851"}, + {file = "librt-0.8.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128"}, + {file = "librt-0.8.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed"}, + {file = "librt-0.8.1-cp39-cp39-win32.whl", hash = "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc"}, + {file = "librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7"}, + {file = "librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73"}, ] [[package]] name = "markdown" -version = "3.10.1" +version = "3.10.2" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3"}, - {file = "markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a"}, + {file = "markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36"}, + {file = "markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950"}, ] [package.extras] @@ -923,14 +942,14 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform [[package]] name = "mkdocs-autorefs" -version = "1.4.3" +version = "1.4.4" description = "Automatically link across pages in MkDocs." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9"}, - {file = "mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75"}, + {file = "mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089"}, + {file = "mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197"}, ] [package.dependencies] @@ -987,14 +1006,14 @@ mkdocs = ">=1.4.1" [[package]] name = "mkdocs-material" -version = "9.7.1" +version = "9.7.4" description = "Documentation that simply works" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c"}, - {file = "mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8"}, + {file = "mkdocs_material-9.7.4-py3-none-any.whl", hash = "sha256:6549ad95e4d130ed5099759dfa76ea34c593eefdb9c18c97273605518e99cfbf"}, + {file = "mkdocs_material-9.7.4.tar.gz", hash = "sha256:711b0ee63aca9a8c7124d4c73e83a25aa996e27e814767c3a3967df1b9e56f32"}, ] [package.dependencies] @@ -1011,9 +1030,9 @@ pymdown-extensions = ">=10.2" requests = ">=2.30" [package.extras] -git = ["mkdocs-git-committers-plugin-2 (>=1.1,<3)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] -imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<12.0)"] -recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] +git = ["mkdocs-git-committers-plugin-2 (>=1.1)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4)"] +imaging = ["cairosvg (>=2.6)", "pillow (>=10.2)"] +recommended = ["mkdocs-minify-plugin (>=0.7)", "mkdocs-redirects (>=1.2)", "mkdocs-rss-plugin (>=1.6)"] [[package]] name = "mkdocs-material-extensions" @@ -1044,14 +1063,14 @@ mkdocs = ">=1.2" [[package]] name = "mkdocstrings" -version = "1.0.2" +version = "1.0.3" description = "Automatic documentation from sources, for MkDocs." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "mkdocstrings-1.0.2-py3-none-any.whl", hash = "sha256:41897815a8026c3634fe5d51472c3a569f92ded0ad8c7a640550873eea3b6817"}, - {file = "mkdocstrings-1.0.2.tar.gz", hash = "sha256:48edd0ccbcb9e30a3121684e165261a9d6af4d63385fc4f39a54a49ac3b32ea8"}, + {file = "mkdocstrings-1.0.3-py3-none-any.whl", hash = "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046"}, + {file = "mkdocstrings-1.0.3.tar.gz", hash = "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434"}, ] [package.dependencies] @@ -1070,18 +1089,18 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" -version = "2.0.1" +version = "2.0.3" description = "A Python handler for mkdocstrings." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90"}, - {file = "mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732"}, + {file = "mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12"}, + {file = "mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8"}, ] [package.dependencies] -griffe = ">=1.13" +griffelib = ">=2.0" mkdocs-autorefs = ">=1.4" mkdocstrings = ">=0.30" typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} @@ -1190,14 +1209,14 @@ lint = ["black"] [[package]] name = "pathspec" -version = "1.0.3" +version = "1.0.4" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c"}, - {file = "pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d"}, + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, ] [package.extras] @@ -1208,21 +1227,16 @@ tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] [[package]] name = "platformdirs" -version = "4.5.1" +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.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, - {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, + {file = "platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"}, + {file = "platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934"}, ] -[package.extras] -docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] -type = ["mypy (>=1.18.2)"] - [[package]] name = "pluggy" version = "1.6.0" @@ -1436,14 +1450,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "4.0.4" +version = "4.0.5" description = "python code static checker" optional = false python-versions = ">=3.10.0" groups = ["dev"] files = [ - {file = "pylint-4.0.4-py3-none-any.whl", hash = "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0"}, - {file = "pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2"}, + {file = "pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2"}, + {file = "pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c"}, ] [package.dependencies] @@ -1454,7 +1468,7 @@ dill = [ {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, {version = ">=0.3.6", markers = "python_version == \"3.11\""}, ] -isort = ">=5,<5.13 || >5.13,<8" +isort = ">=5,<5.13 || >5.13,<9" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2" tomli = {version = ">=1.1", markers = "python_version < \"3.11\""} @@ -1466,14 +1480,14 @@ testutils = ["gitpython (>3)"] [[package]] name = "pymdown-extensions" -version = "10.20.1" +version = "10.21" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pymdown_extensions-10.20.1-py3-none-any.whl", hash = "sha256:24af7feacbca56504b313b7b418c4f5e1317bb5fea60f03d57be7fcc40912aa0"}, - {file = "pymdown_extensions-10.20.1.tar.gz", hash = "sha256:e7e39c865727338d434b55f1dd8da51febcffcaebd6e1a0b9c836243f660740a"}, + {file = "pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f"}, + {file = "pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5"}, ] [package.dependencies] @@ -2216,4 +2230,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "58fe3f1249d51a986bbce23d9aabc53a97a5a673250c30bd66f292268806d7cc" +content-hash = "7508546dbc3baa8a9798a17b2e318138de6d1df4e132ea7f3791cee0588a5b9f" diff --git a/pyproject.toml b/pyproject.toml index 02112d1..4c7232c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "uvicorn[standard] (>=0.40.0,<0.41.0)", "pydantic (>=2.12.5,<3.0.0)", "pydantic-settings (>=2.12.0,<3.0.0)", - "hier-config @ git+https://github.com/netdevops/hier_config.git" + "hier-config (>=4.0.0b1,<5.0.0)" ] From 1cf7d11419cbdf5a710f92ec2967ff9da9197d25 Mon Sep 17 00:00:00 2001 From: jtdub Date: Thu, 6 Aug 2026 10:17:10 -0500 Subject: [PATCH 2/6] Port hier_config lint, typing, and test tooling Adopt the development standards from the hier_config repository so both projects share one toolchain: ruff (ALL rules, preview, line length 88), mypy strict with the pydantic plugin, pyright strict, pylint with extension plugins and pylint_pydantic, yamllint, and flynt, all driven by the parallel runner in scripts/build.py. Coverage is enforced at 95% via 'build.py pytest --coverage' (--cov=hier_config_api). Dev dependencies gain pyright, pylint-pydantic, pytest-xdist, yamllint, flynt, and typer; pytest-asyncio is dropped because the test suite has no async tests. The mypy ignore_missing_imports overrides for hier_config are removed since hier_config 4.x ships py.typed. CI now mirrors hier_config: a Python 3.10-3.14 matrix running 'build.py lint' and 'build.py pytest --coverage' on push/PR to develop and next, and the docs job builds with 'mkdocs build --strict' and deploys to GitHub Pages on push to develop (the default branch; it previously only triggered on the nonexistent main branch). This repo has no CHANGELOG.md, so the change is recorded here. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 109 ++++----------- .github/workflows/docs.yml | 12 +- .yamllint.yml | 9 ++ poetry.lock | 266 +++++++++++++++++++++++++++++++------ pyproject.toml | 160 +++++++++++++--------- scripts/build.py | 258 +++++++++++++++++++++++++++++++++++ 6 files changed, 623 insertions(+), 191 deletions(-) create mode 100644 .yamllint.yml create mode 100755 scripts/build.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c6e217..493aff8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,96 +1,35 @@ -name: CI +name: hier-config-api build and test on: push: - branches: [ main, develop ] + branches: [develop, next] pull_request: - branches: [ main, develop ] + branches: [develop, next] jobs: - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - version: 2.3.1 - virtualenvs-create: true - virtualenvs-in-project: true - - - name: Load cached venv - id: cached-poetry-dependencies - uses: actions/cache@v4 - with: - path: .venv - key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} - - - name: Install dependencies - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction --no-root - - - name: Install project - run: poetry install --no-interaction - - - name: Run Ruff linter - run: poetry run ruff check . - - - name: Run Ruff formatter check - run: poetry run ruff format --check . - - - name: Run mypy - run: poetry run mypy hier_config_api - - test: - name: Test (Python ${{ matrix.python-version }}) + build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12"] - + python-version: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - version: 2.3.1 - virtualenvs-create: true - virtualenvs-in-project: true - - - name: Load cached venv - id: cached-poetry-dependencies - uses: actions/cache@v4 - with: - path: .venv - key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} - - - name: Install dependencies - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction --no-root - - - name: Install project - run: poetry install --no-interaction - - - name: Run tests - run: poetry run pytest --cov=hier_config_api --cov-report=xml --cov-report=term-missing - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - if: matrix.python-version == '3.11' - with: - file: ./coverage.xml - fail_ci_if_error: false + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - name: Install poetry + uses: snok/install-poetry@v1 + with: + version: 2.3.1 + - name: Run tests + run: | + poetry install --no-interaction + poetry run python scripts/build.py lint + poetry run python scripts/build.py pytest --coverage diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cad7ab0..58e516a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,10 +3,12 @@ name: Deploy Documentation on: push: branches: - - main + - develop + - next pull_request: branches: - - main + - develop + - next permissions: contents: write @@ -20,7 +22,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" - name: Install Poetry uses: snok/install-poetry@v1 @@ -44,10 +46,10 @@ jobs: run: poetry install --no-interaction - name: Build documentation - run: poetry run mkdocs build + run: poetry run mkdocs build --strict - name: Deploy to GitHub Pages - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..3c99231 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,9 @@ +extends: default + +rules: + indentation: + spaces: 2 + indent-sequences: consistent + document-start: + present: false + line-length: false diff --git a/poetry.lock b/poetry.lock index 3fc5d57..1557d5d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,7 +6,7 @@ version = "0.0.4" description = "Document parameters, class attributes, return types, and variables inline, with Annotated." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"}, {file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"}, @@ -18,7 +18,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -74,19 +74,6 @@ files = [ [package.extras] dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." -optional = false -python-versions = "<3.11,>=3.8" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, - {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, -] - [[package]] name = "backrefs" version = "6.2" @@ -427,6 +414,21 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "execnet" +version = "2.1.2" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"}, + {file = "execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + [[package]] name = "fastapi" version = "0.128.0" @@ -450,6 +452,24 @@ all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (> standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +[[package]] +name = "flynt" +version = "1.0.6" +description = "CLI tool to convert a python project's %-formatted strings to f-strings." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "flynt-1.0.6-py3-none-any.whl", hash = "sha256:4e837c9597036b634a347855a89acf1483c4f8b73daa82c49372b10b6e1d1778"}, + {file = "flynt-1.0.6.tar.gz", hash = "sha256:471b7ff00756678e2912d4261dcbcd8fc1395129b66bf6977f88a3b3ad220c90"}, +] + +[package.dependencies] +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["build", "pre-commit", "pytest", "pytest-cov", "ruff", "twine"] + [[package]] name = "ghp-import" version = "2.1.0" @@ -786,6 +806,30 @@ files = [ docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python] (>=0.28.3)"] testing = ["coverage", "pyyaml"] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, + {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "pytest-timeout", "requests"] + [[package]] name = "markupsafe" version = "3.0.3" @@ -897,6 +941,18 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "mergedeep" version = "1.3.4" @@ -1179,6 +1235,18 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, + {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, +] + [[package]] name = "packaging" version = "26.0" @@ -1259,7 +1327,7 @@ version = "2.12.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, @@ -1281,7 +1349,7 @@ version = "2.41.5" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, @@ -1478,6 +1546,37 @@ tomlkit = ">=0.10.1" spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] +[[package]] +name = "pylint-plugin-utils" +version = "0.9.0" +description = "Utilities and helpers for writing Pylint plugins" +optional = false +python-versions = "<4.0,>=3.9" +groups = ["dev"] +files = [ + {file = "pylint_plugin_utils-0.9.0-py3-none-any.whl", hash = "sha256:16e9b84e5326ba893a319a0323fcc8b4bcc9c71fc654fcabba0605596c673818"}, + {file = "pylint_plugin_utils-0.9.0.tar.gz", hash = "sha256:5468d763878a18d5cc4db46eaffdda14313b043c962a263a7d78151b90132055"}, +] + +[package.dependencies] +pylint = ">=1.7" + +[[package]] +name = "pylint-pydantic" +version = "0.4.1" +description = "A Pylint plugin to help Pylint understand the Pydantic" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pylint_pydantic-0.4.1-py3-none-any.whl", hash = "sha256:d1b937abe5c346d38de69ee1ada80c93d38ee2356addbabb687e2eb44036ac93"}, +] + +[package.dependencies] +pydantic = "<3.0" +pylint = ">2.0,<5.0" +pylint-plugin-utils = "*" + [[package]] name = "pymdown-extensions" version = "10.21" @@ -1497,6 +1596,27 @@ pyyaml = "*" [package.extras] extra = ["pygments (>=2.19.1)"] +[[package]] +name = "pyright" +version = "1.1.411" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9"}, + {file = "pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" +typing-extensions = ">=4.1" + +[package.extras] +all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] +nodejs = ["nodejs-wheel-binaries"] + [[package]] name = "pytest" version = "9.0.2" @@ -1522,45 +1642,45 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] -name = "pytest-asyncio" -version = "1.3.0" -description = "Pytest support for asyncio" +name = "pytest-cov" +version = "7.0.0" +description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.10" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"}, - {file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"}, + {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] -backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} -pytest = ">=8.2,<10" -typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" [package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +testing = ["process-tests", "pytest-xdist", "virtualenv"] [[package]] -name = "pytest-cov" -version = "7.0.0" -description = "Pytest plugin for measuring coverage." +name = "pytest-xdist" +version = "3.8.0" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" 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"}, + {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, + {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, ] [package.dependencies] -coverage = {version = ">=7.10.6", extras = ["toml"]} -pluggy = ">=1.2" -pytest = ">=7" +execnet = ">=2.1" +pytest = ">=7.0.0" [package.extras] -testing = ["process-tests", "pytest-xdist", "virtualenv"] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] [[package]] name = "python-dateutil" @@ -1712,6 +1832,25 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "rich" +version = "15.0.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, + {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "ruff" version = "0.14.14" @@ -1741,6 +1880,18 @@ files = [ {file = "ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b"}, ] +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + [[package]] name = "six" version = "1.17.0" @@ -1842,6 +1993,24 @@ files = [ {file = "tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"}, ] +[[package]] +name = "typer" +version = "0.27.1" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56"}, + {file = "typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df"}, +] + +[package.dependencies] +annotated-doc = ">=0.0.2" +colorama = {version = "*", markers = "platform_system == \"Windows\""} +rich = ">=13.8.0" +shellingham = ">=1.3.0" + [[package]] name = "types-pyyaml" version = "6.0.12.20250915" @@ -1872,7 +2041,7 @@ version = "0.4.2" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, @@ -2227,7 +2396,26 @@ files = [ {file = "websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5"}, ] +[[package]] +name = "yamllint" +version = "1.38.0" +description = "A linter for YAML files." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "yamllint-1.38.0-py3-none-any.whl", hash = "sha256:fc394a5b3be980a4062607b8fdddc0843f4fa394152b6da21722f5d59013c220"}, + {file = "yamllint-1.38.0.tar.gz", hash = "sha256:09e5f29531daab93366bb061e76019d5e91691ef0a40328f04c927387d1d364d"}, +] + +[package.dependencies] +pathspec = ">=1.0.0" +pyyaml = "*" + +[package.extras] +dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "ruff", "sphinx"] + [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "7508546dbc3baa8a9798a17b2e318138de6d1df4e132ea7f3791cee0588a5b9f" +content-hash = "7d356e4592f8bad9bf49a75f074b5bd98a356537e0aa84bbc292e927bd220ada" diff --git a/pyproject.toml b/pyproject.toml index 4c7232c..bb86577 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,91 +22,127 @@ build-backend = "poetry.core.masonry.api" [dependency-groups] dev = [ - "pytest (>=9.0.2,<10.0.0)", - "pytest-asyncio (>=1.3.0,<2.0.0)", - "pytest-cov (>=7.0.0,<8.0.0)", + "flynt (>=1.0.0,<2.0.0)", "httpx (>=0.28.1,<0.29.0)", - "ruff (>=0.14.14,<0.15.0)", - "mypy (>=1.19.1,<2.0.0)", - "pylint (>=4.0.4,<5.0.0)", - "types-pyyaml (>=6.0.12.20250915,<7.0.0.0)", "mkdocs (>=1.6.1,<2.0.0)", "mkdocs-material (>=9.7.1,<10.0.0)", "mkdocstrings[python] (>=1.0.2,<2.0.0)", "mkdocs-gen-files (>=0.6.0,<0.7.0)", "mkdocs-literate-nav (>=0.6.2,<0.7.0)", - "mkdocs-section-index (>=0.3.10,<0.4.0)" + "mkdocs-section-index (>=0.3.10,<0.4.0)", + "mypy (>=1.19.1,<2.0.0)", + "pylint (>=4.0.4,<5.0.0)", + "pylint-pydantic (>=0.4.1,<0.5.0)", + "pyright (>=1.1.390,<2.0.0)", + "pytest (>=9.0.2,<10.0.0)", + "pytest-cov (>=7.0.0,<8.0.0)", + "pytest-xdist (>=3.6.1,<4.0.0)", + "ruff (>=0.14.14,<0.15.0)", + "typer (>=0.15.1,<1.0.0)", + "types-pyyaml (>=6.0.12.20250915,<7.0.0.0)", + "yamllint (>=1.35.1,<2.0.0)" ] -[tool.ruff] -line-length = 100 -target-version = "py310" -[tool.ruff.lint] -select = [ - "E", # pycodestyle errors - "W", # pycodestyle warnings - "F", # pyflakes - "I", # isort - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "UP", # pyupgrade +[tool.pyright] +typeCheckingMode = "strict" + + +[tool.pylint.message_control] +load-plugins = [ + "pylint.extensions.bad_builtin", + "pylint.extensions.broad_try_clause", + "pylint.extensions.check_elif", + "pylint.extensions.comparison_placement", + "pylint.extensions.confusing_elif", + "pylint.extensions.consider_ternary_expression", + "pylint.extensions.dict_init_mutate", + "pylint.extensions.dunder", + "pylint.extensions.eq_without_hash", + "pylint.extensions.for_any_all", + "pylint.extensions.overlapping_exceptions", + "pylint.extensions.redefined_loop_name", + "pylint.extensions.set_membership", + "pylint.extensions.typing", + "pylint_pydantic", ] -ignore = [ - "E501", # line too long, handled by formatter - "B008", # do not perform function calls in argument defaults - "C901", # too complex +disable = [ + "consider-alternative-union-syntax", + "duplicate-code", # Enable this at some point in the future + "fixme", # Covered by ruff FIX002 + "import-outside-toplevel", # Covered by ruff PLC0415 + "line-too-long", # Whatever ruff format determines the line should look like is fine + "missing-function-docstring", + "missing-module-docstring", + "protected-access", # Covered by ruff SLF001 + "redefined-loop-name", # Covered by ruff PLW2901 + "too-many-arguments", # Covered by ruff PLR0913 + "too-many-return-statements", # Covered by ruff PLR0911 + "too-many-locals", # Covered by ruff PLR0914 + "too-many-public-methods", # Covered by ruff PLR0904 ] -[tool.ruff.lint.per-file-ignores] -"__init__.py" = ["F401"] [tool.mypy] -python_version = "3.10" strict = true -warn_return_any = true -warn_unused_configs = true -disallow_untyped_defs = true -disallow_any_generics = false -warn_redundant_casts = true -warn_unused_ignores = true -check_untyped_defs = true - -[[tool.mypy.overrides]] -module = "hier_config.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "hier_config" -ignore_missing_imports = true - -[tool.pylint.main] -max-line-length = 100 -disable = [ - "C0111", # missing-docstring - "C0103", # invalid-name - "R0903", # too-few-public-methods - "R0913", # too-many-arguments +pretty = true +show_column_numbers = true +show_error_codes = true +show_error_context = true +warn_unreachable = true +plugins = [ + "pydantic.mypy" ] + +[tool.ruff] +line-length = 88 +target-version = "py310" + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +select = ["ALL"] +preview = true +ignore = [ + # The below checks we might never enable + "B019", # using lru_cache can cause memory leaks + "COM812", # DO NOT REMOVE: conflicts with ruff formater + "CPY001", # Missing copyright notice at top of file + "D213", # DO NOT REMOVE: multi-line-summary-second-line is incompatible with D212(multi-line-summary-first-line) + "E501", # line too long + "EXE002", # DO NOT REMOVE: The file is executable but no shebang is present + "ISC001", # DO NOT REMOVE: The following rule may cause conflicts when used with the formatter + "PLR2004", # Magic value used in comparison + # The below checks should eventually be enabled + "D100", # Missing docstring in public module + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D107", # Missing docstring in `__init__` + "D200", # One-line docstring should fit on one line + "D203", # 1 blank line required before class docstring - Incompatible with D211 + "D205", # 1 blank line required between summary line and description + "D401", # First line of docstring should be in imperative mood + "DOC201", # `return` is not documented in docstring + "DOC402", # `yield` is not documented in docstring + "DOC501", # Raised exception missing from docstring +] + +[tool.ruff.lint.flake8-pytest-style] +parametrize-values-type = "tuple" + +[tool.ruff.lint.per-file-ignores] +"**/tests/*" = ["PLC2701", "S101"] + [tool.pytest.ini_options] -asyncio_mode = "auto" testpaths = ["tests"] -python_files = ["test_*.py"] -python_classes = ["Test*"] -python_functions = ["test_*"] -addopts = "--cov=hier_config_api --cov-report=term-missing --cov-report=html" - -[tool.coverage.run] -source = ["hier_config_api"] -omit = ["*/tests/*", "*/__init__.py"] [tool.coverage.report] exclude_lines = [ "pragma: no cover", - "def __repr__", - "raise AssertionError", - "raise NotImplementedError", "if __name__ == .__main__.:", "if TYPE_CHECKING:", ] diff --git a/scripts/build.py b/scripts/build.py new file mode 100755 index 0000000..2a0f425 --- /dev/null +++ b/scripts/build.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import os +import subprocess # noqa: S404 +import sys +from concurrent.futures import ThreadPoolExecutor, as_completed +from functools import cache +from pathlib import Path +from typing import TYPE_CHECKING, NoReturn + +from typer import Typer + +if TYPE_CHECKING: + from collections.abc import Iterable + +app = Typer() + + +@app.callback() +def callback() -> None: + """Build tools.""" + + +@app.command() +def lint(*, fix: bool = False) -> None: + """Run all linters and type checkers in order of importance - returns the first non-zero exit code or zero.""" + _run_commands_threaded( + ( + _ruff_format_command(fix=fix), + _ruff_check_command(fix=fix, unsafe_fixes=False, statistics=False), + _mypy_command(), + _pyright_command(), + _pylint_command(), + _yamllint_command(), + _flynt_command(fix=fix), + ), + ) + + +@app.command() +def lint_and_test(*, fix: bool = False) -> None: + """Run all code fixs in order of importance - returns the first non-zero exit code or zero.""" + _run_commands_threaded( + ( + _ruff_format_command(fix=fix), + _ruff_check_command(fix=fix, unsafe_fixes=False, statistics=False), + _mypy_command(), + _pyright_command(), + _pylint_command(), + _pytest_command(), + _yamllint_command(), + _flynt_command(fix=fix), + ), + ) + + +@app.command() +def ruff_format(*, fix: bool = False) -> None: + """Run Ruff linter.""" + _run(_ruff_format_command(fix=fix)) + + +def _ruff_format_command(*, fix: bool) -> str: + return f"ruff format {'' if fix else '--check '}{_python_base_paths_str()}" + + +@app.command() +def ruff_check( + *, + fix: bool = False, + unsafe_fixes: bool = False, + statistics: bool = False, +) -> None: + """Run Ruff linter.""" + _run(_ruff_check_command(fix=fix, unsafe_fixes=unsafe_fixes, statistics=statistics)) + + +def _ruff_check_command(*, fix: bool, unsafe_fixes: bool, statistics: bool) -> str: + return f"ruff check --output-format=concise {'--fix ' if fix else ''}{'--unsafe-fixes ' if unsafe_fixes else ''}{'--statistics ' if statistics else ''}{_python_base_paths_str()}" + + +@app.command() +def pytest( + *, + coverage: bool = True, + threaded: bool = False, +) -> None: + """Run pytest unittests.""" + _run( + _pytest_command( + coverage=coverage, + threaded=threaded, + ), + environment={"COVERAGE_CORE": "sysmon"}, + ) + + +def _pytest_command( + *, + coverage: bool = True, + threaded: bool = False, +) -> str: + command = "pytest" + if coverage: + command += ( + " --cov=hier_config_api --cov-fail-under=95 --cov-report=term-missing" + ) + if threaded: + command += " -n auto" + return command + + +@app.command() +def yamllint() -> None: + """Run yamllint to check YAML syntax.""" + _run(_yamllint_command()) + + +def _yamllint_command() -> str: + return f"yamllint {_repo_path().relative_to(Path.cwd())}" + + +@app.command() +def pylint() -> None: + """Run pylint linter.""" + _run(_pylint_command()) + + +def _pylint_command() -> str: + return f"pylint {_python_base_paths_str()}" + + +@app.command() +def mypy() -> None: + """Run mypy type checker.""" + _run(_mypy_command()) + + +def _mypy_command() -> str: + return f"mypy {_python_base_paths_str()}" + + +@app.command() +def pyright() -> None: + """Run pyright type checker.""" + _run(_pyright_command()) + + +def _pyright_command() -> str: + return f"pyright {_python_base_paths_str()}" + + +@app.command() +def flynt(*, fix: bool = False) -> None: + """Run flynt to enforce the use of f-strings.""" + _run(_flynt_command(fix=fix)) + + +def _flynt_command(*, fix: bool) -> str: + if fix: + return f"flynt -tc {_python_base_paths_str()}" + return f"flynt -d -tc -f {_python_base_paths_str()}" + + +@cache +def _python_base_paths_str() -> str: + return " ".join(str(p) for p in _python_base_paths()) + + +def _python_base_paths() -> Iterable[Path]: + yield from (f.relative_to(Path.cwd()) for f in _project_base_paths("*.py")) + + +def _project_base_paths(glob: str) -> Iterable[Path]: + yield from _project_paths(glob) + yield from _project_base_files(glob) + + +def _project_base_files(glob: str) -> Iterable[Path]: + yield from _repo_path().glob(glob) + + +def _project_paths(glob: str) -> Iterable[Path]: + for base_dir in ("hier_config_api", "tests", "scripts"): + base_path = _repo_path().joinpath(base_dir) + if not base_path.exists(): + message = f"{base_path=} does not exist" + raise FileNotFoundError(message) + + if next(base_path.glob(glob), None): + yield base_path + + +def _run_commands_threaded(commands: tuple[str, ...]) -> NoReturn: + return_codes: dict[str, int] = {} + with ThreadPoolExecutor(max_workers=len(commands)) as executor: + for future in as_completed( + executor.submit( + _run_for_thread, + command, + ) + for command in commands + ): + command, return_code, output = future.result() + if return_code: + print(output) # noqa: T201 + return_codes[command] = return_code + + error_found = False + for command, return_code in return_codes.items(): + if return_code != 0: + print(f"{command.split()[0]} -> {return_code}") # noqa: T201 + error_found = True + if error_found: + sys.exit(1) + print("No issues found") # noqa: T201 + sys.exit() + + +def _run( + command: str, + *, + check: bool = True, + environment: dict[str, str] | None = None, +) -> int: + print(f"\n======== {command} ========\n") # noqa: T201 + my_env = os.environ.copy() + if environment: + my_env.update(environment) + result = subprocess.run(command.split(), check=False, env=my_env) # noqa: S603 + if check: + sys.exit(result.returncode) + return result.returncode + + +def _run_for_thread(command: str) -> tuple[str, int, str]: + print(f"Running: {command}") # noqa: T201 + result = subprocess.run( # noqa: S603 + command.split(), + check=False, + capture_output=True, + ) + output = f"\n======== {command} ========\n{result.stdout.decode()}\n{result.stderr.decode()}" + return command, result.returncode, output + + +@cache +def _repo_path() -> Path: + return Path(__file__).parents[1] + + +def main() -> None: + app() + + +if __name__ == "__main__": + main() From 37ecd75728261e4edec44f5738a891b0a36d82ef Mon Sep 17 00:00:00 2001 From: jtdub Date: Thu, 6 Aug 2026 10:17:25 -0500 Subject: [PATCH 3/6] Conform code to ported lint and typing standards Fix all violations surfaced by the hier_config lint stack (64 ruff errors, 22 pyright errors, and 12 pylint messages beyond ruff's fixes): - Routers: drop redundant response_model arguments (FAST001), move to Annotated[..., Query()] dependencies (FAST002/B008), rename the export 'format' parameter to export_format with a query alias to avoid shadowing the builtin (A002), and restructure try blocks so each guards a single statement (pylint too-many-try-statements), extracting small orchestration helpers where several calls must be error-wrapped together. - Services: replace typing.Any parameters with HConfig | HConfigChild (ANN401), pass MatchRule/GenerateRemediationRequest models instead of long argument lists (PLR0913/PLR0917), split boolean-heavy matching into a helper (PLR0916), mark PLATFORMS as ClassVar (RUF012), restructure batch processing so the per-device try/except lives outside the loop body (PERF203), and assign exception messages to a variable before raising (EM102/TRY003). - Models: use keyword 'default=' in Field() calls so pyright's strict dataclass_transform handling recognizes optional fields, and use a named module-level default factory for the ordering field. - Annotate empty-list literals so pyright strict can infer element types. Justified suppressions, each with an explanatory comment: - BLE001/broad-exception-caught in validate_config and batch device processing, where any failure must be reported as data, not raised. - S104 on the dev/container uvicorn entry point binding 0.0.0.0. - T201/S404/S603 in scripts/build.py (a console tool that prints and spawns the lint/test processes), replacing the upstream file's invalid 'ruff:ignore' comments with real noqa directives. Co-Authored-By: Claude Fable 5 --- hier_config_api/main.py | 4 +- hier_config_api/models/config.py | 40 +++++-- hier_config_api/models/platform.py | 29 +++-- hier_config_api/models/remediation.py | 44 +++++-- hier_config_api/models/report.py | 14 ++- hier_config_api/routers/batch.py | 59 ++++++---- hier_config_api/routers/configs.py | 63 ++++++---- hier_config_api/routers/platforms.py | 22 ++-- hier_config_api/routers/remediation.py | 108 ++++++++++-------- hier_config_api/routers/reports.py | 53 ++++++--- hier_config_api/services/config_service.py | 75 ++++++------ hier_config_api/services/platform_service.py | 105 +++++++++-------- .../services/remediation_service.py | 46 +++----- hier_config_api/services/report_service.py | 27 +++-- tests/test_configs.py | 4 +- tests/test_platforms.py | 21 +++- tests/test_remediation.py | 19 ++- 17 files changed, 439 insertions(+), 294 deletions(-) diff --git a/hier_config_api/main.py b/hier_config_api/main.py index 453678d..798fc07 100644 --- a/hier_config_api/main.py +++ b/hier_config_api/main.py @@ -50,4 +50,6 @@ async def health() -> dict[str, str]: if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) + # Dev/container entry point: binding all interfaces is intentional so the + # API is reachable from outside the container. + uvicorn.run(app, host="0.0.0.0", port=8000) # noqa: S104 diff --git a/hier_config_api/models/config.py b/hier_config_api/models/config.py index 8cecabc..5618ea3 100644 --- a/hier_config_api/models/config.py +++ b/hier_config_api/models/config.py @@ -8,7 +8,9 @@ class ParseConfigRequest(BaseModel): """Request model for parsing configuration.""" - platform: str = Field(..., description="Platform type (e.g., cisco_ios, juniper_junos)") + platform: str = Field( + ..., description="Platform type (e.g., cisco_ios, juniper_junos)" + ) config_text: str = Field(..., description="Raw configuration text to parse") @@ -16,13 +18,17 @@ class ParseConfigResponse(BaseModel): """Response model for parsed configuration.""" platform: str = Field(..., description="Platform type") - structured_config: dict[str, Any] = Field(..., description="Structured configuration tree") + structured_config: dict[str, Any] = Field( + ..., description="Structured configuration tree" + ) class CompareConfigRequest(BaseModel): """Request model for comparing configurations.""" - platform: str = Field(..., description="Platform type (e.g., cisco_ios, juniper_junos)") + platform: str = Field( + ..., description="Platform type (e.g., cisco_ios, juniper_junos)" + ) running_config: str = Field(..., description="Current running configuration") intended_config: str = Field(..., description="Desired configuration state") @@ -38,9 +44,13 @@ class CompareConfigResponse(BaseModel): class PredictConfigRequest(BaseModel): """Request model for predicting future configuration state.""" - platform: str = Field(..., description="Platform type (e.g., cisco_ios, juniper_junos)") + platform: str = Field( + ..., description="Platform type (e.g., cisco_ios, juniper_junos)" + ) current_config: str = Field(..., description="Current configuration state") - commands_to_apply: str = Field(..., description="Commands to apply to current config") + commands_to_apply: str = Field( + ..., description="Commands to apply to current config" + ) class PredictConfigResponse(BaseModel): @@ -55,7 +65,9 @@ class PredictConfigResponse(BaseModel): class MergeConfigRequest(BaseModel): """Request model for merging multiple configurations.""" - platform: str = Field(..., description="Platform type (e.g., cisco_ios, juniper_junos)") + platform: str = Field( + ..., description="Platform type (e.g., cisco_ios, juniper_junos)" + ) configs: list[str] = Field(..., description="List of configuration texts to merge") @@ -69,18 +81,22 @@ class MergeConfigResponse(BaseModel): class MatchRule(BaseModel): """Rule for matching configuration lines.""" - equals: str | None = Field(None, description="Exact match string") - contains: str | None = Field(None, description="Substring to match") - startswith: str | None = Field(None, description="Prefix to match") - regex: str | None = Field(None, description="Regular expression pattern") + equals: str | None = Field(default=None, description="Exact match string") + contains: str | None = Field(default=None, description="Substring to match") + startswith: str | None = Field(default=None, description="Prefix to match") + regex: str | None = Field(default=None, description="Regular expression pattern") class SearchConfigRequest(BaseModel): """Request model for searching configuration.""" - platform: str = Field(..., description="Platform type (e.g., cisco_ios, juniper_junos)") + platform: str = Field( + ..., description="Platform type (e.g., cisco_ios, juniper_junos)" + ) config_text: str = Field(..., description="Configuration text to search") - match_rules: MatchRule = Field(..., description="Rules for matching configuration sections") + match_rules: MatchRule = Field( + ..., description="Rules for matching configuration sections" + ) class SearchConfigResponse(BaseModel): diff --git a/hier_config_api/models/platform.py b/hier_config_api/models/platform.py index 3ce9836..d88ae82 100644 --- a/hier_config_api/models/platform.py +++ b/hier_config_api/models/platform.py @@ -5,13 +5,19 @@ from pydantic import BaseModel, Field +def _ordering_rules_default() -> list[dict[str, Any]]: + return [] + + class PlatformInfo(BaseModel): """Information about a supported platform.""" platform_name: str = Field(..., description="Platform identifier") display_name: str = Field(..., description="Human-readable platform name") vendor: str = Field(..., description="Vendor name") - supported: bool = Field(True, description="Whether platform is currently supported") + supported: bool = Field( + default=True, description="Whether platform is currently supported" + ) class PlatformRules(BaseModel): @@ -19,13 +25,14 @@ class PlatformRules(BaseModel): platform_name: str = Field(..., description="Platform identifier") negation_default_when: list[str] = Field( - default_factory=list, description="Default negation patterns for when conditions" + default_factory=list, + description="Default negation patterns for when conditions", ) negation_negate_with: list[str] = Field( default_factory=list, description="How to negate specific commands" ) ordering: list[dict[str, Any]] = Field( - default_factory=list, description="Command ordering rules" + default_factory=_ordering_rules_default, description="Command ordering rules" ) idempotent_commands_avoid: list[str] = Field( default_factory=list, description="Commands to avoid in idempotent configs" @@ -53,7 +60,9 @@ class ValidateConfigResponse(BaseModel): class BatchJobRequest(BaseModel): """Request model for batch job.""" - device_configs: list[dict[str, Any]] = Field(..., description="List of device configurations") + device_configs: list[dict[str, Any]] = Field( + ..., description="List of device configurations" + ) class BatchJobResponse(BaseModel): @@ -67,11 +76,15 @@ class BatchJobStatus(BaseModel): """Status of a batch job.""" job_id: str = Field(..., description="Job identifier") - status: str = Field(..., description="Job status (pending, running, completed, failed)") - progress: float = Field(0.0, description="Progress percentage (0.0 to 100.0)", ge=0.0, le=100.0) + status: str = Field( + ..., description="Job status (pending, running, completed, failed)" + ) + progress: float = Field( + default=0.0, description="Progress percentage (0.0 to 100.0)", ge=0.0, le=100.0 + ) total_devices: int = Field(..., description="Total number of devices") - completed_devices: int = Field(0, description="Number of completed devices") - failed_devices: int = Field(0, description="Number of failed devices") + completed_devices: int = Field(default=0, description="Number of completed devices") + failed_devices: int = Field(default=0, description="Number of failed devices") class BatchJobResults(BaseModel): diff --git a/hier_config_api/models/remediation.py b/hier_config_api/models/remediation.py index 56540c9..576685f 100644 --- a/hier_config_api/models/remediation.py +++ b/hier_config_api/models/remediation.py @@ -13,31 +13,47 @@ class TagRule(BaseModel): class RemediationSummary(BaseModel): """Summary of remediation changes.""" - additions: int = Field(0, description="Number of configuration additions") - deletions: int = Field(0, description="Number of configuration deletions") - modifications: int = Field(0, description="Number of configuration modifications") + additions: int = Field(default=0, description="Number of configuration additions") + deletions: int = Field(default=0, description="Number of configuration deletions") + modifications: int = Field( + default=0, description="Number of configuration modifications" + ) class GenerateRemediationRequest(BaseModel): """Request model for generating remediation.""" - platform: str = Field(..., description="Platform type (e.g., cisco_ios, juniper_junos)") + platform: str = Field( + ..., description="Platform type (e.g., cisco_ios, juniper_junos)" + ) running_config: str = Field(..., description="Current running configuration") intended_config: str = Field(..., description="Desired configuration state") - tag_rules: list[TagRule] | None = Field(None, description="Optional tag rules to apply") - include_tags: list[str] | None = Field(None, description="Only include these tags") - exclude_tags: list[str] | None = Field(None, description="Exclude these tags") + tag_rules: list[TagRule] | None = Field( + default=None, description="Optional tag rules to apply" + ) + include_tags: list[str] | None = Field( + default=None, description="Only include these tags" + ) + exclude_tags: list[str] | None = Field( + default=None, description="Exclude these tags" + ) class GenerateRemediationResponse(BaseModel): """Response model for generated remediation.""" - remediation_id: str = Field(..., description="Unique identifier for this remediation") + remediation_id: str = Field( + ..., description="Unique identifier for this remediation" + ) platform: str = Field(..., description="Platform type") - remediation_config: str = Field(..., description="Commands to achieve desired state") + remediation_config: str = Field( + ..., description="Commands to achieve desired state" + ) rollback_config: str = Field(..., description="Commands to rollback changes") summary: RemediationSummary = Field(..., description="Summary of changes") - tags: dict[str, list[str]] = Field(default_factory=dict, description="Tags applied to commands") + tags: dict[str, list[str]] = Field( + default_factory=dict, description="Tags applied to commands" + ) class ApplyTagsRequest(BaseModel): @@ -57,8 +73,12 @@ class ApplyTagsResponse(BaseModel): class FilterRemediationRequest(BaseModel): """Request model for filtering remediation by tags.""" - include_tags: list[str] | None = Field(None, description="Only include these tags") - exclude_tags: list[str] | None = Field(None, description="Exclude these tags") + include_tags: list[str] | None = Field( + default=None, description="Only include these tags" + ) + exclude_tags: list[str] | None = Field( + default=None, description="Exclude these tags" + ) class FilterRemediationResponse(BaseModel): diff --git a/hier_config_api/models/report.py b/hier_config_api/models/report.py index c8c31a8..df6083b 100644 --- a/hier_config_api/models/report.py +++ b/hier_config_api/models/report.py @@ -7,7 +7,9 @@ class DeviceRemediation(BaseModel): """Model for a single device's remediation data.""" device_id: str = Field(..., description="Unique device identifier") - platform: str = Field(..., description="Platform type (e.g., cisco_ios, juniper_junos)") + platform: str = Field( + ..., description="Platform type (e.g., cisco_ios, juniper_junos)" + ) running_config: str = Field(..., description="Current running configuration") intended_config: str = Field(..., description="Desired configuration state") @@ -32,7 +34,9 @@ class ReportSummary(BaseModel): total_devices: int = Field(..., description="Total number of devices") devices_with_changes: int = Field(..., description="Number of devices with changes") - total_changes: int = Field(..., description="Total number of changes across all devices") + total_changes: int = Field( + ..., description="Total number of changes across all devices" + ) changes_by_tag: dict[str, int] = Field( default_factory=dict, description="Count of changes by tag" ) @@ -44,7 +48,9 @@ class ChangeDetail(BaseModel): change_text: str = Field(..., description="The configuration change text") device_count: int = Field(..., description="Number of devices with this change") device_ids: list[str] = Field(..., description="List of affected device IDs") - tags: list[str] = Field(default_factory=list, description="Tags associated with this change") + tags: list[str] = Field( + default_factory=list, description="Tags associated with this change" + ) class GetReportChangesResponse(BaseModel): @@ -58,4 +64,4 @@ class GetReportChangesResponse(BaseModel): class ExportFormat(BaseModel): """Supported export formats.""" - format: str = Field("json", description="Export format (json, csv, yaml)") + format: str = Field(default="json", description="Export format (json, csv, yaml)") diff --git a/hier_config_api/routers/batch.py b/hier_config_api/routers/batch.py index e321e25..2fb097e 100644 --- a/hier_config_api/routers/batch.py +++ b/hier_config_api/routers/batch.py @@ -1,5 +1,7 @@ """API router for batch operations.""" +from typing import Any + from fastapi import APIRouter, HTTPException from hier_config_api.models.platform import ( @@ -14,23 +16,32 @@ router = APIRouter(prefix="/api/v1/batch", tags=["batch"]) -@router.post("/remediation", response_model=BatchJobResponse) +def _create_and_run_job( + device_configs: list[dict[str, Any]], +) -> tuple[str, dict[str, Any]]: + """Create a batch job, process it, and persist the result.""" + job_data = PlatformService.create_batch_job(device_configs) + job_id = storage.store_job(job_data) + + # Process the job (in a real implementation, this would be async/background) + PlatformService.process_batch_job(job_data) + storage.update_job(job_id, job_data) + return job_id, job_data + + +@router.post("/remediation") async def create_batch_remediation(request: BatchJobRequest) -> BatchJobResponse: """Create a batch remediation job for multiple devices.""" try: - job_data = PlatformService.create_batch_job(request.device_configs) - job_id = storage.store_job(job_data) - - # Process the job (in a real implementation, this would be async/background) - PlatformService.process_batch_job(job_data) - storage.update_job(job_id, job_data) - - return BatchJobResponse(job_id=job_id, total_devices=job_data["total_devices"]) + job_id, job_data = _create_and_run_job(request.device_configs) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to create batch job: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to create batch job: {e!s}" + ) from e + return BatchJobResponse(job_id=job_id, total_devices=job_data["total_devices"]) -@router.get("/jobs/{job_id}", response_model=BatchJobStatus) +@router.get("/jobs/{job_id}") async def get_batch_job_status(job_id: str) -> BatchJobStatus: """Get the status of a batch job.""" job_data = storage.get_job(job_id) @@ -47,27 +58,29 @@ async def get_batch_job_status(job_id: str) -> BatchJobStatus: failed_devices=job_data["failed_devices"], ) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to get job status: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to get job status: {e!s}" + ) from e -@router.get("/jobs/{job_id}/results", response_model=BatchJobResults) +@router.get("/jobs/{job_id}/results") async def get_batch_job_results(job_id: str) -> BatchJobResults: """Get the results of a completed batch job.""" job_data = storage.get_job(job_id) if not job_data: raise HTTPException(status_code=404, detail="Job not found") - if job_data["status"] not in ["completed", "failed"]: + if job_data["status"] not in {"completed", "failed"}: raise HTTPException(status_code=400, detail="Job is not yet completed") - try: - summary = { - "total_devices": job_data["total_devices"], - "completed_devices": job_data["completed_devices"], - "failed_devices": job_data["failed_devices"], - "status": job_data["status"], - } + summary = { + "total_devices": job_data["total_devices"], + "completed_devices": job_data["completed_devices"], + "failed_devices": job_data["failed_devices"], + "status": job_data["status"], + } + try: return BatchJobResults( job_id=job_id, status=job_data["status"], @@ -75,4 +88,6 @@ async def get_batch_job_results(job_id: str) -> BatchJobResults: summary=summary, ) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to get job results: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to get job results: {e!s}" + ) from e diff --git a/hier_config_api/routers/configs.py b/hier_config_api/routers/configs.py index faa6283..78ae2a1 100644 --- a/hier_config_api/routers/configs.py +++ b/hier_config_api/routers/configs.py @@ -19,66 +19,81 @@ router = APIRouter(prefix="/api/v1/configs", tags=["configurations"]) -@router.post("/parse", response_model=ParseConfigResponse) +@router.post("/parse") async def parse_config(request: ParseConfigRequest) -> ParseConfigResponse: """Parse configuration text into structured format.""" try: - structured_config = ConfigService.parse_config(request.platform, request.config_text) - return ParseConfigResponse(platform=request.platform, structured_config=structured_config) + structured_config = ConfigService.parse_config( + request.platform, request.config_text + ) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to parse config: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to parse config: {e!s}" + ) from e + return ParseConfigResponse( + platform=request.platform, structured_config=structured_config + ) -@router.post("/compare", response_model=CompareConfigResponse) +@router.post("/compare") async def compare_configs(request: CompareConfigRequest) -> CompareConfigResponse: """Compare two configurations and return differences.""" try: unified_diff, has_changes = ConfigService.compare_configs( request.platform, request.running_config, request.intended_config ) - return CompareConfigResponse( - platform=request.platform, unified_diff=unified_diff, has_changes=has_changes - ) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to compare configs: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to compare configs: {e!s}" + ) from e + return CompareConfigResponse( + platform=request.platform, + unified_diff=unified_diff, + has_changes=has_changes, + ) -@router.post("/predict", response_model=PredictConfigResponse) +@router.post("/predict") async def predict_config(request: PredictConfigRequest) -> PredictConfigResponse: """Predict future configuration state after applying commands.""" try: predicted_config = ConfigService.predict_config( request.platform, request.current_config, request.commands_to_apply ) - return PredictConfigResponse(platform=request.platform, predicted_config=predicted_config) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to predict config: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to predict config: {e!s}" + ) from e + return PredictConfigResponse( + platform=request.platform, predicted_config=predicted_config + ) -@router.post("/merge", response_model=MergeConfigResponse) +@router.post("/merge") async def merge_configs(request: MergeConfigRequest) -> MergeConfigResponse: """Merge multiple configurations into one.""" try: merged_config = ConfigService.merge_configs(request.platform, request.configs) - return MergeConfigResponse(platform=request.platform, merged_config=merged_config) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to merge configs: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to merge configs: {e!s}" + ) from e + return MergeConfigResponse(platform=request.platform, merged_config=merged_config) -@router.post("/search", response_model=SearchConfigResponse) +@router.post("/search") async def search_config(request: SearchConfigRequest) -> SearchConfigResponse: """Search configuration for matching sections.""" try: matches = ConfigService.search_config( platform=request.platform, config_text=request.config_text, - equals=request.match_rules.equals, - contains=request.match_rules.contains, - startswith=request.match_rules.startswith, - regex_pattern=request.match_rules.regex, - ) - return SearchConfigResponse( - platform=request.platform, matches=matches, match_count=len(matches) + match_rules=request.match_rules, ) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to search config: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to search config: {e!s}" + ) from e + return SearchConfigResponse( + platform=request.platform, matches=matches, match_count=len(matches) + ) diff --git a/hier_config_api/routers/platforms.py b/hier_config_api/routers/platforms.py index 93cf56f..da890fc 100644 --- a/hier_config_api/routers/platforms.py +++ b/hier_config_api/routers/platforms.py @@ -13,31 +13,37 @@ router = APIRouter(prefix="/api/v1/platforms", tags=["platforms"]) -@router.get("", response_model=list[PlatformInfo]) +@router.get("") async def list_platforms() -> list[PlatformInfo]: """List all supported platforms.""" try: return PlatformService.list_platforms() except Exception as e: - raise HTTPException(status_code=500, detail=f"Failed to list platforms: {str(e)}") from e + raise HTTPException( + status_code=500, detail=f"Failed to list platforms: {e!s}" + ) from e -@router.get("/{platform}/rules", response_model=PlatformRules) +@router.get("/{platform}/rules") async def get_platform_rules(platform: str) -> PlatformRules: """Get platform-specific rules and behaviors.""" try: return PlatformService.get_platform_rules(platform) except Exception as e: raise HTTPException( - status_code=400, detail=f"Failed to get platform rules: {str(e)}" + status_code=400, detail=f"Failed to get platform rules: {e!s}" ) from e -@router.post("/{platform}/validate", response_model=ValidateConfigResponse) -async def validate_config(platform: str, request: ValidateConfigRequest) -> ValidateConfigResponse: +@router.post("/{platform}/validate") +async def validate_config( + platform: str, request: ValidateConfigRequest +) -> ValidateConfigResponse: """Validate configuration for a specific platform.""" try: result = PlatformService.validate_config(platform, request.config_text) - return ValidateConfigResponse(**result) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to validate config: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to validate config: {e!s}" + ) from e + return ValidateConfigResponse(**result) diff --git a/hier_config_api/routers/remediation.py b/hier_config_api/routers/remediation.py index 0961f1c..e93a40b 100644 --- a/hier_config_api/routers/remediation.py +++ b/hier_config_api/routers/remediation.py @@ -1,5 +1,7 @@ """API router for remediation operations.""" +from typing import Annotated, Any + from fastapi import APIRouter, HTTPException, Query from hier_config_api.models.remediation import ( @@ -15,81 +17,95 @@ router = APIRouter(prefix="/api/v1/remediation", tags=["remediation"]) -@router.post("/generate", response_model=GenerateRemediationResponse) -async def generate_remediation(request: GenerateRemediationRequest) -> GenerateRemediationResponse: +def _generate_and_store(request: GenerateRemediationRequest) -> dict[str, Any]: + """Generate a remediation, persist it, and stamp it with its ID.""" + result = RemediationService.generate_remediation(request) + remediation_id = storage.store_remediation(result) + result["remediation_id"] = remediation_id + return result + + +@router.post("/generate") +async def generate_remediation( + request: GenerateRemediationRequest, +) -> GenerateRemediationResponse: """Generate remediation and rollback configurations.""" try: - result = RemediationService.generate_remediation( - platform=request.platform, - running_config=request.running_config, - intended_config=request.intended_config, - tag_rules=request.tag_rules, - include_tags=request.include_tags, - exclude_tags=request.exclude_tags, - ) - - # Store remediation - remediation_id = storage.store_remediation(result) - result["remediation_id"] = remediation_id - - return GenerateRemediationResponse( - remediation_id=remediation_id, - platform=result["platform"], - remediation_config=result["remediation_config"], - rollback_config=result["rollback_config"], - summary=result["summary"], - tags=result["tags"], - ) + result = _generate_and_store(request) except Exception as e: raise HTTPException( - status_code=400, detail=f"Failed to generate remediation: {str(e)}" + status_code=400, detail=f"Failed to generate remediation: {e!s}" ) from e + return GenerateRemediationResponse( + remediation_id=result["remediation_id"], + platform=result["platform"], + remediation_config=result["remediation_config"], + rollback_config=result["rollback_config"], + summary=result["summary"], + tags=result["tags"], + ) -@router.post("/{remediation_id}/tags", response_model=ApplyTagsResponse) -async def apply_tags(remediation_id: str, request: ApplyTagsRequest) -> ApplyTagsResponse: +def _apply_and_store_tags( + remediation_id: str, + remediation_data: dict[str, Any], + request: ApplyTagsRequest, +) -> tuple[str, dict[str, list[str]]]: + """Apply tag rules to a stored remediation and persist the tags.""" + remediation_config = remediation_data["remediation_config"] + tagged_config, tags = RemediationService.apply_tags( + remediation_config, request.tag_rules + ) + storage.update_remediation(remediation_id, {"tags": tags}) + return tagged_config, tags + + +@router.post("/{remediation_id}/tags") +async def apply_tags( + remediation_id: str, request: ApplyTagsRequest +) -> ApplyTagsResponse: """Apply tags to an existing remediation.""" remediation_data = storage.get_remediation(remediation_id) if not remediation_data: raise HTTPException(status_code=404, detail="Remediation not found") try: - remediation_config = remediation_data["remediation_config"] - tagged_config, tags = RemediationService.apply_tags(remediation_config, request.tag_rules) - - # Update stored remediation - storage.update_remediation(remediation_id, {"tags": tags}) - - return ApplyTagsResponse( - remediation_id=remediation_id, remediation_config=tagged_config, tags=tags + tagged_config, tags = _apply_and_store_tags( + remediation_id, remediation_data, request ) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to apply tags: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to apply tags: {e!s}" + ) from e + return ApplyTagsResponse( + remediation_id=remediation_id, remediation_config=tagged_config, tags=tags + ) -@router.get("/{remediation_id}/filter", response_model=FilterRemediationResponse) +@router.get("/{remediation_id}/filter") async def filter_remediation( remediation_id: str, - include_tags: list[str] = Query(None), - exclude_tags: list[str] = Query(None), + include_tags: Annotated[list[str] | None, Query()] = None, + exclude_tags: Annotated[list[str] | None, Query()] = None, ) -> FilterRemediationResponse: """Filter remediation by tags.""" remediation_data = storage.get_remediation(remediation_id) if not remediation_data: raise HTTPException(status_code=404, detail="Remediation not found") - try: - remediation_config = remediation_data["remediation_config"] - tags = remediation_data.get("tags", {}) + remediation_config = remediation_data["remediation_config"] + tags = remediation_data.get("tags", {}) + try: filtered_config, summary = RemediationService.filter_remediation( remediation_config, tags, include_tags, exclude_tags ) - - return FilterRemediationResponse( - remediation_id=remediation_id, filtered_config=filtered_config, summary=summary - ) except Exception as e: raise HTTPException( - status_code=400, detail=f"Failed to filter remediation: {str(e)}" + status_code=400, detail=f"Failed to filter remediation: {e!s}" ) from e + return FilterRemediationResponse( + remediation_id=remediation_id, + filtered_config=filtered_config, + summary=summary, + ) diff --git a/hier_config_api/routers/reports.py b/hier_config_api/routers/reports.py index 4adf11e..53beccc 100644 --- a/hier_config_api/routers/reports.py +++ b/hier_config_api/routers/reports.py @@ -1,5 +1,7 @@ """API router for multi-device reporting.""" +from typing import Annotated + from fastapi import APIRouter, HTTPException, Query from fastapi.responses import PlainTextResponse @@ -15,19 +17,22 @@ router = APIRouter(prefix="/api/v1/reports", tags=["reports"]) -@router.post("", response_model=CreateReportResponse) +@router.post("") async def create_report(request: CreateReportRequest) -> CreateReportResponse: """Create a multi-device report.""" try: report_data = ReportService.create_report(request.remediations) - report_id = storage.store_report(report_data) - - return CreateReportResponse(report_id=report_id, total_devices=report_data["total_devices"]) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to create report: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to create report: {e!s}" + ) from e + report_id = storage.store_report(report_data) + return CreateReportResponse( + report_id=report_id, total_devices=report_data["total_devices"] + ) -@router.get("/{report_id}/summary", response_model=ReportSummary) +@router.get("/{report_id}/summary") async def get_report_summary(report_id: str) -> ReportSummary: """Get summary statistics for a report.""" report_data = storage.get_report(report_id) @@ -38,13 +43,15 @@ async def get_report_summary(report_id: str) -> ReportSummary: return ReportService.get_summary(report_data) except Exception as e: raise HTTPException( - status_code=400, detail=f"Failed to get report summary: {str(e)}" + status_code=400, detail=f"Failed to get report summary: {e!s}" ) from e -@router.get("/{report_id}/changes", response_model=GetReportChangesResponse) +@router.get("/{report_id}/changes") async def get_report_changes( - report_id: str, tag: str | None = Query(None), min_devices: int = Query(1) + report_id: str, + tag: Annotated[str | None, Query()] = None, + min_devices: Annotated[int, Query()] = 1, ) -> GetReportChangesResponse: """Get detailed change analysis for a report.""" report_data = storage.get_report(report_id) @@ -52,28 +59,36 @@ async def get_report_changes( raise HTTPException(status_code=404, detail="Report not found") try: - changes = ReportService.get_changes(report_data, tag_filter=tag, min_devices=min_devices) - - return GetReportChangesResponse( - report_id=report_id, changes=changes, total_unique_changes=len(changes) + changes = ReportService.get_changes( + report_data, tag_filter=tag, min_devices=min_devices ) except Exception as e: raise HTTPException( - status_code=400, detail=f"Failed to get report changes: {str(e)}" + status_code=400, detail=f"Failed to get report changes: {e!s}" ) from e + return GetReportChangesResponse( + report_id=report_id, changes=changes, total_unique_changes=len(changes) + ) @router.get("/{report_id}/export", response_class=PlainTextResponse) -async def export_report(report_id: str, format: str = Query("json")) -> str: +async def export_report( + report_id: str, + export_format: Annotated[str, Query(alias="format")] = "json", +) -> str: """Export report in specified format (json, csv, yaml).""" report_data = storage.get_report(report_id) if not report_data: raise HTTPException(status_code=404, detail="Report not found") - if format not in ["json", "csv", "yaml"]: - raise HTTPException(status_code=400, detail="Format must be one of: json, csv, yaml") + if export_format not in {"json", "csv", "yaml"}: + raise HTTPException( + status_code=400, detail="Format must be one of: json, csv, yaml" + ) try: - return ReportService.export_report(report_data, format_type=format) + return ReportService.export_report(report_data, format_type=export_format) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to export report: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to export report: {e!s}" + ) from e diff --git a/hier_config_api/services/config_service.py b/hier_config_api/services/config_service.py index 6be493e..eaf8022 100644 --- a/hier_config_api/services/config_service.py +++ b/hier_config_api/services/config_service.py @@ -3,7 +3,20 @@ import re from typing import Any -from hier_config import HConfig, Platform, WorkflowRemediation +from hier_config import HConfig, HConfigChild, Platform, WorkflowRemediation + +from hier_config_api.models.config import MatchRule + + +def _line_matches(config_line: str, match_rules: MatchRule) -> bool: + """Check whether a configuration line satisfies any of the match rules.""" + if match_rules.equals and config_line == match_rules.equals: + return True + if match_rules.contains and match_rules.contains in config_line: + return True + if match_rules.startswith and config_line.startswith(match_rules.startswith): + return True + return bool(match_rules.regex and re.search(match_rules.regex, config_line)) class ConfigService: @@ -29,15 +42,11 @@ def parse_config(platform: str, config_text: str) -> dict[str, Any]: hconfig = HConfig.from_text(platform_enum, config_text) # Convert HConfig tree to dictionary representation - def config_to_dict(config_obj: Any) -> dict[str, Any]: - result: dict[str, Any] = { + def config_to_dict(config_obj: HConfig | HConfigChild) -> dict[str, Any]: + return { "text": str(config_obj), - "children": [], + "children": [config_to_dict(child) for child in config_obj.children], } - if hasattr(config_obj, "children"): - for child in config_obj.children: - result["children"].append(config_to_dict(child)) - return result return config_to_dict(hconfig) @@ -54,20 +63,19 @@ def compare_configs( remediation = workflow.remediation_config rollback = workflow.rollback_config - diff_lines = [] + diff_lines: list[str] = [] # Generate unified diff format if remediation: - diff_lines.append("--- running_config") - diff_lines.append("+++ intended_config") - for line in str(remediation).splitlines(): - if line.strip(): - diff_lines.append(f"+ {line}") + diff_lines.extend(("--- running_config", "+++ intended_config")) + diff_lines.extend( + f"+ {line}" for line in str(remediation).splitlines() if line.strip() + ) if rollback: - for line in str(rollback).splitlines(): - if line.strip(): - diff_lines.append(f"- {line}") + diff_lines.extend( + f"- {line}" for line in str(rollback).splitlines() if line.strip() + ) unified_diff = "\n".join(diff_lines) if diff_lines else "No differences found" has_changes = bool(diff_lines) @@ -75,7 +83,9 @@ def compare_configs( return unified_diff, has_changes @staticmethod - def predict_config(platform: str, current_config: str, commands_to_apply: str) -> str: + def predict_config( + _platform: str, current_config: str, commands_to_apply: str + ) -> str: """Predict configuration state after applying commands.""" # Simple merge: append new commands config_lines = current_config.splitlines() @@ -106,7 +116,7 @@ def merge_configs(platform: str, configs: list[str]) -> str: workflow = WorkflowRemediation(running_hconfig, intended_hconfig) remediation = workflow.remediation_config if remediation: - merged += "\n" + str(remediation) + merged += f"\n{remediation!s}" return merged @@ -114,38 +124,23 @@ def merge_configs(platform: str, configs: list[str]) -> str: def search_config( platform: str, config_text: str, - equals: str | None = None, - contains: str | None = None, - startswith: str | None = None, - regex_pattern: str | None = None, + match_rules: MatchRule, ) -> list[str]: """Search configuration for matching lines.""" platform_enum = ConfigService._get_platform(platform) hconfig = HConfig.from_text(platform_enum, config_text) - matches = [] + matches: list[str] = [] - def search_recursive(config_obj: Any) -> None: + def search_recursive(config_obj: HConfig | HConfigChild) -> None: config_line = str(config_obj).strip() - # Check matching conditions - is_match = False - if equals and config_line == equals: - is_match = True - elif contains and contains in config_line: - is_match = True - elif startswith and config_line.startswith(startswith): - is_match = True - elif regex_pattern and re.search(regex_pattern, config_line): - is_match = True - - if is_match: + if _line_matches(config_line, match_rules): matches.append(config_line) # Recursively search children - if hasattr(config_obj, "children"): - for child in config_obj.children: - search_recursive(child) + for child in config_obj.children: + search_recursive(child) search_recursive(hconfig) return matches diff --git a/hier_config_api/services/platform_service.py b/hier_config_api/services/platform_service.py index d89c5b0..883c614 100644 --- a/hier_config_api/services/platform_service.py +++ b/hier_config_api/services/platform_service.py @@ -1,6 +1,6 @@ """Service layer for platform information and batch operations.""" -from typing import Any +from typing import Any, ClassVar from hier_config import HConfig, Platform, WorkflowRemediation @@ -11,7 +11,7 @@ class PlatformService: """Service for handling platform-related operations.""" # Common platform definitions - PLATFORMS = { + PLATFORMS: ClassVar[dict[str, PlatformInfo]] = { "cisco_ios": PlatformInfo( platform_name="cisco_ios", display_name="Cisco IOS", @@ -79,29 +79,29 @@ def _get_platform(platform_str: str) -> Platform: @staticmethod def validate_config(platform: str, config_text: str) -> dict[str, Any]: """Validate configuration for a platform.""" - warnings = [] - errors = [] + warnings: list[str] = [] + errors: list[str] = [] is_valid = True + platform_enum = PlatformService._get_platform(platform) try: # Try to parse the configuration - platform_enum = PlatformService._get_platform(platform) HConfig.from_text(platform_enum, config_text) - + # A validation endpoint must convert any parsing failure into a + # validation error rather than propagate it as a server error. + except Exception as exc: # noqa: BLE001 # pylint: disable=broad-exception-caught + errors.append(f"Configuration parsing error: {exc!s}") + is_valid = False + else: # Basic validation checks if not config_text.strip(): warnings.append("Configuration is empty") is_valid = False # Platform-specific validation could be added here - if platform == "cisco_ios": - # Check for common Cisco IOS patterns - if "hostname" not in config_text: - warnings.append("No hostname configured") - - except Exception as e: - errors.append(f"Configuration parsing error: {str(e)}") - is_valid = False + # Check for common Cisco IOS patterns + if platform == "cisco_ios" and "hostname" not in config_text: + warnings.append("No hostname configured") return { "platform": platform, @@ -123,46 +123,59 @@ def create_batch_job(device_configs: list[dict[str, Any]]) -> dict[str, Any]: "results": [], } + @staticmethod + def _remediate_device(device_config: dict[str, Any]) -> dict[str, Any]: + """Generate remediation and rollback for a single batch device.""" + platform = device_config.get("platform", "cisco_ios") + running_config = device_config.get("running_config", "") + intended_config = device_config.get("intended_config", "") + + platform_enum = PlatformService._get_platform(platform) + running_hconfig = HConfig.from_text(platform_enum, running_config) + intended_hconfig = HConfig.from_text(platform_enum, intended_config) + + workflow = WorkflowRemediation(running_hconfig, intended_hconfig) + remediation = workflow.remediation_config + rollback = workflow.rollback_config + + return { + "device_id": device_config.get("device_id"), + "status": "success", + "remediation": str(remediation) if remediation else "", + "rollback": str(rollback) if rollback else "", + } + + @staticmethod + def _process_device(device_config: dict[str, Any]) -> tuple[dict[str, Any], bool]: + """Process one batch device, returning its result and success flag.""" + try: + result = PlatformService._remediate_device(device_config) + # Batch jobs record per-device failures instead of aborting the job, + # so any processing error must be captured here. + except Exception as exc: # noqa: BLE001 # pylint: disable=broad-exception-caught + return ( + { + "device_id": device_config.get("device_id"), + "status": "failed", + "error": str(exc), + }, + False, + ) + return result, True + @staticmethod def process_batch_job(job_data: dict[str, Any]) -> dict[str, Any]: """Process a batch job (simplified synchronous version).""" - results = [] + results: list[dict[str, Any]] = [] completed = 0 failed = 0 for device_config in job_data["device_configs"]: - try: - # Process each device - platform = device_config.get("platform", "cisco_ios") - running_config = device_config.get("running_config", "") - intended_config = device_config.get("intended_config", "") - - platform_enum = PlatformService._get_platform(platform) - running_hconfig = HConfig.from_text(platform_enum, running_config) - intended_hconfig = HConfig.from_text(platform_enum, intended_config) - - workflow = WorkflowRemediation(running_hconfig, intended_hconfig) - remediation = workflow.remediation_config - rollback = workflow.rollback_config - - results.append( - { - "device_id": device_config.get("device_id"), - "status": "success", - "remediation": str(remediation) if remediation else "", - "rollback": str(rollback) if rollback else "", - } - ) + result, succeeded = PlatformService._process_device(device_config) + results.append(result) + if succeeded: completed += 1 - - except Exception as e: - results.append( - { - "device_id": device_config.get("device_id"), - "status": "failed", - "error": str(e), - } - ) + else: failed += 1 # Update job data diff --git a/hier_config_api/services/remediation_service.py b/hier_config_api/services/remediation_service.py index b027c57..d8bd79b 100644 --- a/hier_config_api/services/remediation_service.py +++ b/hier_config_api/services/remediation_service.py @@ -4,7 +4,11 @@ from hier_config import HConfig, Platform, WorkflowRemediation -from hier_config_api.models.remediation import RemediationSummary, TagRule +from hier_config_api.models.remediation import ( + GenerateRemediationRequest, + RemediationSummary, + TagRule, +) class RemediationService: @@ -24,24 +28,11 @@ def _get_platform(platform_str: str) -> Platform: return platform_map.get(platform_str.lower(), Platform.GENERIC) @staticmethod - def generate_remediation( - platform: str, - running_config: str, - intended_config: str, - tag_rules: list[TagRule] | None = None, - include_tags: list[str] | None = None, - exclude_tags: list[str] | None = None, - ) -> dict[str, Any]: + def generate_remediation(request: GenerateRemediationRequest) -> dict[str, Any]: """Generate remediation and rollback configurations.""" - platform_enum = RemediationService._get_platform(platform) - running_hconfig = HConfig.from_text(platform_enum, running_config) - intended_hconfig = HConfig.from_text(platform_enum, intended_config) - - # Load tag rules if provided - if tag_rules: - # Convert tag rules to hier_config format - # This is simplified - actual implementation would need proper tag loading - pass + platform_enum = RemediationService._get_platform(request.platform) + running_hconfig = HConfig.from_text(platform_enum, request.running_config) + intended_hconfig = HConfig.from_text(platform_enum, request.intended_config) # Generate remediation and rollback workflow = WorkflowRemediation(running_hconfig, intended_hconfig) @@ -61,23 +52,16 @@ def generate_remediation( additions=additions, deletions=deletions, modifications=modifications ) - # Apply tag filtering if specified - filtered_remediation = str(remediation) if remediation else "" - if include_tags or exclude_tags: - # Simplified tag filtering - # In a real implementation, you'd filter based on tags - pass + tags: dict[str, list[str]] = {} - result = { - "remediation_config": filtered_remediation, + return { + "remediation_config": str(remediation) if remediation else "", "rollback_config": str(rollback) if rollback else "", "summary": summary, - "tags": {}, - "platform": platform, + "tags": tags, + "platform": request.platform, } - return result - @staticmethod def apply_tags( remediation_config: str, tag_rules: list[TagRule] @@ -105,7 +89,7 @@ def filter_remediation( ) -> tuple[str, RemediationSummary]: """Filter remediation configuration by tags.""" lines = remediation_config.splitlines() - filtered_lines = [] + filtered_lines: list[str] = [] for i, line in enumerate(lines): line_tags = tags.get(str(i), []) diff --git a/hier_config_api/services/report_service.py b/hier_config_api/services/report_service.py index e2b6c27..e742557 100644 --- a/hier_config_api/services/report_service.py +++ b/hier_config_api/services/report_service.py @@ -8,6 +8,7 @@ import yaml +from hier_config_api.models.remediation import GenerateRemediationRequest from hier_config_api.models.report import ChangeDetail, DeviceRemediation, ReportSummary from hier_config_api.services.remediation_service import RemediationService @@ -35,9 +36,11 @@ def create_report(remediations: list[DeviceRemediation]) -> dict[str, Any]: for device_rem in remediations: # Generate remediation for this device remediation_result = RemediationService.generate_remediation( - platform=device_rem.platform, - running_config=device_rem.running_config, - intended_config=device_rem.intended_config, + GenerateRemediationRequest( + platform=device_rem.platform, + running_config=device_rem.running_config, + intended_config=device_rem.intended_config, + ) ) remediation_config = remediation_result["remediation_config"] @@ -123,16 +126,22 @@ def export_report(report_data: dict[str, Any], format_type: str = "json") -> str if format_type == "json": return json.dumps(report_data, indent=2) - elif format_type == "yaml": + if format_type == "yaml": return yaml.dump(report_data, default_flow_style=False) - elif format_type == "csv": + if format_type == "csv": output = io.StringIO() writer = csv.writer(output) # Write header writer.writerow( - ["Device ID", "Platform", "Has Changes", "Change Count", "Remediation Summary"] + [ + "Device ID", + "Platform", + "Has Changes", + "Change Count", + "Remediation Summary", + ] ) # Write device rows @@ -144,7 +153,7 @@ def export_report(report_data: dict[str, Any], format_type: str = "json") -> str device["has_changes"], device["change_count"], ( - device["remediation"][:50] + "..." + f"{device['remediation'][:50]}..." if len(device["remediation"]) > 50 else device["remediation"] ), @@ -153,5 +162,5 @@ def export_report(report_data: dict[str, Any], format_type: str = "json") -> str return output.getvalue() - else: - raise ValueError(f"Unsupported format: {format_type}") + message = f"Unsupported format: {format_type}" + raise ValueError(message) diff --git a/tests/test_configs.py b/tests/test_configs.py index 647a056..d99efa3 100644 --- a/tests/test_configs.py +++ b/tests/test_configs.py @@ -16,7 +16,9 @@ def test_parse_config(client: TestClient, sample_cisco_ios_config: str) -> None: def test_compare_configs( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test comparing configurations.""" response = client.post( diff --git a/tests/test_platforms.py b/tests/test_platforms.py index 2c78d4d..560ff73 100644 --- a/tests/test_platforms.py +++ b/tests/test_platforms.py @@ -7,7 +7,7 @@ def test_list_platforms(client: TestClient) -> None: """Test listing all platforms.""" response = client.get("/api/v1/platforms") assert response.status_code == 200 - data = response.json() + data: list[dict[str, str]] = response.json() assert isinstance(data, list) assert len(data) > 0 # Check that cisco_ios is in the list @@ -28,7 +28,8 @@ def test_get_platform_rules(client: TestClient) -> None: def test_validate_config(client: TestClient, sample_cisco_ios_config: str) -> None: """Test validating configuration.""" response = client.post( - "/api/v1/platforms/cisco_ios/validate", json={"config_text": sample_cisco_ios_config} + "/api/v1/platforms/cisco_ios/validate", + json={"config_text": sample_cisco_ios_config}, ) assert response.status_code == 200 data = response.json() @@ -40,7 +41,9 @@ def test_validate_config(client: TestClient, sample_cisco_ios_config: str) -> No def test_validate_empty_config(client: TestClient) -> None: """Test validating empty configuration.""" - response = client.post("/api/v1/platforms/cisco_ios/validate", json={"config_text": ""}) + response = client.post( + "/api/v1/platforms/cisco_ios/validate", json={"config_text": ""} + ) assert response.status_code == 200 data = response.json() assert data["platform"] == "cisco_ios" @@ -49,7 +52,9 @@ def test_validate_empty_config(client: TestClient) -> None: def test_create_batch_job( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test creating a batch remediation job.""" response = client.post( @@ -78,7 +83,9 @@ def test_create_batch_job( def test_get_batch_job_status( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test getting batch job status.""" # First, create a batch job @@ -108,7 +115,9 @@ def test_get_batch_job_status( def test_get_batch_job_results( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test getting batch job results.""" # First, create a batch job diff --git a/tests/test_remediation.py b/tests/test_remediation.py index 9ad3d32..5b04bba 100644 --- a/tests/test_remediation.py +++ b/tests/test_remediation.py @@ -4,7 +4,9 @@ def test_generate_remediation( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test generating remediation.""" response = client.post( @@ -26,7 +28,9 @@ def test_generate_remediation( def test_generate_remediation_with_tags( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test generating remediation with tag rules.""" response = client.post( @@ -46,7 +50,9 @@ def test_generate_remediation_with_tags( def test_apply_tags( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test applying tags to remediation.""" # First, generate a remediation @@ -73,7 +79,9 @@ def test_apply_tags( def test_filter_remediation( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test filtering remediation by tags.""" # First, generate a remediation @@ -90,7 +98,8 @@ def test_filter_remediation( # Filter remediation response = client.get( - f"/api/v1/remediation/{remediation_id}/filter", params={"include_tags": ["safe"]} + f"/api/v1/remediation/{remediation_id}/filter", + params={"include_tags": ["safe"]}, ) assert response.status_code == 200 data = response.json() From 05c3154f3656603a81840d8a0da7ad366e0732d5 Mon Sep 17 00:00:00 2001 From: jtdub Date: Thu, 6 Aug 2026 10:17:39 -0500 Subject: [PATCH 4/6] Add tests to meet 95% coverage floor and document standards Coverage sat at 89.4% once the hier_config coverage gate was enforced. Add function-based, fully annotated tests for the previously uncovered paths: the root and health endpoints, 404 responses for unknown remediations, reports, and batch jobs, 400 responses when parsing fails (via monkeypatched HConfig.from_text) or a search regex is invalid, the invalid export format guard, YAML export and tag-filtered change queries, per-device failure recording in batch jobs, storage updates for unknown IDs, remediation tag include/exclude filtering, and merge/search service edge cases. Total coverage is now 95.9% with no '# pragma: no cover' exclusions. Update CLAUDE.md and the README development section to document the scripts/build.py commands, the lint/typing stack, the coverage floor, and the new CI matrix. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 49 +++++---- README.md | 45 +++++--- tests/test_errors.py | 231 +++++++++++++++++++++++++++++++++++++++++ tests/test_main.py | 19 ++++ tests/test_reports.py | 78 +++++++++++++- tests/test_services.py | 97 +++++++++++++++++ 6 files changed, 479 insertions(+), 40 deletions(-) create mode 100644 tests/test_errors.py create mode 100644 tests/test_main.py create mode 100644 tests/test_services.py diff --git a/CLAUDE.md b/CLAUDE.md index 54fdec4..0582a9d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,31 +8,35 @@ hier-config-api is a FastAPI REST API providing an interface to the [hier_config ## Commands +All commands use **poetry** (not pip): + ```bash # Install dependencies -poetry install # production deps -poetry install --with dev # include dev deps +poetry install # Run development server (with hot reload) poetry run uvicorn hier_config_api.main:app --reload -# Linting & formatting -poetry run ruff check . # lint -poetry run ruff check . --fix # lint with auto-fix -poetry run ruff format . # format -poetry run ruff format --check . # check formatting +# Full lint + test suite (equivalent to CI) +poetry run python scripts/build.py lint-and-test + +# Lint only (ruff format + check, mypy, pyright, pylint, yamllint, flynt — run in parallel) +poetry run python scripts/build.py lint -# Type checking -poetry run mypy hier_config_api +# Lint with auto-fixes (ruff --fix, ruff format, flynt) +poetry run python scripts/build.py lint --fix -# Tests -poetry run pytest # all tests (includes coverage) +# Tests with coverage (95% coverage required) +poetry run python scripts/build.py pytest --coverage + +# Tests without coverage +poetry run pytest # all tests poetry run pytest tests/test_configs.py -v # single test file poetry run pytest tests/test_configs.py::test_parse_config -v # single test # Documentation -poetry run mkdocs serve # local preview with live reload -poetry run mkdocs build # build static site +poetry run mkdocs serve # local preview with live reload +poetry run mkdocs build --strict # build static site (CI runs this) ``` ## Architecture @@ -54,13 +58,20 @@ poetry run mkdocs build # build static site ## Code Quality -- **Ruff**: linter and formatter, line length 100, target Python 3.10 -- **MyPy**: strict mode enabled; `hier_config.*` has `ignore_missing_imports` -- **Pytest**: asyncio_mode="auto", coverage configured via `--cov=hier_config_api` +This repo follows the hier_config lint/typing/testing standards, enforced by +`scripts/build.py` and CI: + +- **Ruff**: `select = ["ALL"]` with preview rules, line length 88, target Python 3.10; formatting via `ruff format` with `docstring-code-format` +- **MyPy**: `strict = true` with the pydantic plugin +- **Pyright**: `typeCheckingMode = "strict"` +- **Pylint**: extension plugins + `pylint_pydantic`; rules already covered by ruff are disabled +- **yamllint / flynt**: YAML style (2-space indent, no document-start) and f-string enforcement +- **Pytest**: flat function-based tests with full type annotations; 95% coverage floor (`--cov=hier_config_api --cov-fail-under=95`) - Tests use `FastAPI.TestClient` with fixtures in `tests/conftest.py` +- Never loosen lint or coverage configuration to make a change pass; do not add unjustified `# noqa` / `# type: ignore` suppressions ## CI -GitHub Actions runs on push/PR to `main`/`develop`: -- **Lint job**: ruff check, ruff format --check, mypy -- **Test job**: pytest across Python 3.10, 3.11, 3.12 +GitHub Actions runs on push/PR to `develop`/`next`: +- **build job** (Python 3.10–3.14 matrix): `poetry run python scripts/build.py lint` then `poetry run python scripts/build.py pytest --coverage` +- **docs job**: `poetry run mkdocs build --strict`; deploys to GitHub Pages on push to `develop` diff --git a/README.md b/README.md index ade6876..caf339d 100644 --- a/README.md +++ b/README.md @@ -238,34 +238,47 @@ Get the results of a completed batch job. ## Development +This project follows the same development, testing, and linting standards as +[hier_config](https://github.com/netdevops/hier_config). All checks are driven +by `scripts/build.py`: + +```bash +# Full lint + test suite (what CI runs) +poetry run python scripts/build.py lint-and-test + +# Lint only (ruff format + check, mypy, pyright, pylint, yamllint, flynt — in parallel) +poetry run python scripts/build.py lint + +# Lint with auto-fixes applied +poetry run python scripts/build.py lint --fix + +# Tests with coverage (95% minimum enforced) +poetry run python scripts/build.py pytest --coverage +``` + ### Running Tests ```bash # Run all tests poetry run pytest -# Run with coverage -poetry run pytest --cov=hier_config_api --cov-report=html - # Run specific test file poetry run pytest tests/test_configs.py -v -``` - -### Code Quality -```bash -# Run ruff linter -poetry run ruff check . +# Run a single test +poetry run pytest tests/test_configs.py::test_parse_config -v +``` -# Run ruff formatter -poetry run ruff format . +### Code Quality Standards -# Run mypy type checker -poetry run mypy hier_config_api +- **ruff**: `select = ["ALL"]` with preview rules, line length 88; formatting via `ruff format` +- **mypy**: strict mode with the pydantic plugin +- **pyright**: `typeCheckingMode = "strict"` +- **pylint**: extension plugins + `pylint_pydantic`, for rules not covered by ruff +- **yamllint / flynt**: YAML style and f-string enforcement +- **pytest**: flat function-based tests with full type annotations; 95% coverage floor -# Run all linters -poetry run ruff check . && poetry run mypy hier_config_api -``` +Never loosen the lint or coverage configuration to make a change pass. ## Supported Platforms diff --git a/tests/test_errors.py b/tests/test_errors.py new file mode 100644 index 0000000..21fc936 --- /dev/null +++ b/tests/test_errors.py @@ -0,0 +1,231 @@ +"""Tests for API error handling paths.""" + +from typing import NoReturn + +import pytest +from fastapi.testclient import TestClient +from hier_config import HConfig + + +def _raise_parse_error(*_args: object, **_kwargs: object) -> NoReturn: + """Stand-in for HConfig.from_text that always fails.""" + message = "simulated parse failure" + raise ValueError(message) + + +def test_parse_config_error( + client: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that a parsing failure returns a 400 error.""" + monkeypatch.setattr(HConfig, "from_text", _raise_parse_error) + response = client.post( + "/api/v1/configs/parse", + json={"platform": "cisco_ios", "config_text": "hostname router1"}, + ) + assert response.status_code == 400 + assert "Failed to parse config" in response.json()["detail"] + + +def test_compare_configs_error( + client: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that a comparison failure returns a 400 error.""" + monkeypatch.setattr(HConfig, "from_text", _raise_parse_error) + response = client.post( + "/api/v1/configs/compare", + json={ + "platform": "cisco_ios", + "running_config": "hostname a", + "intended_config": "hostname b", + }, + ) + assert response.status_code == 400 + assert "Failed to compare configs" in response.json()["detail"] + + +def test_merge_configs_error( + client: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that a merge failure returns a 400 error.""" + monkeypatch.setattr(HConfig, "from_text", _raise_parse_error) + response = client.post( + "/api/v1/configs/merge", + json={"platform": "cisco_ios", "configs": ["hostname a", "hostname b"]}, + ) + assert response.status_code == 400 + assert "Failed to merge configs" in response.json()["detail"] + + +def test_search_config_invalid_regex(client: TestClient) -> None: + """Test that an invalid regex pattern returns a 400 error.""" + response = client.post( + "/api/v1/configs/search", + json={ + "platform": "cisco_ios", + "config_text": "hostname router1", + "match_rules": {"regex": "["}, + }, + ) + assert response.status_code == 400 + assert "Failed to search config" in response.json()["detail"] + + +def test_generate_remediation_error( + client: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that a remediation generation failure returns a 400 error.""" + monkeypatch.setattr(HConfig, "from_text", _raise_parse_error) + response = client.post( + "/api/v1/remediation/generate", + json={ + "platform": "cisco_ios", + "running_config": "hostname a", + "intended_config": "hostname b", + }, + ) + assert response.status_code == 400 + assert "Failed to generate remediation" in response.json()["detail"] + + +def test_apply_tags_unknown_remediation(client: TestClient) -> None: + """Test that applying tags to an unknown remediation returns 404.""" + response = client.post( + "/api/v1/remediation/unknown-id/tags", + json={"tag_rules": [{"match_rules": ["interface"], "tags": ["intf"]}]}, + ) + assert response.status_code == 404 + + +def test_filter_unknown_remediation(client: TestClient) -> None: + """Test that filtering an unknown remediation returns 404.""" + response = client.get("/api/v1/remediation/unknown-id/filter") + assert response.status_code == 404 + + +def test_create_report_error( + client: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that a report creation failure returns a 400 error.""" + monkeypatch.setattr(HConfig, "from_text", _raise_parse_error) + response = client.post( + "/api/v1/reports", + json={ + "remediations": [ + { + "device_id": "router1", + "platform": "cisco_ios", + "running_config": "hostname a", + "intended_config": "hostname b", + } + ] + }, + ) + assert response.status_code == 400 + assert "Failed to create report" in response.json()["detail"] + + +def test_get_summary_unknown_report(client: TestClient) -> None: + """Test that requesting a summary for an unknown report returns 404.""" + response = client.get("/api/v1/reports/unknown-id/summary") + assert response.status_code == 404 + + +def test_get_changes_unknown_report(client: TestClient) -> None: + """Test that requesting changes for an unknown report returns 404.""" + response = client.get("/api/v1/reports/unknown-id/changes") + assert response.status_code == 404 + + +def test_export_unknown_report(client: TestClient) -> None: + """Test that exporting an unknown report returns 404.""" + response = client.get("/api/v1/reports/unknown-id/export") + assert response.status_code == 404 + + +def test_export_report_invalid_format( + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, +) -> None: + """Test that exporting a report with an unsupported format returns 400.""" + create_response = client.post( + "/api/v1/reports", + json={ + "remediations": [ + { + "device_id": "router1", + "platform": "cisco_ios", + "running_config": sample_cisco_ios_config, + "intended_config": sample_cisco_ios_intended_config, + } + ] + }, + ) + assert create_response.status_code == 200 + report_id = create_response.json()["report_id"] + + response = client.get(f"/api/v1/reports/{report_id}/export?format=xml") + assert response.status_code == 400 + + +def test_get_status_unknown_batch_job(client: TestClient) -> None: + """Test that requesting the status of an unknown batch job returns 404.""" + response = client.get("/api/v1/batch/jobs/unknown-id") + assert response.status_code == 404 + + +def test_get_results_unknown_batch_job(client: TestClient) -> None: + """Test that requesting the results of an unknown batch job returns 404.""" + response = client.get("/api/v1/batch/jobs/unknown-id/results") + assert response.status_code == 404 + + +def test_validate_config_parse_error( + client: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that validation reports parsing failures as validation errors.""" + monkeypatch.setattr(HConfig, "from_text", _raise_parse_error) + response = client.post( + "/api/v1/platforms/cisco_ios/validate", + json={"config_text": "hostname router1"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["is_valid"] is False + assert any("parsing error" in error for error in data["errors"]) + + +def test_batch_job_with_failing_device( + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, +) -> None: + """Test that a batch job records per-device failures without aborting.""" + response = client.post( + "/api/v1/batch/remediation", + json={ + "device_configs": [ + { + "device_id": "good-router", + "platform": "cisco_ios", + "running_config": sample_cisco_ios_config, + "intended_config": sample_cisco_ios_intended_config, + }, + { + "device_id": "bad-router", + "platform": "cisco_ios", + "running_config": 123, + "intended_config": 456, + }, + ] + }, + ) + assert response.status_code == 200 + job_id = response.json()["job_id"] + + results_response = client.get(f"/api/v1/batch/jobs/{job_id}/results") + assert results_response.status_code == 200 + data = results_response.json() + statuses = {result["device_id"]: result["status"] for result in data["results"]} + assert statuses == {"good-router": "success", "bad-router": "failed"} + assert data["summary"]["failed_devices"] == 1 diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..c2dfebd --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,19 @@ +"""Tests for the application root endpoints.""" + +from fastapi.testclient import TestClient + + +def test_root(client: TestClient) -> None: + """Test the root endpoint.""" + response = client.get("/") + assert response.status_code == 200 + data = response.json() + assert data["message"] == "Hier-Config API" + assert data["docs"] == "/api/docs" + + +def test_health(client: TestClient) -> None: + """Test the health check endpoint.""" + response = client.get("/health") + assert response.status_code == 200 + assert response.json() == {"status": "healthy"} diff --git a/tests/test_reports.py b/tests/test_reports.py index e0d9d90..badf4d2 100644 --- a/tests/test_reports.py +++ b/tests/test_reports.py @@ -4,7 +4,9 @@ def test_create_report( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test creating a multi-device report.""" response = client.post( @@ -33,7 +35,9 @@ def test_create_report( def test_get_report_summary( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test getting report summary.""" # First, create a report @@ -63,7 +67,9 @@ def test_get_report_summary( def test_get_report_changes( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test getting detailed change analysis.""" # First, create a report @@ -93,7 +99,9 @@ def test_get_report_changes( def test_export_report_json( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test exporting report as JSON.""" # First, create a report @@ -120,7 +128,9 @@ def test_export_report_json( def test_export_report_csv( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test exporting report as CSV.""" # First, create a report @@ -144,3 +154,61 @@ def test_export_report_csv( response = client.get(f"/api/v1/reports/{report_id}/export?format=csv") assert response.status_code == 200 assert "Device ID" in response.text + + +def test_export_report_yaml( + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, +) -> None: + """Test exporting report as YAML.""" + # First, create a report + create_response = client.post( + "/api/v1/reports", + json={ + "remediations": [ + { + "device_id": "router1", + "platform": "cisco_ios", + "running_config": sample_cisco_ios_config, + "intended_config": sample_cisco_ios_intended_config, + } + ] + }, + ) + assert create_response.status_code == 200 + report_id = create_response.json()["report_id"] + + # Export as YAML + response = client.get(f"/api/v1/reports/{report_id}/export?format=yaml") + assert response.status_code == 200 + assert "total_devices" in response.text + + +def test_get_report_changes_with_tag_filter( + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, +) -> None: + """Test filtering report changes by tag.""" + # First, create a report + create_response = client.post( + "/api/v1/reports", + json={ + "remediations": [ + { + "device_id": "router1", + "platform": "cisco_ios", + "running_config": sample_cisco_ios_config, + "intended_config": sample_cisco_ios_intended_config, + } + ] + }, + ) + assert create_response.status_code == 200 + report_id = create_response.json()["report_id"] + + # No changes carry tags, so filtering by a tag yields no changes + response = client.get(f"/api/v1/reports/{report_id}/changes?tag=missing-tag") + assert response.status_code == 200 + assert response.json()["total_unique_changes"] == 0 diff --git a/tests/test_services.py b/tests/test_services.py new file mode 100644 index 0000000..0c0c1c8 --- /dev/null +++ b/tests/test_services.py @@ -0,0 +1,97 @@ +"""Unit tests for service and utility edge cases.""" + +import pytest +from fastapi.testclient import TestClient + +from hier_config_api.models.platform import PlatformRules +from hier_config_api.services.config_service import ConfigService +from hier_config_api.services.remediation_service import RemediationService +from hier_config_api.services.report_service import ReportService +from hier_config_api.utils.storage import InMemoryStorage + + +def test_platform_rules_defaults() -> None: + """Test that PlatformRules fields default to empty collections.""" + rules = PlatformRules(platform_name="cisco_ios") + assert not rules.ordering + assert not rules.negation_default_when + + +def test_storage_update_unknown_job() -> None: + """Test that updating an unknown job returns False.""" + storage = InMemoryStorage() + assert storage.update_job("unknown-id", {"status": "completed"}) is False + + +def test_storage_update_unknown_remediation() -> None: + """Test that updating an unknown remediation returns False.""" + storage = InMemoryStorage() + assert storage.update_remediation("unknown-id", {"tags": {}}) is False + + +def test_merge_configs_empty() -> None: + """Test that merging an empty config list returns an empty string.""" + assert not ConfigService.merge_configs("cisco_ios", []) + + +def test_merge_configs_single() -> None: + """Test that merging a single config returns it unchanged.""" + assert ConfigService.merge_configs("cisco_ios", ["hostname a"]) == "hostname a" + + +def test_filter_remediation_include_tags() -> None: + """Test filtering remediation lines by include tags.""" + filtered_config, summary = RemediationService.filter_remediation( + "line one\nline two", + {"0": ["keep"]}, + include_tags=["keep"], + ) + assert filtered_config == "line one" + assert summary.additions == 1 + + +def test_filter_remediation_exclude_tags() -> None: + """Test filtering remediation lines by exclude tags.""" + filtered_config, summary = RemediationService.filter_remediation( + "line one\nline two", + {"0": ["drop"]}, + exclude_tags=["drop"], + ) + assert filtered_config == "line two" + assert summary.additions == 1 + + +def test_export_report_unsupported_format() -> None: + """Test that exporting with an unsupported format raises ValueError.""" + with pytest.raises(ValueError, match="Unsupported format"): + ReportService.export_report({"devices": []}, format_type="xml") + + +def test_search_config_contains(client: TestClient) -> None: + """Test searching configuration lines with a contains rule.""" + response = client.post( + "/api/v1/configs/search", + json={ + "platform": "cisco_ios", + "config_text": "hostname router1\ninterface GigabitEthernet0/0", + "match_rules": {"contains": "Gigabit"}, + }, + ) + assert response.status_code == 200 + matches = response.json()["matches"] + assert "interface GigabitEthernet0/0" in matches + + +def test_search_config_startswith(client: TestClient) -> None: + """Test searching configuration lines with a startswith rule.""" + response = client.post( + "/api/v1/configs/search", + json={ + "platform": "cisco_ios", + "config_text": "hostname router1\ninterface GigabitEthernet0/0", + "match_rules": {"startswith": "hostname"}, + }, + ) + assert response.status_code == 200 + matches = response.json()["matches"] + assert "hostname router1" in matches From 2c437df33a807eb5e1b3b976f9162cc49b5fa968 Mon Sep 17 00:00:00 2001 From: jtdub Date: Thu, 6 Aug 2026 18:46:37 -0500 Subject: [PATCH 5/6] Add release workflows for versioning and PyPI publishing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Releases were previously manual with no defined process. Add two GitHub Actions workflows: Prepare Release (admin-gated workflow_dispatch that bumps the version with poetry, opens a release/vX.Y.Z PR against the dispatched branch, and creates a draft GitHub release) and Release (publishes to PyPI via poetry publish when a release is published — the published event, not created, so publishing a draft triggers it). Document the three-step process in the README. Co-Authored-By: Claude Fable 5 --- .github/workflows/prepare-release.yml | 92 +++++++++++++++++++++++++++ .github/workflows/release.yml | 29 +++++++++ README.md | 15 +++++ 3 files changed, 136 insertions(+) create mode 100644 .github/workflows/prepare-release.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..289b6cb --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,92 @@ +name: Prepare Release + +# The branch to release from is chosen via the standard "Run workflow" +# branch dropdown in the GitHub UI — this workflow runs against, and +# releases from, whatever ref it was dispatched on. +on: + workflow_dispatch: + inputs: + bump: + description: Version bump type + required: true + type: choice + options: + - major + - minor + - patch + - prerelease + +permissions: + contents: write + pull-requests: write + +jobs: + prepare: + runs-on: ubuntu-latest + steps: + - name: Require repository admin + uses: actions/github-script@v8 + with: + script: | + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.actor, + }); + if (data.permission !== 'admin') { + core.setFailed( + `@${context.actor} has '${data.permission}' permission; ` + + 'only repository admins may prepare a release.', + ); + } + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install poetry + uses: snok/install-poetry@v1 + with: + version: 2.3.1 + - name: Bump version + id: bump + run: | + poetry version "${{ github.event.inputs.bump }}" + version="$(poetry version --short)" + echo "version=${version}" >> "$GITHUB_OUTPUT" + case "$version" in + *a*|*b*|*rc*|*dev*) echo "prerelease=true" >> "$GITHUB_OUTPUT" ;; + *) echo "prerelease=false" >> "$GITHUB_OUTPUT" ;; + esac + - name: Commit bump to release branch + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "release/v${{ steps.bump.outputs.version }}" + git commit -am "chore(release): prepare ${{ steps.bump.outputs.version }}" + git push origin "release/v${{ steps.bump.outputs.version }}" + - name: Open release pull request + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr create \ + --base "${{ github.ref_name }}" \ + --head "release/v${{ steps.bump.outputs.version }}" \ + --title "chore(release): prepare ${{ steps.bump.outputs.version }}" \ + --body "Bumps the version to ${{ steps.bump.outputs.version }} (bump type: ${{ github.event.inputs.bump }}). + + Merge this PR first, then publish the draft release v${{ steps.bump.outputs.version }} to trigger the PyPI publish." + - name: Create draft GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + args=() + if [ "${{ steps.bump.outputs.prerelease }}" = "true" ]; then + args+=(--prerelease) + fi + gh release create "v${{ steps.bump.outputs.version }}" \ + --target "${{ github.ref_name }}" \ + --draft \ + --title "v${{ steps.bump.outputs.version }}" \ + --generate-notes \ + "${args[@]}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4c95aa7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +name: Release + +# Fires when a release is published, including a draft release being +# published (the `created` type never fires for drafts). +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + steps: + # On release events, checkout defaults to the release's tag ref. + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install poetry + uses: snok/install-poetry@v1 + with: + version: 2.3.1 + - name: Publish to PyPI + run: | + poetry config pypi-token.pypi "${{ secrets.PYPI_TOKEN }}" + poetry publish --build diff --git a/README.md b/README.md index caf339d..33ef5c7 100644 --- a/README.md +++ b/README.md @@ -280,6 +280,21 @@ poetry run pytest tests/test_configs.py::test_parse_config -v Never loosen the lint or coverage configuration to make a change pass. +### Releasing + +Releases are driven by two GitHub Actions workflows and require repository +admin permission: + +1. Run the **Prepare Release** workflow (`Actions` → `Prepare Release` → + `Run workflow`), choosing the branch to release from in the branch + dropdown and the version bump type (`major`, `minor`, `patch`, or + `prerelease`). The workflow bumps the version in `pyproject.toml`, opens + a `release/vX.Y.Z` pull request against the chosen branch, and creates a + draft GitHub release tagged `vX.Y.Z` targeting that branch. +2. Merge the release pull request. +3. Publish the draft release. Publishing fires the **Release** workflow, + which builds the package and uploads it to PyPI automatically. + ## Supported Platforms - Cisco IOS (`cisco_ios`) From b588ebf0b49578edffbe82cff5837ffbad732275 Mon Sep 17 00:00:00 2001 From: jtdub Date: Mon, 17 Aug 2026 19:57:20 -0500 Subject: [PATCH 6/6] Fix Read the Docs build, add Docker dev environment and standards sync Read the Docs produced no usable build. The config ran `poetry install` without pointing VIRTUAL_ENV at the environment Read the Docs runs mkdocs from, so Poetry created its own virtualenv and mkdocs was never installed where the build could find it. It also declared a second, conflicting install path via docs/requirements.txt. Point the install at $READTHEDOCS_VIRTUALENV_PATH and drop the requirements file, leaving pyproject.toml as the single source of docs dependencies. The package itself must be installed because the mkdocstrings reference pages import it, so the simpler requirements-only approach used by hier_config does not work here. Add a Docker development environment driven by invoke tasks, and adopt the shared standards published by netdevops/hier_config: build.py, sync_standards.py, .yamllint.yml, and .dockerignore now come from there via the .standards.yml manifest, with hier_config rewritten to hier_config_api as they are fetched. A scheduled workflow applies the sync weekly and opens a PR so CI validates the update before it merges. Relax the ruff pin from <0.15 to ^0.16 to match the canonical repository. The old pin was the reason scripts/build.py had diverged: ruff 0.14 does not understand the `# ruff: ignore[...]` directive the canonical file uses. On 0.16 that directive is not merely accepted but required, so the existing `# noqa` comments were converted. Add pytest-profiling for the --profile option the canonical build.py exposes. With those aligned, `invoke sync-standards` reports zero drift. Co-Authored-By: Claude Opus 5 (1M context) --- .dockerignore | 16 ++ .github/workflows/sync-standards.yml | 39 +++++ .readthedocs.yaml | 19 +-- .standards.yml | 20 +++ Dockerfile | 42 +++++ README.md | 23 +++ docker-compose.yml | 24 +++ docs/guides/deployment.md | 47 ++---- docs/guides/development.md | 63 ++++++++ docs/requirements.txt | 7 - hier_config_api/main.py | 2 +- hier_config_api/services/platform_service.py | 4 +- poetry.lock | 82 +++++++--- pyproject.toml | 7 +- scripts/build.py | 21 ++- scripts/sync_standards.py | 156 +++++++++++++++++++ tasks.py | 78 ++++++++++ 17 files changed, 560 insertions(+), 90 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/sync-standards.yml create mode 100644 .standards.yml create mode 100644 Dockerfile create mode 100644 docker-compose.yml delete mode 100644 docs/requirements.txt create mode 100755 scripts/sync_standards.py create mode 100644 tasks.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c349249 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.git +.github +.claude +.venv +__pycache__ +*.pyc +.pytest_cache +.mypy_cache +.ruff_cache +.coverage +htmlcov +site +dist +.dockerignore +Dockerfile +docker-compose.yml diff --git a/.github/workflows/sync-standards.yml b/.github/workflows/sync-standards.yml new file mode 100644 index 0000000..bed4f74 --- /dev/null +++ b/.github/workflows/sync-standards.yml @@ -0,0 +1,39 @@ +name: hier-config-api sync standards + +on: + schedule: + # Weekly, Monday 06:00 UTC + - cron: "0 6 * * 1" + workflow_dispatch: + +jobs: + sync: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Install poetry + uses: snok/install-poetry@v1 + with: + version: 2.3.1 + - name: Apply canonical standards + run: | + poetry install --no-interaction + poetry run python scripts/sync_standards.py apply + - name: Open pull request if standards drifted + uses: peter-evans/create-pull-request@v7 + with: + branch: sync-standards + base: develop + title: Sync development standards from netdevops/hier_config + commit-message: Sync development standards from netdevops/hier_config + body: > + Automated update of shared development-standard files from the + canonical repository declared in `.standards.yml`. Review CI + results and reconcile any incompatibilities before merging. diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 5503c1b..22f32df 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,12 +1,10 @@ # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details -# Required version: 2 -# Set the OS, Python version and other tools you might need build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: python: "3.12" jobs: @@ -14,18 +12,11 @@ build: # Install poetry - pip install poetry post_install: - # Install dependencies with poetry - - poetry install --with dev + # Install the project and all dependency groups into the Read the Docs + # virtualenv (VIRTUAL_ENV must point at it, otherwise poetry installs + # into its own virtualenv and mkdocs is not found at build time) + - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --with dev # Build documentation with MkDocs mkdocs: configuration: mkdocs.yml - -# Optional but recommended, declare the Python requirements required -# to build your documentation -# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -python: - install: - - method: pip - path: . - - requirements: docs/requirements.txt diff --git a/.standards.yml b/.standards.yml new file mode 100644 index 0000000..885a206 --- /dev/null +++ b/.standards.yml @@ -0,0 +1,20 @@ +# Shared development standards for netdevops hier-config projects. +# +# The files listed below are owned by the canonical repository and kept in +# sync across all hier-config projects. Use `invoke sync-standards` (or +# `poetry run python scripts/sync_standards.py check|apply`) to compare the +# local copies against the canonical versions and pull in upstream changes. +source: + repo: netdevops/hier_config + ref: master + +# Whole-word replacements applied to fetched file contents so package +# references match this project. +substitutions: + hier_config: hier_config_api + +files: + - scripts/build.py + - scripts/sync_standards.py + - .yamllint.yml + - .dockerignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..664e451 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +FROM python:3.12-slim AS base + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + POETRY_NO_INTERACTION=1 \ + POETRY_VIRTUALENVS_CREATE=false + +RUN pip install --no-cache-dir "poetry>=2.0,<3.0" + +WORKDIR /app + +# README.md is required because pyproject.toml declares it as the readme +COPY pyproject.toml poetry.lock README.md ./ + + +# Development image: full toolchain; source is bind-mounted by docker compose +FROM base AS development + +RUN poetry install --no-root --with dev + +COPY . . + +RUN poetry install --with dev + +EXPOSE 8000 + +CMD ["uvicorn", "hier_config_api.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + + +# Production image: runtime dependencies only +FROM base AS production + +RUN poetry install --no-root --only main + +COPY hier_config_api ./hier_config_api + +RUN poetry install --only main + +EXPOSE 8000 + +CMD ["uvicorn", "hier_config_api.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/README.md b/README.md index 33ef5c7..044e707 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,29 @@ poetry run python scripts/build.py lint --fix poetry run python scripts/build.py pytest --coverage ``` +### Docker Development Environment + +A Docker-based development environment, driven by +[invoke](https://www.pyinvoke.org/) tasks, is available as an alternative to a +local Poetry setup: + +```bash +invoke build # build the development image +invoke serve # API with hot reload at http://localhost:8000 +invoke docs # docs with live reload at http://localhost:8001 +invoke pytest # tests inside the container (--coverage for the gate) +invoke lint # linters inside the container (--fix for auto-fixes) +invoke lint-and-test # full suite, same as CI +invoke cli # shell inside the container +invoke sync-standards # check drift against canonical netdevops standards +invoke destroy # tear down containers +``` + +Shared development standards (lint/typing/test tooling, YAML style) are owned +by [hier_config](https://github.com/netdevops/hier_config) and declared in +`.standards.yml`; `invoke sync-standards --apply` pulls in the canonical +versions, and a weekly workflow opens a PR when they change. + ### Running Tests ```bash diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7078c2f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +services: + api: + build: + context: . + target: development + ports: + - "8000:8000" + volumes: + - .:/app + command: > + uvicorn hier_config_api.main:app --host 0.0.0.0 --port 8000 --reload + + # Live-reloading documentation server; opt in with: docker compose --profile docs up + docs: + build: + context: . + target: development + profiles: + - docs + ports: + - "8001:8001" + volumes: + - .:/app + command: mkdocs serve --dev-addr 0.0.0.0:8001 diff --git a/docs/guides/deployment.md b/docs/guides/deployment.md index 81d6710..c8255ef 100644 --- a/docs/guides/deployment.md +++ b/docs/guides/deployment.md @@ -76,38 +76,15 @@ sudo certbot --nginx -d api.example.com ### Dockerfile -Create `Dockerfile`: - -```dockerfile -FROM python:3.11-slim - -WORKDIR /app - -# Install Poetry -RUN pip install poetry==2.3.1 - -# Copy dependency files -COPY pyproject.toml poetry.lock ./ - -# Install dependencies -RUN poetry config virtualenvs.create false \ - && poetry install --no-interaction --no-ansi --no-root --only main - -# Copy application -COPY hier_config_api ./hier_config_api - -# Expose port -EXPOSE 8000 - -# Run application -CMD ["uvicorn", "hier_config_api.main:app", "--host", "0.0.0.0", "--port", "8000"] -``` +The repository ships a multi-stage `Dockerfile`. The `production` target +installs only runtime dependencies; the `development` target (used by +`docker-compose.yml` for local development) adds the full toolchain. ### Build and Run ```bash -# Build image -docker build -t hier-config-api:latest . +# Build the production image +docker build --target production -t hier-config-api:latest . # Run container docker run -d \ @@ -118,26 +95,26 @@ docker run -d \ ### Docker Compose -Create `docker-compose.yml`: +The `docker-compose.yml` in the repository is tuned for development (hot +reload, bind-mounted source). For a production deployment, use a compose file +that targets the production image: ```yaml -version: '3.8' - services: api: - build: . + build: + context: . + target: production ports: - "8000:8000" restart: always - environment: - - LOG_LEVEL=info command: uvicorn hier_config_api.main:app --host 0.0.0.0 --port 8000 --workers 4 ``` Run: ```bash -docker-compose up -d +docker compose up -d ``` ## Kubernetes Deployment diff --git a/docs/guides/development.md b/docs/guides/development.md index 1b882c8..7f0f5bf 100644 --- a/docs/guides/development.md +++ b/docs/guides/development.md @@ -24,6 +24,69 @@ poetry install poetry shell ``` +### Docker + +As an alternative to a local Poetry environment, the repository ships a Docker +development environment driven by [invoke](https://www.pyinvoke.org/) tasks +(`tasks.py`). The `development` image target installs the full toolchain, and +`docker-compose.yml` bind-mounts the repository so the API server hot-reloads +on changes: + +```bash +# Build the development image (rerun after dependency changes) +invoke build + +# Start the API with hot reload at http://localhost:8000 +invoke serve + +# Serve the documentation with live reload at http://localhost:8001 +invoke docs + +# Run tests and linters inside the container +invoke pytest # add --coverage for the 95% coverage gate +invoke lint # add --fix to apply auto-fixes +invoke lint-and-test # full suite, same as CI + +# Open a shell inside the container +invoke cli + +# Tear everything down +invoke destroy +``` + +The raw `docker compose` commands work as well; the invoke tasks are thin +wrappers around them. + +## Common Development Standards + +All netdevops hier-config projects share a common development model: the same +lint/typing/test tooling, YAML style, Docker development environment, and +invoke task names. + +The canonical copies live in +[netdevops/hier_config](https://github.com/netdevops/hier_config) — see its +[Shared Development Standards](https://netdevops.github.io/hier_config/dev/shared-standards/) +guide for the full model. This repository declares what it pulls in via the +`.standards.yml` manifest: `scripts/build.py`, `scripts/sync_standards.py`, +`.yamllint.yml`, and `.dockerignore`. Package references are rewritten +(`hier_config` → `hier_config_api`) as files are fetched. + +Do not edit those files directly — the next sync overwrites them. Change them +in `hier_config` instead. To compare the local copies against the canonical +versions: + +```bash +# Report drift (exits non-zero when local files differ from canonical) +invoke sync-standards + +# Pull the canonical versions into the local files +invoke sync-standards --apply +``` + +A scheduled GitHub Actions workflow (`sync-standards.yml`) runs the sync +weekly and opens a pull request when the canonical standards change, so CI +validates the update before it merges. + ## Project Structure ``` diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index ab222c0..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Documentation build requirements for ReadTheDocs -mkdocs>=1.6.1,<2.0.0 -mkdocs-material>=9.7.1,<10.0.0 -mkdocstrings[python]>=1.0.2,<2.0.0 -mkdocs-gen-files>=0.6.0,<0.7.0 -mkdocs-literate-nav>=0.6.2,<0.7.0 -mkdocs-section-index>=0.3.10,<0.4.0 diff --git a/hier_config_api/main.py b/hier_config_api/main.py index 798fc07..64bf77e 100644 --- a/hier_config_api/main.py +++ b/hier_config_api/main.py @@ -52,4 +52,4 @@ async def health() -> dict[str, str]: # Dev/container entry point: binding all interfaces is intentional so the # API is reachable from outside the container. - uvicorn.run(app, host="0.0.0.0", port=8000) # noqa: S104 + uvicorn.run(app, host="0.0.0.0", port=8000) # ruff: ignore[hardcoded-bind-all-interfaces] diff --git a/hier_config_api/services/platform_service.py b/hier_config_api/services/platform_service.py index 883c614..be92e7e 100644 --- a/hier_config_api/services/platform_service.py +++ b/hier_config_api/services/platform_service.py @@ -89,7 +89,7 @@ def validate_config(platform: str, config_text: str) -> dict[str, Any]: HConfig.from_text(platform_enum, config_text) # A validation endpoint must convert any parsing failure into a # validation error rather than propagate it as a server error. - except Exception as exc: # noqa: BLE001 # pylint: disable=broad-exception-caught + except Exception as exc: # ruff: ignore[blind-except] # pylint: disable=broad-exception-caught errors.append(f"Configuration parsing error: {exc!s}") is_valid = False else: @@ -152,7 +152,7 @@ def _process_device(device_config: dict[str, Any]) -> tuple[dict[str, Any], bool result = PlatformService._remediate_device(device_config) # Batch jobs record per-device failures instead of aborting the job, # so any processing error must be captured here. - except Exception as exc: # noqa: BLE001 # pylint: disable=broad-exception-caught + except Exception as exc: # ruff: ignore[blind-except] # pylint: disable=broad-exception-caught return ( { "device_id": device_config.get("device_id"), diff --git a/poetry.lock b/poetry.lock index 1557d5d..5ba35aa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -488,6 +488,18 @@ python-dateutil = ">=2.8.1" [package.extras] dev = ["flake8", "markdown", "twine", "wheel"] +[[package]] +name = "gprof2dot" +version = "2025.4.14" +description = "Generate a dot graph from the output of several profilers." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "gprof2dot-2025.4.14-py3-none-any.whl", hash = "sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e"}, + {file = "gprof2dot-2025.4.14.tar.gz", hash = "sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce"}, +] + [[package]] name = "griffelib" version = "2.0.0" @@ -656,6 +668,18 @@ files = [ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] +[[package]] +name = "invoke" +version = "3.0.3" +description = "Pythonic task execution" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053"}, + {file = "invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c"}, +] + [[package]] name = "isort" version = "8.0.1" @@ -1661,6 +1685,23 @@ pytest = ">=7" [package.extras] testing = ["process-tests", "pytest-xdist", "virtualenv"] +[[package]] +name = "pytest-profiling" +version = "1.8.1" +description = "Profiling plugin for py.test" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "pytest-profiling-1.8.1.tar.gz", hash = "sha256:3f171fa69d5c82fa9aab76d66abd5f59da69135c37d6ae5bf7557f1b154cb08d"}, + {file = "pytest_profiling-1.8.1-py3-none-any.whl", hash = "sha256:3dd8713a96298b42d83de8f5951df3ada3e61b3e5d2a06956684175529e17aea"}, +] + +[package.dependencies] +gprof2dot = "*" +pytest = "*" +six = "*" + [[package]] name = "pytest-xdist" version = "3.8.0" @@ -1853,31 +1894,30 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.14.14" +version = "0.16.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed"}, - {file = "ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c"}, - {file = "ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66"}, - {file = "ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13"}, - {file = "ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412"}, - {file = "ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3"}, - {file = "ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b"}, - {file = "ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167"}, - {file = "ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd"}, - {file = "ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c"}, - {file = "ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b"}, + {file = "ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7"}, + {file = "ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081"}, + {file = "ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474"}, + {file = "ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da"}, + {file = "ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50"}, + {file = "ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506"}, + {file = "ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d"}, + {file = "ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a"}, + {file = "ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948"}, + {file = "ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a"}, + {file = "ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2"}, ] [[package]] @@ -2418,4 +2458,4 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "ruff", "sph [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "7d356e4592f8bad9bf49a75f074b5bd98a356537e0aa84bbc292e927bd220ada" +content-hash = "d6def5174ede90e4e2d205b0b0181e656c5741112e458c54385fca6afdb2788d" diff --git a/pyproject.toml b/pyproject.toml index bb86577..0c72ed1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,10 +37,13 @@ dev = [ "pytest (>=9.0.2,<10.0.0)", "pytest-cov (>=7.0.0,<8.0.0)", "pytest-xdist (>=3.6.1,<4.0.0)", - "ruff (>=0.14.14,<0.15.0)", + "ruff (>=0.16.1,<0.17.0)", "typer (>=0.15.1,<1.0.0)", "types-pyyaml (>=6.0.12.20250915,<7.0.0.0)", - "yamllint (>=1.35.1,<2.0.0)" + "yamllint (>=1.35.1,<2.0.0)", + "invoke (>=3.0.3,<4.0.0)", + "pyyaml (>=6.0.3,<7.0.0)", + "pytest-profiling (>=1.8.1,<2.0.0)" ] diff --git a/scripts/build.py b/scripts/build.py index 2a0f425..df5185c 100755 --- a/scripts/build.py +++ b/scripts/build.py @@ -2,7 +2,7 @@ from __future__ import annotations import os -import subprocess # noqa: S404 +import subprocess # ruff:ignore[suspicious-subprocess-import] import sys from concurrent.futures import ThreadPoolExecutor, as_completed from functools import cache @@ -83,12 +83,14 @@ def _ruff_check_command(*, fix: bool, unsafe_fixes: bool, statistics: bool) -> s @app.command() def pytest( *, + profile: bool = False, coverage: bool = True, threaded: bool = False, ) -> None: """Run pytest unittests.""" _run( _pytest_command( + profile=profile, coverage=coverage, threaded=threaded, ), @@ -98,10 +100,13 @@ def pytest( def _pytest_command( *, + profile: bool = False, coverage: bool = True, threaded: bool = False, ) -> str: command = "pytest" + if profile: + command += " --profile --profile-svg" if coverage: command += ( " --cov=hier_config_api --cov-fail-under=95 --cov-report=term-missing" @@ -204,17 +209,17 @@ def _run_commands_threaded(commands: tuple[str, ...]) -> NoReturn: ): command, return_code, output = future.result() if return_code: - print(output) # noqa: T201 + print(output) # ruff:ignore[print] return_codes[command] = return_code error_found = False for command, return_code in return_codes.items(): if return_code != 0: - print(f"{command.split()[0]} -> {return_code}") # noqa: T201 + print(f"{command.split()[0]} -> {return_code}") # ruff:ignore[print] error_found = True if error_found: sys.exit(1) - print("No issues found") # noqa: T201 + print("No issues found") # ruff:ignore[print] sys.exit() @@ -224,19 +229,19 @@ def _run( check: bool = True, environment: dict[str, str] | None = None, ) -> int: - print(f"\n======== {command} ========\n") # noqa: T201 + print(f"\n======== {command} ========\n") # ruff:ignore[print] my_env = os.environ.copy() if environment: my_env.update(environment) - result = subprocess.run(command.split(), check=False, env=my_env) # noqa: S603 + result = subprocess.run(command.split(), check=False, env=my_env) # ruff:ignore[subprocess-without-shell-equals-true] if check: sys.exit(result.returncode) return result.returncode def _run_for_thread(command: str) -> tuple[str, int, str]: - print(f"Running: {command}") # noqa: T201 - result = subprocess.run( # noqa: S603 + print(f"Running: {command}") # ruff:ignore[print] + result = subprocess.run( # ruff:ignore[subprocess-without-shell-equals-true] command.split(), check=False, capture_output=True, diff --git a/scripts/sync_standards.py b/scripts/sync_standards.py new file mode 100755 index 0000000..536c8f0 --- /dev/null +++ b/scripts/sync_standards.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +"""Sync shared development-standard files from the canonical netdevops repository. + +The `.standards.yml` manifest at the repository root declares the canonical +source repository and the files it owns. `check` reports drift between the +local copies and the canonical versions; `apply` overwrites the local copies +with the canonical versions. + +Package-name substitutions can push a canonical line past the formatter's +line-length limit, so Python files are re-formatted with `ruff format` after +substitution. Without this the synced file would never converge: `apply` +would write a file that `ruff format` immediately rewrites, and the next +`check` would report drift again. +""" + +from __future__ import annotations + +import difflib +import re +import shutil +import subprocess # ruff: ignore[suspicious-subprocess-import] +import sys +from http import HTTPStatus +from pathlib import Path + +import httpx +import yaml +from pydantic import BaseModel, Field +from typer import Typer + +app = Typer() + +_REPO_ROOT = Path(__file__).parent.parent +_MANIFEST_PATH = _REPO_ROOT / ".standards.yml" + + +class Source(BaseModel): + """Canonical repository holding the standard files.""" + + repo: str + ref: str + + +class Manifest(BaseModel): + """Parsed representation of the `.standards.yml` manifest.""" + + source: Source + substitutions: dict[str, str] = Field(default_factory=dict) + files: tuple[str, ...] + + +@app.callback() +def callback() -> None: + """Sync shared development standards from the canonical repository.""" + + +@app.command() +def check() -> None: + """Report drift between local files and the canonical standards.""" + if _sync(write=False): + sys.exit(1) + + +@app.command() +def apply() -> None: + """Overwrite local files with the canonical standards.""" + _sync(write=True) + + +def _sync(*, write: bool) -> list[str]: + manifest = _load_manifest() + drifted: list[str] = [] + for file_path in manifest.files: + canonical = _canonical_content(manifest, file_path) + if canonical is None: + source = manifest.source + print( # ruff: ignore[print] + f"{file_path}: not published on {source.repo}@{source.ref} — " + "the manifest lists a file the canonical source does not have yet", + ) + drifted.append(file_path) + continue + local_path = _REPO_ROOT / file_path + local = local_path.read_text(encoding="utf-8") if local_path.is_file() else None + if local == canonical: + print(f"{file_path}: in sync") # ruff: ignore[print] + continue + drifted.append(file_path) + if write: + local_path.parent.mkdir(parents=True, exist_ok=True) + local_path.write_text(canonical, encoding="utf-8") + print(f"{file_path}: updated from canonical") # ruff: ignore[print] + else: + print(f"{file_path}: drifted from canonical") # ruff: ignore[print] + _print_diff(file_path, local or "", canonical) + return drifted + + +def _load_manifest() -> Manifest: + data = yaml.safe_load(_MANIFEST_PATH.read_text(encoding="utf-8")) + return Manifest.model_validate(data) + + +def _fetch(source: Source, file_path: str) -> str | None: + """Return the canonical file contents, or None when the source lacks the file.""" + url = f"https://raw.githubusercontent.com/{source.repo}/{source.ref}/{file_path}" + response = httpx.get(url, timeout=30.0, follow_redirects=True) + if response.status_code == HTTPStatus.NOT_FOUND: + return None + response.raise_for_status() + return response.text + + +def _canonical_content(manifest: Manifest, file_path: str) -> str | None: + fetched = _fetch(manifest.source, file_path) + if fetched is None: + return None + content = _apply_substitutions(fetched, manifest.substitutions) + if file_path.endswith(".py"): + content = _ruff_format(content, file_path) + return content + + +def _apply_substitutions(content: str, substitutions: dict[str, str]) -> str: + for old, new in substitutions.items(): + content = re.sub(rf"\b{re.escape(old)}\b", new, content) + return content + + +def _ruff_format(content: str, file_path: str) -> str: + ruff = shutil.which("ruff") + if ruff is None: + message = "ruff is not installed; run this from the dev environment" + raise RuntimeError(message) + result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] + [ruff, "format", "--stdin-filename", file_path, "-"], + check=True, + capture_output=True, + text=True, + input=content, + ) + return result.stdout + + +def _print_diff(file_path: str, local: str, canonical: str) -> None: + diff = difflib.unified_diff( + local.splitlines(keepends=True), + canonical.splitlines(keepends=True), + fromfile=f"local/{file_path}", + tofile=f"canonical/{file_path}", + ) + sys.stdout.writelines(diff) + + +if __name__ == "__main__": + app() diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..0309591 --- /dev/null +++ b/tasks.py @@ -0,0 +1,78 @@ +"""Invoke tasks for the Docker development environment.""" + +from typing import TYPE_CHECKING + +from invoke.context import Context + +if TYPE_CHECKING: + from collections.abc import Callable + + # Typed stand-in for invoke's partially typed task decorator + def task(_func: Callable[..., None]) -> Callable[..., None]: ... + +else: + from invoke.tasks import task + + +@task +def build(context: Context) -> None: + """Build the Docker development image.""" + context.run("docker compose build", pty=True) + + +@task +def serve(context: Context) -> None: + """Run the API with hot reload at http://localhost:8000.""" + context.run("docker compose up api", pty=True) + + +@task +def docs(context: Context) -> None: + """Serve the documentation with live reload at http://localhost:8001.""" + context.run("docker compose --profile docs up docs", pty=True) + + +@task +def pytest(context: Context, *, coverage: bool = False) -> None: + """Run the test suite inside the development container.""" + command = "python scripts/build.py pytest --coverage" if coverage else "pytest" + context.run(f"docker compose run --rm api {command}", pty=True) + + +@task +def lint(context: Context, *, fix: bool = False) -> None: + """Run all linters and type checkers inside the development container.""" + context.run( + f"docker compose run --rm api python scripts/build.py lint{' --fix' if fix else ''}", + pty=True, + ) + + +@task +def lint_and_test(context: Context) -> None: + """Run the full lint + test suite (what CI runs) inside the development container.""" + context.run( + "docker compose run --rm api python scripts/build.py lint-and-test", pty=True + ) + + +@task +def cli(context: Context) -> None: + """Open a shell inside the development container.""" + context.run("docker compose run --rm api bash", pty=True) + + +@task +def sync_standards(context: Context, *, apply: bool = False) -> None: + """Check drift against the canonical netdevops standards (--apply to update).""" + action = "apply" if apply else "check" + context.run( + f"docker compose run --rm api python scripts/sync_standards.py {action}", + pty=True, + ) + + +@task +def destroy(context: Context) -> None: + """Stop and remove the development containers.""" + context.run("docker compose --profile docs down --remove-orphans", pty=True)