diff --git a/app_python/README.md b/app_python/README.md
index 1415a24a2a..dd63b7a64a 100644
--- a/app_python/README.md
+++ b/app_python/README.md
@@ -4,11 +4,11 @@
## Overview
-Small Flask web service that reports service metadata, system information, runtime uptime, and basic request details. Includes a simple health check endpoint for monitoring.
+Small Flask web service that reports service metadata, system information, runtime uptime, and basic request details. Includes health and Prometheus metrics endpoints for monitoring.
## Prerequisites
-- Python 3.13+
+- Python 3.14+
- Poetry
## Installation
@@ -54,6 +54,7 @@ Gunicorn access logs are emitted as JSON so Loki can parse request fields cleanl
- `GET /` - Service and system information
- `GET /health` - Health check
+- `GET /metrics` - Prometheus metrics exposition
## Configuration
diff --git a/app_python/docs/LAB08.md b/app_python/docs/LAB08.md
new file mode 100644
index 0000000000..1d947a2350
--- /dev/null
+++ b/app_python/docs/LAB08.md
@@ -0,0 +1,138 @@
+# LAB08 - Metrics and Monitoring (Task 1)
+
+## 1. Overview
+
+Prometheus instrumentation was added to the Flask service using `prometheus-client==0.23.1`.
+
+Implemented metrics:
+
+- `http_requests_total` counter with `method`, `endpoint`, and `status_code`
+- `http_request_duration_seconds` histogram with `method`, `endpoint`, and `status_code`
+- `http_requests_in_progress` gauge with `method` and `endpoint`
+- `devops_info_endpoint_calls_total` counter for application endpoint usage
+- `devops_info_system_info_duration_seconds` histogram for system-info collection latency
+
+Labeling choice:
+
+- Matched routes use normalized Flask rules such as `/`, `/health`, and `/metrics`
+- Unmatched requests are grouped under `endpoint="unmatched"` to keep label cardinality low
+- The in-progress gauge does not include `status_code` because that value does not exist until a response is produced
+
+## 2. Verification
+
+Install and run with the project-local Poetry binary:
+
+```bash
+cd app_python
+.venv/bin/poetry install --with dev
+.venv/bin/poetry run pytest
+.venv/bin/poetry run gunicorn --config gunicorn.conf.py src.main:app
+```
+
+Generate a few requests, then inspect metrics:
+
+```bash
+curl -fSsL http://127.0.0.1:5000/ | jq
+curl -fSsL http://127.0.0.1:5000/health | jq
+curl -fSsL http://127.0.0.1:5000/metrics
+```
+
+
+/metrics output
+
+```text
+$ curl -fSsL http://127.0.0.1:5000/metrics
+# HELP http_requests_total Total HTTP requests handled by the service.
+# TYPE http_requests_total counter
+http_requests_total{endpoint="/",method="GET",status_code="200"} 6.0
+http_requests_total{endpoint="/health",method="GET",status_code="200"} 6.0
+# HELP http_requests_created Total HTTP requests handled by the service.
+# TYPE http_requests_created gauge
+http_requests_created{endpoint="/",method="GET",status_code="200"} 1.7739616481696362e+09
+http_requests_created{endpoint="/health",method="GET",status_code="200"} 1.7739616482041702e+09
+# HELP http_request_duration_seconds HTTP request duration in seconds.
+# TYPE http_request_duration_seconds histogram
+http_request_duration_seconds_bucket{endpoint="/",le="0.005",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/",le="0.01",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/",le="0.025",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/",le="0.05",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/",le="0.075",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/",le="0.1",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/",le="0.25",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/",le="0.5",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/",le="0.75",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/",le="1.0",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/",le="2.5",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/",le="5.0",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/",le="7.5",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/",le="10.0",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/",le="+Inf",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_count{endpoint="/",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_sum{endpoint="/",method="GET",status_code="200"} 0.0015464909993170295
+http_request_duration_seconds_bucket{endpoint="/health",le="0.005",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/health",le="0.01",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/health",le="0.025",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/health",le="0.05",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/health",le="0.075",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/health",le="0.1",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/health",le="0.25",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/health",le="0.5",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/health",le="0.75",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/health",le="1.0",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/health",le="2.5",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/health",le="5.0",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/health",le="7.5",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/health",le="10.0",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_bucket{endpoint="/health",le="+Inf",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_count{endpoint="/health",method="GET",status_code="200"} 6.0
+http_request_duration_seconds_sum{endpoint="/health",method="GET",status_code="200"} 0.0019912700008717366
+# HELP http_request_duration_seconds_created HTTP request duration in seconds.
+# TYPE http_request_duration_seconds_created gauge
+http_request_duration_seconds_created{endpoint="/",method="GET",status_code="200"} 1.7739616481696527e+09
+http_request_duration_seconds_created{endpoint="/health",method="GET",status_code="200"} 1.7739616482041845e+09
+# HELP http_requests_in_progress HTTP requests currently being processed.
+# TYPE http_requests_in_progress gauge
+http_requests_in_progress{endpoint="/",method="GET"} 0.0
+http_requests_in_progress{endpoint="/health",method="GET"} 0.0
+http_requests_in_progress{endpoint="/metrics",method="GET"} 1.0
+# HELP devops_info_endpoint_calls_total Total calls to application endpoints.
+# TYPE devops_info_endpoint_calls_total counter
+devops_info_endpoint_calls_total{endpoint="/"} 6.0
+devops_info_endpoint_calls_total{endpoint="/health"} 6.0
+devops_info_endpoint_calls_total{endpoint="/metrics"} 1.0
+# HELP devops_info_endpoint_calls_created Total calls to application endpoints.
+# TYPE devops_info_endpoint_calls_created gauge
+devops_info_endpoint_calls_created{endpoint="/"} 1.773961648169205e+09
+devops_info_endpoint_calls_created{endpoint="/health"} 1.7739616482040732e+09
+devops_info_endpoint_calls_created{endpoint="/metrics"} 1.7739616631203315e+09
+# HELP devops_info_system_info_duration_seconds Time spent collecting system information.
+# TYPE devops_info_system_info_duration_seconds histogram
+devops_info_system_info_duration_seconds_bucket{le="0.005"} 6.0
+devops_info_system_info_duration_seconds_bucket{le="0.01"} 6.0
+devops_info_system_info_duration_seconds_bucket{le="0.025"} 6.0
+devops_info_system_info_duration_seconds_bucket{le="0.05"} 6.0
+devops_info_system_info_duration_seconds_bucket{le="0.075"} 6.0
+devops_info_system_info_duration_seconds_bucket{le="0.1"} 6.0
+devops_info_system_info_duration_seconds_bucket{le="0.25"} 6.0
+devops_info_system_info_duration_seconds_bucket{le="0.5"} 6.0
+devops_info_system_info_duration_seconds_bucket{le="0.75"} 6.0
+devops_info_system_info_duration_seconds_bucket{le="1.0"} 6.0
+devops_info_system_info_duration_seconds_bucket{le="2.5"} 6.0
+devops_info_system_info_duration_seconds_bucket{le="5.0"} 6.0
+devops_info_system_info_duration_seconds_bucket{le="7.5"} 6.0
+devops_info_system_info_duration_seconds_bucket{le="10.0"} 6.0
+devops_info_system_info_duration_seconds_bucket{le="+Inf"} 6.0
+devops_info_system_info_duration_seconds_count 6.0
+devops_info_system_info_duration_seconds_sum 0.00042895499973383266
+# HELP devops_info_system_info_duration_seconds_created Time spent collecting system information.
+# TYPE devops_info_system_info_duration_seconds_created gauge
+devops_info_system_info_duration_seconds_created 1.7739616389214125e+09
+```
+
+
+
+## 3. Notes
+
+- HTTP metrics are captured with Flask request hooks so 2xx, 4xx, and 5xx responses are all measured consistently.
+- Application-specific metrics are recorded in route handlers and around system-info collection.
+- Automated tests cover `/metrics` exposure plus label handling for `200`, `404`, and `500` responses.
diff --git a/app_python/poetry.lock b/app_python/poetry.lock
index e4918ed237..748bfe2685 100644
--- a/app_python/poetry.lock
+++ b/app_python/poetry.lock
@@ -14,137 +14,153 @@ files = [
[[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 = ["main"]
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.6"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7"
groups = ["main"]
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.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6"},
+ {file = "charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4"},
+ {file = "charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb"},
+ {file = "charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389"},
+ {file = "charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4"},
+ {file = "charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-win32.whl", hash = "sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae"},
+ {file = "charset_normalizer-3.4.6-cp38-cp38-win_amd64.whl", hash = "sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-win32.whl", hash = "sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-win_amd64.whl", hash = "sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8"},
+ {file = "charset_normalizer-3.4.6-cp39-cp39-win_arm64.whl", hash = "sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8"},
+ {file = "charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69"},
+ {file = "charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6"},
]
[[package]]
@@ -177,118 +193,118 @@ markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win
[[package]]
name = "coverage"
-version = "7.13.4"
+version = "7.13.5"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
- {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"},
+ {file = "coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5"},
+ {file = "coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf"},
+ {file = "coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8"},
+ {file = "coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4"},
+ {file = "coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d"},
+ {file = "coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930"},
+ {file = "coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d"},
+ {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40"},
+ {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878"},
+ {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400"},
+ {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0"},
+ {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0"},
+ {file = "coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58"},
+ {file = "coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e"},
+ {file = "coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d"},
+ {file = "coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587"},
+ {file = "coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642"},
+ {file = "coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b"},
+ {file = "coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686"},
+ {file = "coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743"},
+ {file = "coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75"},
+ {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209"},
+ {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a"},
+ {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e"},
+ {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd"},
+ {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8"},
+ {file = "coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf"},
+ {file = "coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9"},
+ {file = "coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028"},
+ {file = "coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01"},
+ {file = "coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422"},
+ {file = "coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f"},
+ {file = "coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5"},
+ {file = "coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376"},
+ {file = "coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256"},
+ {file = "coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c"},
+ {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5"},
+ {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09"},
+ {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9"},
+ {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf"},
+ {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c"},
+ {file = "coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf"},
+ {file = "coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810"},
+ {file = "coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de"},
+ {file = "coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1"},
+ {file = "coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3"},
+ {file = "coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26"},
+ {file = "coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3"},
+ {file = "coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b"},
+ {file = "coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a"},
+ {file = "coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969"},
+ {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161"},
+ {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15"},
+ {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1"},
+ {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6"},
+ {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17"},
+ {file = "coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85"},
+ {file = "coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b"},
+ {file = "coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664"},
+ {file = "coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d"},
+ {file = "coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0"},
+ {file = "coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806"},
+ {file = "coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3"},
+ {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9"},
+ {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd"},
+ {file = "coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606"},
+ {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e"},
+ {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0"},
+ {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87"},
+ {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479"},
+ {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2"},
+ {file = "coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a"},
+ {file = "coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819"},
+ {file = "coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911"},
+ {file = "coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"},
+ {file = "coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e"},
+ {file = "coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a"},
+ {file = "coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510"},
+ {file = "coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247"},
+ {file = "coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6"},
+ {file = "coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0"},
+ {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882"},
+ {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740"},
+ {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16"},
+ {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0"},
+ {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0"},
+ {file = "coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc"},
+ {file = "coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633"},
+ {file = "coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8"},
+ {file = "coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b"},
+ {file = "coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c"},
+ {file = "coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9"},
+ {file = "coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29"},
+ {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607"},
+ {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90"},
+ {file = "coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3"},
+ {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab"},
+ {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562"},
+ {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2"},
+ {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea"},
+ {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a"},
+ {file = "coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215"},
+ {file = "coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43"},
+ {file = "coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45"},
+ {file = "coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61"},
+ {file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"},
]
[package.extras]
@@ -313,14 +329,14 @@ pyflakes = ">=3.4.0,<3.5.0"
[[package]]
name = "flask"
-version = "3.1.2"
+version = "3.1.3"
description = "A simple framework for building complex web applications."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c"},
- {file = "flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87"},
+ {file = "flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c"},
+ {file = "flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb"},
]
[package.dependencies]
@@ -337,14 +353,14 @@ dotenv = ["python-dotenv"]
[[package]]
name = "gunicorn"
-version = "25.0.3"
+version = "25.1.0"
description = "WSGI HTTP Server for UNIX"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
- {file = "gunicorn-25.0.3-py3-none-any.whl", hash = "sha256:aca364c096c81ca11acd4cede0aaeea91ba76ca74e2c0d7f879154db9d890f35"},
- {file = "gunicorn-25.0.3.tar.gz", hash = "sha256:b53a7fff1a07b825b962af320554de44ae77a26abfa373711ff3f83d57d3506d"},
+ {file = "gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b"},
+ {file = "gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616"},
]
[package.dependencies]
@@ -569,6 +585,21 @@ files = [
dev = ["pre-commit", "tox"]
testing = ["coverage", "pytest", "pytest-benchmark"]
+[[package]]
+name = "prometheus-client"
+version = "0.23.1"
+description = "Python client for the Prometheus monitoring system."
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99"},
+ {file = "prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce"},
+]
+
+[package.extras]
+twisted = ["twisted"]
+
[[package]]
name = "pycodestyle"
version = "2.14.0"
@@ -692,14 +723,14 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
[[package]]
name = "werkzeug"
-version = "3.1.5"
+version = "3.1.6"
description = "The comprehensive WSGI web application library."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc"},
- {file = "werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67"},
+ {file = "werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131"},
+ {file = "werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25"},
]
[package.dependencies]
@@ -710,5 +741,5 @@ watchdog = ["watchdog (>=2.3)"]
[metadata]
lock-version = "2.1"
-python-versions = ">=3.13"
-content-hash = "5bcb333e951818ca4706d50bae307ab22a95462b6e393691b1a6d0992e4ffc41"
+python-versions = ">=3.14"
+content-hash = "978605bae0c54f50d967c46bf36e71be1c8baa8f5deab0c54397546aaa573a3a"
diff --git a/app_python/pyproject.toml b/app_python/pyproject.toml
index 1e8b7fba15..deb962795e 100644
--- a/app_python/pyproject.toml
+++ b/app_python/pyproject.toml
@@ -1,20 +1,18 @@
-[tool.poetry]
+[project]
name = "devops-info-service"
-version = "0.1.0"
+version = "1.8.0"
description = ""
-authors = ["LocalT0aster"]
+authors = [
+ {name = "LocalT0aster",email = "90502400+LocalT0aster@users.noreply.github.com"}
+]
readme = "README.md"
-packages = [{ include = "src" }]
-
-[tool.poetry.dependencies]
-python = ">=3.13"
-flask = ">=3.1.2,<4.0.0"
-requests = ">=2.32.5,<3.0.0"
-gunicorn = "^25.0.3"
-
-[build-system]
-requires = ["poetry-core>=2.0.0,<3.0.0"]
-build-backend = "poetry.core.masonry.api"
+requires-python = ">=3.14"
+dependencies = [
+ "flask (>=3.1.3,<4.0.0)",
+ "requests (>=2.32.5,<3.0.0)",
+ "gunicorn (>=25.1.0,<26.0.0)",
+ "prometheus-client (==0.23.1)"
+]
[dependency-groups]
dev = [
@@ -23,3 +21,10 @@ dev = [
"flake8 (>=7.3.0,<8.0.0)",
"pep8-naming (>=0.15.1,<0.16.0)"
]
+
+[tool.poetry]
+packages = [{ include = "src" }]
+
+[build-system]
+requires = ["poetry-core>=2.0.0,<3.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/app_python/src/metrics.py b/app_python/src/metrics.py
new file mode 100644
index 0000000000..1441b3f8f1
--- /dev/null
+++ b/app_python/src/metrics.py
@@ -0,0 +1,119 @@
+"""Prometheus metrics and Flask request instrumentation."""
+
+from time import perf_counter
+
+from flask import Response, g, request
+from prometheus_client import (
+ CONTENT_TYPE_LATEST,
+ CollectorRegistry,
+ Counter,
+ Gauge,
+ Histogram,
+ generate_latest,
+)
+
+try:
+ from .flask_instance import app
+except ImportError: # pragma: no cover - allows `python src/main.py`
+ from flask_instance import app
+
+METRICS_REGISTRY = CollectorRegistry()
+
+HTTP_REQUESTS_TOTAL = Counter(
+ "http_requests_total",
+ "Total HTTP requests handled by the service.",
+ ["method", "endpoint", "status_code"],
+ registry=METRICS_REGISTRY,
+)
+HTTP_REQUEST_DURATION_SECONDS = Histogram(
+ "http_request_duration_seconds",
+ "HTTP request duration in seconds.",
+ ["method", "endpoint", "status_code"],
+ registry=METRICS_REGISTRY,
+)
+HTTP_REQUESTS_IN_PROGRESS = Gauge(
+ "http_requests_in_progress",
+ "HTTP requests currently being processed.",
+ ["method", "endpoint"],
+ registry=METRICS_REGISTRY,
+)
+DEVOPS_INFO_ENDPOINT_CALLS_TOTAL = Counter(
+ "devops_info_endpoint_calls_total",
+ "Total calls to application endpoints.",
+ ["endpoint"],
+ registry=METRICS_REGISTRY,
+)
+DEVOPS_INFO_SYSTEM_INFO_DURATION_SECONDS = Histogram(
+ "devops_info_system_info_duration_seconds",
+ "Time spent collecting system information.",
+ registry=METRICS_REGISTRY,
+)
+
+
+def normalize_endpoint_label() -> str:
+ """Return a low-cardinality endpoint label for the current request."""
+ rule = getattr(request, "url_rule", None)
+ if rule is not None:
+ return rule.rule
+ return "unmatched"
+
+
+def record_endpoint_call(endpoint: str) -> None:
+ """Increment the app-specific endpoint usage counter."""
+ DEVOPS_INFO_ENDPOINT_CALLS_TOTAL.labels(endpoint=endpoint).inc()
+
+
+def generate_metrics_response() -> Response:
+ """Return the current Prometheus exposition payload."""
+ return Response(
+ generate_latest(METRICS_REGISTRY),
+ content_type=CONTENT_TYPE_LATEST,
+ )
+
+
+@app.before_request
+def start_http_request_metrics() -> None:
+ """Capture request start time and increase the in-flight gauge."""
+ endpoint = normalize_endpoint_label()
+ g.metrics_method = request.method
+ g.metrics_endpoint = endpoint
+ g.metrics_start_time = perf_counter()
+ g.metrics_in_progress = True
+ HTTP_REQUESTS_IN_PROGRESS.labels(
+ method=request.method,
+ endpoint=endpoint,
+ ).inc()
+
+
+@app.after_request
+def record_http_request_metrics(response: Response) -> Response:
+ """Persist request counter and latency observations."""
+ method = getattr(g, "metrics_method", request.method)
+ endpoint = getattr(g, "metrics_endpoint", normalize_endpoint_label())
+ start_time = getattr(g, "metrics_start_time", None)
+ if start_time is None:
+ return response
+
+ labels = {
+ "method": method,
+ "endpoint": endpoint,
+ "status_code": str(response.status_code),
+ }
+ HTTP_REQUESTS_TOTAL.labels(**labels).inc()
+ HTTP_REQUEST_DURATION_SECONDS.labels(**labels).observe(
+ perf_counter() - start_time
+ )
+ return response
+
+
+@app.teardown_request
+def finish_http_request_metrics(error: BaseException | None) -> None: # noqa: ARG001
+ """Decrease the in-flight gauge after the request finishes."""
+ if not getattr(g, "metrics_in_progress", False):
+ return
+
+ HTTP_REQUESTS_IN_PROGRESS.labels(
+ method=g.metrics_method,
+ endpoint=g.metrics_endpoint,
+ ).dec()
+ g.metrics_in_progress = False
diff --git a/app_python/src/router.py b/app_python/src/router.py
index b2590de495..294be6246d 100644
--- a/app_python/src/router.py
+++ b/app_python/src/router.py
@@ -12,10 +12,20 @@
try:
from .flask_instance import START_TIME, app, logger
+ from .metrics import (
+ DEVOPS_INFO_SYSTEM_INFO_DURATION_SECONDS,
+ generate_metrics_response,
+ record_endpoint_call,
+ )
except ImportError: # pragma: no cover - allows `python src/main.py`
from flask_instance import START_TIME, app, logger
+ from metrics import (
+ DEVOPS_INFO_SYSTEM_INFO_DURATION_SECONDS,
+ generate_metrics_response,
+ record_endpoint_call,
+ )
-__version__ = "1.7.0"
+__version__ = "1.8.0"
def get_service_info() -> dict[str, str]:
@@ -28,6 +38,7 @@ def get_service_info() -> dict[str, str]:
}
+@DEVOPS_INFO_SYSTEM_INFO_DURATION_SECONDS.time()
def get_platform_info() -> dict[str, str | int]:
"""Collect system information."""
@@ -120,6 +131,7 @@ def list_routes() -> list[dict[str, str]]:
@app.route("/")
def index():
"""Service information."""
+ record_endpoint_call("/")
return jsonify(
{
"service": get_service_info(),
@@ -134,6 +146,7 @@ def index():
@app.route("/health")
def health():
"""Health check."""
+ record_endpoint_call("/health")
return jsonify(
{
"status": "healthy",
@@ -143,6 +156,13 @@ def health():
)
+@app.route("/metrics")
+def metrics():
+ """Prometheus metrics."""
+ record_endpoint_call("/metrics")
+ return generate_metrics_response()
+
+
@app.errorhandler(404)
def not_found(error): # noqa: ARG001
"""Return a JSON 404 payload."""
diff --git a/app_python/tests/test_endpoints.py b/app_python/tests/test_endpoints.py
index d0aca40338..97c217b476 100644
--- a/app_python/tests/test_endpoints.py
+++ b/app_python/tests/test_endpoints.py
@@ -62,6 +62,7 @@ def test_index_returns_expected_json_structure_and_types(client):
route_index = {(endpoint["method"], endpoint["path"]) for endpoint in endpoints}
assert ("GET", "/") in route_index
assert ("GET", "/health") in route_index
+ assert ("GET", "/metrics") in route_index
def test_health_returns_expected_json_structure_and_types(client):
diff --git a/app_python/tests/test_metrics.py b/app_python/tests/test_metrics.py
new file mode 100644
index 0000000000..ac7da3ad0b
--- /dev/null
+++ b/app_python/tests/test_metrics.py
@@ -0,0 +1,106 @@
+"""Tests for Prometheus metrics exposure and labels."""
+
+from collections.abc import Mapping
+
+from prometheus_client.parser import text_string_to_metric_families
+
+import src.router as router
+
+
+def _raise_runtime_error() -> None:
+ raise RuntimeError("simulated failure")
+
+
+def _metric_value(
+ metrics_text: str,
+ sample_name: str,
+ labels: Mapping[str, str] | None = None,
+) -> float | None:
+ expected_labels = labels or {}
+
+ for family in text_string_to_metric_families(metrics_text):
+ for sample in family.samples:
+ if sample.name != sample_name:
+ continue
+ if all(
+ sample.labels.get(key) == value
+ for key, value in expected_labels.items()
+ ):
+ return float(sample.value)
+ return None
+
+
+def _metrics_text(client) -> str:
+ response = client.get("/metrics")
+ assert response.status_code == 200
+ return response.get_data(as_text=True)
+
+
+def test_metrics_endpoint_exposes_http_and_application_metrics(client):
+ """Metrics endpoint should expose HTTP RED data and app-specific metrics."""
+ client.get("/")
+ client.get("/health")
+ client.get("/does-not-exist")
+
+ response = client.get("/metrics")
+ metrics_text = response.get_data(as_text=True)
+
+ assert response.status_code == 200
+ assert response.content_type.startswith("text/plain")
+
+ root_total = _metric_value(
+ metrics_text,
+ "http_requests_total",
+ {"method": "GET", "endpoint": "/", "status_code": "200"},
+ )
+ health_total = _metric_value(
+ metrics_text,
+ "http_requests_total",
+ {"method": "GET", "endpoint": "/health", "status_code": "200"},
+ )
+ unmatched_total = _metric_value(
+ metrics_text,
+ "http_requests_total",
+ {"method": "GET", "endpoint": "unmatched", "status_code": "404"},
+ )
+ root_duration_count = _metric_value(
+ metrics_text,
+ "http_request_duration_seconds_count",
+ {"method": "GET", "endpoint": "/", "status_code": "200"},
+ )
+ root_in_progress = _metric_value(
+ metrics_text,
+ "http_requests_in_progress",
+ {"method": "GET", "endpoint": "/"},
+ )
+ endpoint_calls = _metric_value(
+ metrics_text,
+ "devops_info_endpoint_calls_total",
+ {"endpoint": "/"},
+ )
+ system_info_count = _metric_value(
+ metrics_text,
+ "devops_info_system_info_duration_seconds_count",
+ )
+
+ assert root_total is not None and root_total >= 1.0
+ assert health_total is not None and health_total >= 1.0
+ assert unmatched_total is not None and unmatched_total >= 1.0
+ assert root_duration_count is not None and root_duration_count >= 1.0
+ assert root_in_progress == 0.0
+ assert endpoint_calls is not None and endpoint_calls >= 1.0
+ assert system_info_count is not None and system_info_count >= 1.0
+
+
+def test_metrics_count_internal_server_errors_with_status_labels(client, monkeypatch):
+ """Failed requests should still be counted with a 500 status code label."""
+ labels = {"method": "GET", "endpoint": "/", "status_code": "500"}
+ before = _metric_value(_metrics_text(client), "http_requests_total", labels) or 0.0
+
+ monkeypatch.setattr(router, "get_platform_info", _raise_runtime_error)
+
+ response = client.get("/")
+ after = _metric_value(_metrics_text(client), "http_requests_total", labels)
+
+ assert response.status_code == 500
+ assert after == before + 1.0
diff --git a/monitoring/docker-compose.yml b/monitoring/docker-compose.yml
index ac36c52b2e..8b155f0c50 100644
--- a/monitoring/docker-compose.yml
+++ b/monitoring/docker-compose.yml
@@ -43,6 +43,17 @@ services:
- promtail-data:/run/promtail
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
+ healthcheck:
+ test:
+ - CMD-SHELL
+ - >-
+ bash -lc 'exec 3<>/dev/tcp/127.0.0.1/9080
+ && printf "GET /ready HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n" >&3
+ && grep -q "200 OK" <&3'
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 10s
deploy:
resources:
limits:
@@ -81,7 +92,7 @@ services:
deploy:
resources:
limits:
- cpus: "1.0"
+ cpus: "0.5"
memory: 512M
reservations:
cpus: "0.25"
@@ -90,12 +101,39 @@ services:
- monitoring
restart: unless-stopped
+ prometheus:
+ image: prom/prometheus:v3.9.0
+ command:
+ - --config.file=/etc/prometheus/prometheus.yml
+ - --storage.tsdb.retention.time=15d
+ - --storage.tsdb.retention.size=10GB
+ ports:
+ - "9090:9090"
+ volumes:
+ - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
+ - prometheus-data:/prometheus
+ healthcheck:
+ test:
+ - CMD-SHELL
+ - wget --no-verbose --tries=1 --spider http://127.0.0.1:9090/-/healthy || exit 1
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 10s
+ deploy:
+ resources:
+ limits:
+ cpus: "1.0"
+ memory: 1G
+ reservations:
+ cpus: "0.25"
+ memory: 256M
+ networks:
+ - monitoring
+ restart: unless-stopped
+
app-python:
- image: localt0aster/devops-app-py:1.7.9a42ee5
- # Re-enable local builds if Docker networking breaks behind the tun/VPN setup.
- # build:
- # context: ../app_python
- # network: host
+ image: localt0aster/devops-app-py:1.8.806c77e
environment:
HOST: "0.0.0.0"
PORT: "8000"
@@ -182,6 +220,7 @@ volumes:
loki-data:
promtail-data:
grafana-data:
+ prometheus-data:
networks:
monitoring:
diff --git a/monitoring/docs/LAB08.md b/monitoring/docs/LAB08.md
new file mode 100644
index 0000000000..053664e40b
--- /dev/null
+++ b/monitoring/docs/LAB08.md
@@ -0,0 +1,612 @@
+# LAB08 - Metrics and Monitoring
+
+## Task 1 — Application Instrumentation
+
+### Metrics Added
+
+The Python service was instrumented with Prometheus metrics in `app_python/src/metrics.py` and `app_python/src/router.py`.
+
+- `http_requests_total`
+ - Counter for total HTTP requests
+ - Labels: `method`, `endpoint`, `status_code`
+- `http_request_duration_seconds`
+ - Histogram for request latency
+ - Labels: `method`, `endpoint`, `status_code`
+- `http_requests_in_progress`
+ - Gauge for active in-flight requests
+ - Labels: `method`, `endpoint`
+- `devops_info_endpoint_calls_total`
+ - Counter for endpoint usage inside the app
+ - Labels: `endpoint`
+- `devops_info_system_info_duration_seconds`
+ - Histogram for the platform-info collection path
+ - No labels
+
+### Why These Metrics
+
+The metric set follows the RED method for a request-driven service.
+
+- Rate: `http_requests_total`
+- Errors: `http_requests_total{status_code=~"5.."}`
+- Duration: `http_request_duration_seconds`
+
+Two extra business-level metrics were added so the dashboard shows something specific to this service rather than only generic HTTP traffic.
+
+### Labeling Choices
+
+- Matched routes use normalized endpoint labels such as `/`, `/health`, and `/metrics`
+- Unknown routes are grouped as `endpoint="unmatched"` to keep cardinality low
+- The implementation uses `status_code`, not `status`
+
+That last point matters because some of the lab examples use `status`, but this service exports `status_code`.
+
+### Metrics vs Logs
+
+Metrics and logs solve different problems.
+
+- Metrics answer trend questions quickly: request rate, latency, error rate, uptime
+- Logs answer forensic questions: which request failed, what stack trace occurred, what client sent the request
+- Lab 7 kept Loki + Promtail for logs
+- Lab 8 adds Prometheus for numeric time-series monitoring
+
+In practice:
+
+- Use metrics for dashboards, SLO-style views, and alert conditions
+- Use logs for debugging after a metric tells you something is wrong
+
+## Task 2 — Prometheus Setup
+
+### Architecture
+
+```mermaid
+flowchart TD
+ A[app-python:8000
/metrics]
+ P[prometheus:9090
TSDB + retention]
+ G[grafana:3000
dashboards + panels]
+ PT[promtail]
+ L[loki]
+
+ A -- scrape every 15s --> P
+ P -- query / visualize --> G
+
+ A -- stdout logs --> PT
+ PT --> L
+ L --> G
+```
+
+### Prometheus Configuration
+
+Prometheus was added to the existing monitoring stack in `monitoring/docker-compose.yml` and configured with `monitoring/prometheus/prometheus.yml`.
+
+Current scrape targets:
+
+- `prometheus` -> `localhost:9090`
+- `app` -> `app-python:8000/metrics`
+- `loki` -> `loki:3100/metrics`
+- `grafana` -> `grafana:3000/metrics`
+
+Current scrape/evaluation interval:
+
+- `15s`
+
+### Task 2 Commands Used
+
+```bash
+PS1="$ "
+cd monitoring
+docker compose up -d
+docker compose ps | tee /tmp/lab08_task2_compose_ps.txt
+curl -fSs http://127.0.0.1:9090/api/v1/targets \
+ | jq '{status, data: {activeTargets: [.data.activeTargets[] | {labels, scrapeUrl, lastError, health}]}}' \
+ | tee /tmp/lab08_task2_targets_final.json
+curl -fSsG --data-urlencode 'query=up' http://127.0.0.1:9090/api/v1/query \
+ | jq '{status, data: {resultType: .data.resultType, result: .data.result}}' \
+ | tee /tmp/lab08_task2_up_final.json
+```
+
+### Task 2 Evidence
+
+Prometheus target screenshot:
+
+
+
+PromQL `up` screenshot:
+
+
+
+
+/api/v1/targets output
+
+```json
+$ curl -fSs http://127.0.0.1:9090/api/v1/targets | jq '{status, data: {activeTargets: [.data.activeTargets[] | {labels, scrapeUrl, lastError, health}]}}' | tee /tmp/lab08_task2_targets_final.json
+{
+ "status": "success",
+ "data": {
+ "activeTargets": [
+ {
+ "labels": {
+ "instance": "app-python:8000",
+ "job": "app"
+ },
+ "scrapeUrl": "http://app-python:8000/metrics",
+ "lastError": "",
+ "health": "up"
+ },
+ {
+ "labels": {
+ "instance": "grafana:3000",
+ "job": "grafana"
+ },
+ "scrapeUrl": "http://grafana:3000/metrics",
+ "lastError": "",
+ "health": "up"
+ },
+ {
+ "labels": {
+ "instance": "loki:3100",
+ "job": "loki"
+ },
+ "scrapeUrl": "http://loki:3100/metrics",
+ "lastError": "",
+ "health": "up"
+ },
+ {
+ "labels": {
+ "instance": "localhost:9090",
+ "job": "prometheus"
+ },
+ "scrapeUrl": "http://localhost:9090/metrics",
+ "lastError": "",
+ "health": "up"
+ }
+ ]
+ }
+}
+```
+
+
+
+
+query=up output
+
+```json
+$ curl -fSsG --data-urlencode 'query=up' http://127.0.0.1:9090/api/v1/query | jq '{status, data: {resultType: .data.resultType, result: .data.result}}' | tee /tmp/lab08_task2_up_final.json
+{
+ "status": "success",
+ "data": {
+ "resultType": "vector",
+ "result": [
+ {
+ "metric": {
+ "__name__": "up",
+ "instance": "grafana:3000",
+ "job": "grafana"
+ },
+ "value": [
+ 1773967701.736,
+ "1"
+ ]
+ },
+ {
+ "metric": {
+ "__name__": "up",
+ "instance": "localhost:9090",
+ "job": "prometheus"
+ },
+ "value": [
+ 1773967701.736,
+ "1"
+ ]
+ },
+ {
+ "metric": {
+ "__name__": "up",
+ "instance": "app-python:8000",
+ "job": "app"
+ },
+ "value": [
+ 1773967701.736,
+ "1"
+ ]
+ },
+ {
+ "metric": {
+ "__name__": "up",
+ "instance": "loki:3100",
+ "job": "loki"
+ },
+ "value": [
+ 1773967701.736,
+ "1"
+ ]
+ }
+ ]
+ }
+}
+```
+
+
+
+## Task 3 — Grafana Dashboards
+
+### Prometheus Data Source
+
+The Prometheus data source was added in Grafana with:
+
+- URL: `http://prometheus:9090`
+- access mode: proxy
+
+### Dashboard Walkthrough
+
+The exported dashboard is stored in `monitoring/grafana/dashbboard.json`.
+
+Panels in the current dashboard:
+
+- `Status Code Distribution`
+ - Type: `piechart`
+ - Query: `sum by (status_code) (rate(http_requests_total[5m]))`
+ - Purpose: show 2xx/4xx/5xx mix
+- `Uptime`
+ - Type: `stat`
+ - Query: `up{job="app"}`
+ - Purpose: show whether the app is scrapeable
+- `Active Requests`
+ - Type: `timeseries`
+ - Query: `http_requests_in_progress`
+ - Purpose: show in-flight request concurrency
+- `Error Rate`
+ - Type: `timeseries`
+ - Query: `sum(rate(http_requests_total{status_code=~"5.."}[5m]))`
+ - Purpose: highlight 5xx traffic
+- `Request Rate`
+ - Type: `timeseries`
+ - Query: `sum(rate(http_requests_total[5m])) by (endpoint)`
+ - Purpose: show throughput per endpoint
+- `Request Duration p95`
+ - Type: `timeseries`
+ - Query: `histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))`
+ - Purpose: track latency percentile
+- `Request Duration Heatmap`
+ - Type: `heatmap`
+ - Query: `rate(http_request_duration_seconds_bucket[5m])`
+ - Purpose: visualize latency distribution
+
+### Task 3 Notes
+
+Two corrections were made to the exported JSON during the final review:
+
+- `status` was replaced with `status_code`
+- `Request Duration p95` was corrected from `heatmap` to `timeseries`
+
+These changes align the dashboard with the actual metric schema emitted by the Python app.
+
+### Task 3 Commands Used
+
+```bash
+PS1="$ "
+cd monitoring
+jq '{uid, title, panels: [.panels[] | {title, type, expr: .targets[0].expr}]}' monitoring/grafana/dashbboard.json \
+ | tee /tmp/lab08_task3_dashboard_summary.json
+curl -fSsG --data-urlencode 'query=http_requests_total' http://127.0.0.1:9090/api/v1/query \
+ | jq '{status, data: {resultType: .data.resultType, resultCount: (.data.result | length), result: .data.result[0:4]}}' \
+ | tee /tmp/lab08_task3_requests_total.json
+```
+
+### Task 3 Evidence
+
+Custom dashboard screenshot:
+
+
+
+
+dashboard export summary output
+
+```json
+$ jq '{uid, title, panels: [.panels[] | {title, type, expr: .targets[0].expr}]}' monitoring/grafana/dashbboard.json | tee /tmp/lab08_task3_dashboard_summary.json
+{
+ "uid": "adksq66",
+ "title": "Custom",
+ "panels": [
+ {
+ "title": "Status Code Distribution",
+ "type": "piechart",
+ "expr": "sum by (status_code) (rate(http_requests_total[5m]))"
+ },
+ {
+ "title": "Uptime",
+ "type": "stat",
+ "expr": "up{job=\"app\"}"
+ },
+ {
+ "title": "Active Requests",
+ "type": "timeseries",
+ "expr": "http_requests_in_progress"
+ },
+ {
+ "title": "Error Rate",
+ "type": "timeseries",
+ "expr": "sum(rate(http_requests_total{status_code=~\"5..\"}[5m]))"
+ },
+ {
+ "title": "Request Rate",
+ "type": "timeseries",
+ "expr": "sum(rate(http_requests_total[5m])) by (endpoint)"
+ },
+ {
+ "title": "Request Duration p95",
+ "type": "timeseries",
+ "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))"
+ },
+ {
+ "title": "Request Duration Heatmap",
+ "type": "heatmap",
+ "expr": "rate(http_request_duration_seconds_bucket[5m])"
+ }
+ ]
+}
+```
+
+
+
+
+http_requests_total query output
+
+```json
+$ curl -fSsG --data-urlencode 'query=http_requests_total' http://127.0.0.1:9090/api/v1/query | jq '{status, data: {resultType: .data.resultType, resultCount: (.data.result | length), result: .data.result[0:4]}}' | tee /tmp/lab08_task3_requests_total.json
+{
+ "status": "success",
+ "data": {
+ "resultType": "vector",
+ "resultCount": 4,
+ "result": [
+ {
+ "metric": {
+ "__name__": "http_requests_total",
+ "endpoint": "/health",
+ "instance": "app-python:8000",
+ "job": "app",
+ "method": "GET",
+ "status_code": "200"
+ },
+ "value": [
+ 1773967701.768,
+ "9"
+ ]
+ },
+ {
+ "metric": {
+ "__name__": "http_requests_total",
+ "endpoint": "/metrics",
+ "instance": "app-python:8000",
+ "job": "app",
+ "method": "GET",
+ "status_code": "200"
+ },
+ "value": [
+ 1773967701.768,
+ "8"
+ ]
+ },
+ {
+ "metric": {
+ "__name__": "http_requests_total",
+ "endpoint": "/metrics",
+ "instance": "app-python:8000",
+ "job": "app",
+ "method": "HEAD",
+ "status_code": "200"
+ },
+ "value": [
+ 1773967701.768,
+ "1"
+ ]
+ },
+ {
+ "metric": {
+ "__name__": "http_requests_total",
+ "endpoint": "/",
+ "instance": "app-python:8000",
+ "job": "app",
+ "method": "GET",
+ "status_code": "200"
+ },
+ "value": [
+ 1773967701.768,
+ "5"
+ ]
+ }
+ ]
+ }
+}
+```
+
+
+
+## Task 4 — Production Configuration
+
+### Health Checks
+
+The stack now includes production-style health checks for the services that can reasonably self-test.
+
+- `loki`
+ - endpoint: `http://127.0.0.1:3100/ready`
+- `grafana`
+ - endpoint: `http://127.0.0.1:3000/api/health`
+- `prometheus`
+ - endpoint: `http://127.0.0.1:9090/-/healthy`
+- `promtail`
+ - endpoint: `http://127.0.0.1:9080/ready`
+ - implemented with `bash` + `/dev/tcp` because this image does not include `wget` or `curl`
+- `app-python`
+ - endpoint: `http://127.0.0.1:8000/health`
+- `app-go`
+ - monitored by the existing `app-go-healthcheck` helper container
+ - reason: the Go image is built `FROM scratch`, so it cannot run an in-container shell-based HTTP probe
+
+### Resource Limits
+
+Configured limits in `monitoring/docker-compose.yml`:
+
+- Prometheus: `1.0` CPU, `1G` memory
+- Loki: `1.0` CPU, `1G` memory
+- Grafana: `0.5` CPU, `512M` memory
+- app-python: `0.5` CPU, `256M` memory
+- app-go: `0.5` CPU, `256M` memory
+- Promtail: `0.5` CPU, `256M` memory
+
+### Retention and Persistence
+
+Prometheus retention is enforced through container flags:
+
+- `--storage.tsdb.retention.time=15d`
+- `--storage.tsdb.retention.size=10GB`
+
+Persistent volumes in the stack:
+
+- `prometheus-data`
+- `loki-data`
+- `grafana-data`
+- `promtail-data`
+
+### Task 4 Commands Used
+
+```bash
+PS1="$ "
+cd monitoring
+set -a && source .env
+curl -fSs -u "$GRAFANA_ADMIN_USER:$GRAFANA_ADMIN_PASSWORD" 'http://127.0.0.1:3000/api/search?query=Custom' \
+ | jq '{count: length, dashboards: [.[] | {uid, title, url}]}' \
+ | tee /tmp/lab08_task4_grafana_before.json
+docker compose down
+docker compose up -d
+docker compose ps | tee /tmp/lab08_task4_compose_ps_final.txt
+curl -fSs -u "$GRAFANA_ADMIN_USER:$GRAFANA_ADMIN_PASSWORD" 'http://127.0.0.1:3000/api/search?query=Custom' \
+ | jq '{count: length, dashboards: [.[] | {uid, title, url}]}' \
+ | tee /tmp/lab08_task4_grafana_after.json
+docker inspect monitoring-prometheus-1 --format '{{json .Config.Healthcheck.Test}} {{json .Config.Cmd}}' \
+ | tee /tmp/lab08_task4_prometheus_inspect.txt
+```
+
+### Task 4 Evidence
+
+
+docker compose ps after restart
+
+```text
+$ docker compose ps | tee /tmp/lab08_task4_compose_ps_final.txt
+NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
+monitoring-app-go-1 localt0aster/devops-app-go:1.7.9a42ee5 "/devops-info-servic…" app-go 2 minutes ago Up 2 minutes 0.0.0.0:8001->8001/tcp, [::]:8001->8001/tcp
+monitoring-app-go-healthcheck-1 curlimages/curl:8.18.0 "/entrypoint.sh sh -…" app-go-healthcheck 2 minutes ago Up 2 minutes (healthy)
+monitoring-app-python-1 localt0aster/devops-app-py:1.8.806c77e "sh -c 'gunicorn --c…" app-python 2 minutes ago Up 2 minutes (healthy) 0.0.0.0:8000->8000/tcp, [::]:8000->8000/tcp
+monitoring-grafana-1 grafana/grafana:12.3.1 "/run.sh" grafana 2 minutes ago Up About a minute (healthy) 0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp
+monitoring-loki-1 grafana/loki:3.0.0 "/usr/bin/loki -conf…" loki 2 minutes ago Up 2 minutes (healthy) 0.0.0.0:3100->3100/tcp, [::]:3100->3100/tcp
+monitoring-prometheus-1 prom/prometheus:v3.9.0 "/bin/prometheus --c…" prometheus 2 minutes ago Up 2 minutes (healthy) 0.0.0.0:9090->9090/tcp, [::]:9090->9090/tcp
+monitoring-promtail-1 grafana/promtail:3.0.0 "/usr/bin/promtail -…" promtail 23 seconds ago Up 22 seconds (healthy) 0.0.0.0:9080->9080/tcp, [::]:9080->9080/tcp
+```
+
+
+
+
+Grafana dashboard inventory before restart
+
+```json
+$ curl -fSs -u "$GRAFANA_ADMIN_USER:$GRAFANA_ADMIN_PASSWORD" 'http://127.0.0.1:3000/api/search?query=Custom' | jq '{count: length, dashboards: [.[] | {uid, title, url}]}' | tee /tmp/lab08_task4_grafana_before.json
+{
+ "count": 2,
+ "dashboards": [
+ {
+ "uid": "adksq66",
+ "title": "Custom",
+ "url": "/d/adksq66/custom"
+ },
+ {
+ "uid": "adksq661",
+ "title": "Custom2",
+ "url": "/d/adksq661/custom2"
+ }
+ ]
+}
+```
+
+
+
+
+Grafana dashboard inventory after restart
+
+```json
+$ curl -fSs -u "$GRAFANA_ADMIN_USER:$GRAFANA_ADMIN_PASSWORD" 'http://127.0.0.1:3000/api/search?query=Custom' | jq '{count: length, dashboards: [.[] | {uid, title, url}]}' | tee /tmp/lab08_task4_grafana_after.json
+{
+ "count": 2,
+ "dashboards": [
+ {
+ "uid": "adksq66",
+ "title": "Custom",
+ "url": "/d/adksq66/custom"
+ },
+ {
+ "uid": "adksq661",
+ "title": "Custom2",
+ "url": "/d/adksq661/custom2"
+ }
+ ]
+}
+```
+
+
+
+
+Prometheus healthcheck and retention flags
+
+```text
+$ docker inspect monitoring-prometheus-1 --format '{{json .Config.Healthcheck.Test}} {{json .Config.Cmd}}' | tee /tmp/lab08_task4_prometheus_inspect.txt
+["CMD-SHELL","wget --no-verbose --tries=1 --spider http://127.0.0.1:9090/-/healthy || exit 1"] ["--config.file=/etc/prometheus/prometheus.yml","--storage.tsdb.retention.time=15d","--storage.tsdb.retention.size=10GB"]
+```
+
+
+
+### Persistence Result
+
+Dashboard persistence was confirmed because the same Grafana dashboard UIDs existed before and after `docker compose down` and `docker compose up -d`.
+
+## Task 5 — Final Documentation Pass
+
+### PromQL Examples
+
+The following queries match the actual exported label names:
+
+- `up{job="app"}`
+ - Is the Python app currently scrapeable?
+- `sum(rate(http_requests_total[5m])) by (endpoint)`
+ - Requests per second per endpoint
+- `sum(rate(http_requests_total{status_code=~"5.."}[5m]))`
+ - 5xx error rate
+- `sum by (status_code) (rate(http_requests_total[5m]))`
+ - Status-code distribution for the pie chart
+- `http_requests_in_progress`
+ - Current in-flight requests
+- `histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))`
+ - p95 latency estimate
+- `devops_info_endpoint_calls_total`
+ - App-specific endpoint usage counter
+
+### Testing Results
+
+What was verified during this lab:
+
+- the Python app exposes `/metrics`
+- Prometheus scrapes the app, Loki, Grafana, and itself successfully
+- Grafana dashboard panels render live Prometheus data
+- Prometheus retention flags are applied to the running container
+- Grafana dashboards persist across `docker compose down` and `up -d`
+- Promtail, Loki, Grafana, Prometheus, and the Python app all report healthy status after the final restart
+
+### Challenges and Solutions
+
+- Challenge: the branch image tag mattered because the older published Python image did not contain the new `/metrics` endpoint
+ - Solution: use `localt0aster/devops-app-py:1.8.806c77e`
+- Challenge: the lab examples used `status`, while the implemented app uses `status_code`
+ - Solution: adapt the Grafana and PromQL queries to `status_code`
+- Challenge: the Promtail image does not include `wget` or `curl`
+ - Solution: use a `bash` + `/dev/tcp` healthcheck against `/ready`
+- Challenge: the Go service is built from `scratch`, so it cannot run a normal in-container HTTP healthcheck
+ - Solution: keep the dedicated `app-go-healthcheck` helper container
diff --git a/monitoring/docs/img/lab08_task2_targets.png b/monitoring/docs/img/lab08_task2_targets.png
new file mode 100644
index 0000000000..be3c369b91
Binary files /dev/null and b/monitoring/docs/img/lab08_task2_targets.png differ
diff --git a/monitoring/docs/img/lab08_task2_up_query.png b/monitoring/docs/img/lab08_task2_up_query.png
new file mode 100644
index 0000000000..dc281d37b6
Binary files /dev/null and b/monitoring/docs/img/lab08_task2_up_query.png differ
diff --git a/monitoring/docs/img/lab08_task3_custom_dashboard.png b/monitoring/docs/img/lab08_task3_custom_dashboard.png
new file mode 100644
index 0000000000..a018051d99
Binary files /dev/null and b/monitoring/docs/img/lab08_task3_custom_dashboard.png differ
diff --git a/monitoring/grafana/dashbboard.json b/monitoring/grafana/dashbboard.json
new file mode 100644
index 0000000000..8300187931
--- /dev/null
+++ b/monitoring/grafana/dashbboard.json
@@ -0,0 +1,620 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": {
+ "type": "grafana",
+ "uid": "-- Grafana --"
+ },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "id": 0,
+ "links": [],
+ "panels": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "afgj96bua3bpce"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ }
+ },
+ "mappings": []
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 0,
+ "y": 0
+ },
+ "id": 6,
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "pieType": "pie",
+ "reduceOptions": {
+ "calcs": ["lastNotNull"],
+ "fields": "",
+ "values": false
+ },
+ "sort": "desc",
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "12.3.1",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "afgj96bua3bpce"
+ },
+ "editorMode": "code",
+ "expr": "sum by (status_code) (rate(http_requests_total[5m]))",
+ "legendFormat": "__auto",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Status Code Distribution",
+ "type": "piechart"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "afgj96bua3bpce"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": 0
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 6,
+ "y": 0
+ },
+ "id": 7,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "percentChangeColorMode": "standard",
+ "reduceOptions": {
+ "calcs": ["lastNotNull"],
+ "fields": "",
+ "values": false
+ },
+ "showPercentChange": false,
+ "textMode": "auto",
+ "wideLayout": true
+ },
+ "pluginVersion": "12.3.1",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "afgj96bua3bpce"
+ },
+ "editorMode": "code",
+ "expr": "up{job=\"app\"}",
+ "legendFormat": "__auto",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Uptime",
+ "type": "stat"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "afgj96bua3bpce"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "showValues": false,
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": 0
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 0
+ },
+ "id": 5,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "12.3.1",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "afgj96bua3bpce"
+ },
+ "editorMode": "code",
+ "expr": "http_requests_in_progress",
+ "legendFormat": "__auto",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Active Requests",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "afgj96bua3bpce"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "showValues": false,
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": 0
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 8
+ },
+ "id": 2,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "12.3.1",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "afgj96bua3bpce"
+ },
+ "editorMode": "code",
+ "expr": "sum(rate(http_requests_total{status_code=~\"5..\"}[5m]))",
+ "legendFormat": "__auto",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Error Rate",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "afgj96bua3bpce"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "showValues": false,
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": 0
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 8
+ },
+ "id": 1,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "12.3.1",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "afgj96bua3bpce"
+ },
+ "editorMode": "code",
+ "expr": "sum(rate(http_requests_total[5m])) by (endpoint)",
+ "legendFormat": "__auto",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Request Rate",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "afgj96bua3bpce"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "scaleDistribution": {
+ "type": "linear"
+ }
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 9,
+ "w": 12,
+ "x": 0,
+ "y": 16
+ },
+ "id": 3,
+ "options": {
+ "calculate": false,
+ "cellGap": 1,
+ "color": {
+ "exponent": 0.5,
+ "fill": "dark-orange",
+ "mode": "scheme",
+ "reverse": false,
+ "scale": "exponential",
+ "scheme": "Oranges",
+ "steps": 64
+ },
+ "exemplars": {
+ "color": "rgba(255,0,255,0.7)"
+ },
+ "filterValues": {
+ "le": 1e-9
+ },
+ "legend": {
+ "show": true
+ },
+ "rowsFrame": {
+ "layout": "auto"
+ },
+ "tooltip": {
+ "mode": "single",
+ "showColorScale": false,
+ "yHistogram": false
+ },
+ "yAxis": {
+ "axisPlacement": "left",
+ "reverse": false
+ }
+ },
+ "pluginVersion": "12.3.1",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "afgj96bua3bpce"
+ },
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
+ "legendFormat": "__auto",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Request Duration p95",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "afgj96bua3bpce"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "scaleDistribution": {
+ "type": "linear"
+ }
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 16
+ },
+ "id": 4,
+ "options": {
+ "calculate": false,
+ "cellGap": 1,
+ "color": {
+ "exponent": 0.5,
+ "fill": "dark-orange",
+ "mode": "scheme",
+ "reverse": false,
+ "scale": "exponential",
+ "scheme": "Oranges",
+ "steps": 64
+ },
+ "exemplars": {
+ "color": "rgba(255,0,255,0.7)"
+ },
+ "filterValues": {
+ "le": 1e-9
+ },
+ "legend": {
+ "show": true
+ },
+ "rowsFrame": {
+ "layout": "auto"
+ },
+ "tooltip": {
+ "mode": "single",
+ "showColorScale": false,
+ "yHistogram": false
+ },
+ "yAxis": {
+ "axisPlacement": "left",
+ "reverse": false
+ }
+ },
+ "pluginVersion": "12.3.1",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "afgj96bua3bpce"
+ },
+ "editorMode": "code",
+ "expr": "rate(http_request_duration_seconds_bucket[5m])",
+ "legendFormat": "__auto",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Request Duration Heatmap",
+ "type": "heatmap"
+ }
+ ],
+ "preload": false,
+ "schemaVersion": 42,
+ "tags": [],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-1h",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "browser",
+ "title": "Custom",
+ "uid": "adksq66",
+ "version": 10
+}
diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml
new file mode 100644
index 0000000000..405abc0fdf
--- /dev/null
+++ b/monitoring/prometheus/prometheus.yml
@@ -0,0 +1,27 @@
+global:
+ scrape_interval: 15s
+ evaluation_interval: 15s
+
+scrape_configs:
+ - job_name: prometheus
+ static_configs:
+ - targets:
+ - localhost:9090
+
+ - job_name: app
+ metrics_path: /metrics
+ static_configs:
+ - targets:
+ - app-python:8000
+
+ - job_name: loki
+ metrics_path: /metrics
+ static_configs:
+ - targets:
+ - loki:3100
+
+ - job_name: grafana
+ metrics_path: /metrics
+ static_configs:
+ - targets:
+ - grafana:3000
diff --git a/vagrant/README.md b/vagrant/README.md
index 24174798f9..1b44c25725 100644
--- a/vagrant/README.md
+++ b/vagrant/README.md
@@ -113,14 +113,16 @@ cp shared/github-runner.env.example shared/github-runner.env
2. Edit `shared/github-runner.env` and fill in:
- `GH_RUNNER_URL`
-- `GH_RUNNER_TOKEN`
+- `GH_RUNNER_API_TOKEN` or `GH_RUNNER_TOKEN`
- optional runner name / labels / group / workdir
-If the runner VM is already running, sync the updated file into the guest:
+`GH_RUNNER_API_TOKEN` is the safer option because the provisioner will exchange it for a fresh one-hour runner registration token every time it runs. For a repository runner, GitHub's REST API requires a token that can create registration tokens for that repository. For a fine-grained PAT, that means repository `Administration: write`. A manually copied `GH_RUNNER_TOKEN` still works, but it expires after one hour and must be refreshed before provisioning.
-```bash
-vagrant rsync github-runner
-```
+> ❗ If the runner VM is already running, don't forget to sync the updated shared files into the guest:
+>
+> ```bash
+> vagrant rsync github-runner
+> ```
3. Run the registration provisioner:
diff --git a/vagrant/Vagrantfile b/vagrant/Vagrantfile
index f6bc680c11..80152c12e6 100644
--- a/vagrant/Vagrantfile
+++ b/vagrant/Vagrantfile
@@ -11,6 +11,8 @@ Vagrant.configure("2") do |config|
"GH_RUNNER_VERSION" => ENV.fetch("GH_RUNNER_VERSION", ""),
"GH_RUNNER_URL" => ENV.fetch("GH_RUNNER_URL", ""),
"GH_RUNNER_TOKEN" => ENV.fetch("GH_RUNNER_TOKEN", ""),
+ "GH_RUNNER_API_TOKEN" => ENV.fetch("GH_RUNNER_API_TOKEN", ""),
+ "GH_RUNNER_API_URL" => ENV.fetch("GH_RUNNER_API_URL", ""),
"GH_RUNNER_NAME" => ENV.fetch("GH_RUNNER_NAME", ""),
"GH_RUNNER_LABELS" => ENV.fetch("GH_RUNNER_LABELS", ""),
"GH_RUNNER_GROUP" => ENV.fetch("GH_RUNNER_GROUP", ""),
diff --git a/vagrant/shared/github-runner.env.example b/vagrant/shared/github-runner.env.example
index 7c750fba09..4e1c1b215f 100644
--- a/vagrant/shared/github-runner.env.example
+++ b/vagrant/shared/github-runner.env.example
@@ -3,7 +3,19 @@
# vagrant provision github-runner --provision-with github-runner-register
GH_RUNNER_URL="https://github.com/OWNER/REPOSITORY"
-GH_RUNNER_TOKEN="REPLACE_WITH_EPHEMERAL_REGISTRATION_TOKEN"
+
+# Preferred: a GitHub API token that can mint a fresh registration token during
+# provisioning. For repository runners on github.com, a fine-grained PAT needs
+# repository Administration: write. For organization runners, it needs
+# organization Self-hosted runners: write.
+# GH_RUNNER_API_TOKEN="github_pat_..."
+
+# Optional: override the API root for GitHub Enterprise Server instances.
+# GH_RUNNER_API_URL="https://github.example.com/api/v3"
+
+# Fallback: a time-limited registration token copied from GitHub. This expires
+# after one hour, so stale values will fail during runner registration.
+GH_RUNNER_TOKEN=""
# Optional overrides
GH_RUNNER_NAME="github-runner-s26"
diff --git a/vagrant/shared/provision-gh-runner-register.sh b/vagrant/shared/provision-gh-runner-register.sh
index be6902328a..ac29aecad1 100644
--- a/vagrant/shared/provision-gh-runner-register.sh
+++ b/vagrant/shared/provision-gh-runner-register.sh
@@ -13,24 +13,141 @@ if [ -f /shared/github-runner.env ]; then
set +a
fi
-if [ ! -f /shared/github-runner.env ] && [ -z "${GH_RUNNER_URL:-}" ] && [ -z "${GH_RUNNER_TOKEN:-}" ]; then
+if [ ! -f /shared/github-runner.env ] && [ -z "${GH_RUNNER_URL:-}" ] && [ -z "${GH_RUNNER_TOKEN:-}" ] && [ -z "${GH_RUNNER_API_TOKEN:-}" ]; then
cat <<'EOF' >&2
Runner configuration is missing inside the guest.
If you created vagrant/shared/github-runner.env on the host after the VM was already running,
sync it first with:
vagrant rsync github-runner
-Or bypass the shared file and pass GH_RUNNER_URL / GH_RUNNER_TOKEN in the host environment.
+Or bypass the shared file and pass GH_RUNNER_URL plus GH_RUNNER_API_TOKEN or GH_RUNNER_TOKEN in the host environment.
EOF
fi
: "${GH_RUNNER_URL:?Set GH_RUNNER_URL in /shared/github-runner.env or the host environment.}"
-: "${GH_RUNNER_TOKEN:?Set GH_RUNNER_TOKEN in /shared/github-runner.env or the host environment.}"
+
+if [ -z "${GH_RUNNER_TOKEN:-}" ] && [ -z "${GH_RUNNER_API_TOKEN:-}" ]; then
+ echo "Set GH_RUNNER_API_TOKEN (preferred) or GH_RUNNER_TOKEN in /shared/github-runner.env or the host environment." >&2
+ exit 1
+fi
if [ ! -x /opt/actions-runner/config.sh ]; then
echo "GitHub runner is not installed. Run the base provisioner first." >&2
exit 1
fi
+runner_url="${GH_RUNNER_URL%/}"
+case "$runner_url" in
+ https://*)
+ runner_scheme="https"
+ ;;
+ http://*)
+ runner_scheme="http"
+ ;;
+ *)
+ echo "GH_RUNNER_URL must start with http:// or https:// and point to a repository or organization root." >&2
+ exit 1
+ ;;
+esac
+
+runner_url_no_scheme="${runner_url#*://}"
+runner_host="${runner_url_no_scheme%%/*}"
+runner_path=""
+if [ "$runner_url_no_scheme" != "$runner_host" ]; then
+ runner_path="/${runner_url_no_scheme#*/}"
+fi
+runner_path="${runner_path%%\?*}"
+runner_path="${runner_path%%\#*}"
+runner_path="${runner_path%/}"
+
+IFS='/' read -r runner_segment1 runner_segment2 runner_segment3 _runner_extra <<< "${runner_path#/}"
+
+if [ -z "${runner_segment1:-}" ]; then
+ echo "GH_RUNNER_URL must point to a repository or organization root, for example https://github.com/OWNER/REPOSITORY." >&2
+ exit 1
+fi
+
+if [ -n "${runner_segment3:-}" ] || [ -n "${_runner_extra:-}" ]; then
+ echo "GH_RUNNER_URL must be a repository or organization root URL, not a deeper settings page." >&2
+ exit 1
+fi
+
+runner_scope="organization"
+runner_owner="$runner_segment1"
+runner_repo=""
+if [ -n "${runner_segment2:-}" ]; then
+ runner_scope="repository"
+ runner_repo="${runner_segment2%.git}"
+fi
+
+runner_api_base="${GH_RUNNER_API_URL:-}"
+if [ -z "$runner_api_base" ]; then
+ if [ "$runner_host" = "github.com" ]; then
+ runner_api_base="https://api.github.com"
+ else
+ runner_api_base="${runner_scheme}://${runner_host}/api/v3"
+ fi
+fi
+runner_api_base="${runner_api_base%/}"
+
+create_registration_token() {
+ local endpoint response http_code body token expires_at message
+
+ if [ -n "${GH_RUNNER_API_TOKEN:-}" ]; then
+ case "$runner_scope" in
+ repository)
+ endpoint="${runner_api_base}/repos/${runner_owner}/${runner_repo}/actions/runners/registration-token"
+ ;;
+ organization)
+ endpoint="${runner_api_base}/orgs/${runner_owner}/actions/runners/registration-token"
+ ;;
+ *)
+ echo "Unsupported runner scope: $runner_scope" >&2
+ exit 1
+ ;;
+ esac
+
+ response="$(
+ curl -sS -X POST \
+ -H "Accept: application/vnd.github+json" \
+ -H "Authorization: Bearer ${GH_RUNNER_API_TOKEN}" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ "$endpoint" \
+ -w $'\n%{http_code}'
+ )"
+
+ http_code="${response##*$'\n'}"
+ body="${response%$'\n'*}"
+
+ if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
+ message="$(printf '%s' "$body" | jq -r '.message // empty' 2>/dev/null || true)"
+ echo "Failed to create a runner registration token from ${endpoint} (HTTP ${http_code})." >&2
+ if [ -n "$message" ]; then
+ echo "GitHub API message: ${message}" >&2
+ fi
+ echo "Check that GH_RUNNER_API_TOKEN has admin access to the target and the required runner permissions." >&2
+ exit 1
+ fi
+
+ token="$(printf '%s' "$body" | jq -r '.token // empty')"
+ expires_at="$(printf '%s' "$body" | jq -r '.expires_at // empty')"
+ if [ -z "$token" ]; then
+ echo "GitHub did not return a runner registration token." >&2
+ exit 1
+ fi
+
+ export RUNNER_TOKEN="$token"
+ if [ -n "$expires_at" ]; then
+ echo "Generated a fresh runner registration token via the GitHub API (expires at ${expires_at})."
+ else
+ echo "Generated a fresh runner registration token via the GitHub API."
+ fi
+ return 0
+ fi
+
+ export RUNNER_TOKEN="${GH_RUNNER_TOKEN}"
+ echo "Using GH_RUNNER_TOKEN from the environment. GitHub runner registration tokens expire after one hour."
+}
+
runner_name="${GH_RUNNER_NAME:-$(hostname -s)}"
runner_labels="${GH_RUNNER_LABELS:-self-hosted,linux,vagrant}"
runner_group="${GH_RUNNER_GROUP:-Default}"
@@ -38,7 +155,6 @@ runner_workdir="${GH_RUNNER_WORKDIR:-_work}"
runner_disable_update="${GH_RUNNER_DISABLE_UPDATE:-false}"
export RUNNER_URL="$GH_RUNNER_URL"
-export RUNNER_TOKEN="$GH_RUNNER_TOKEN"
export RUNNER_NAME="$runner_name"
export RUNNER_LABELS="$runner_labels"
export RUNNER_GROUP="$runner_group"
@@ -46,6 +162,7 @@ export RUNNER_WORKDIR="$runner_workdir"
export RUNNER_DISABLE_UPDATE="$runner_disable_update"
if [ ! -f /opt/actions-runner/.runner ]; then
+ create_registration_token
sudo -u github-runner --preserve-env=RUNNER_URL,RUNNER_TOKEN,RUNNER_NAME,RUNNER_LABELS,RUNNER_GROUP,RUNNER_WORKDIR,RUNNER_DISABLE_UPDATE bash <<'EOF'
set -euo pipefail
cd /opt/actions-runner
@@ -98,3 +215,4 @@ for service_unit in "${service_units[@]}"; do
done
echo "GitHub runner configured and started successfully."
+#
diff --git a/vagrant/shared/provision-gh-runner.sh b/vagrant/shared/provision-gh-runner.sh
index fe22cd89e2..0e58c5c2d2 100644
--- a/vagrant/shared/provision-gh-runner.sh
+++ b/vagrant/shared/provision-gh-runner.sh
@@ -85,7 +85,7 @@ if [ ! -f /shared/github-runner.env ]; then
cat <<'EOF'
GitHub runner base installation complete.
Create /shared/github-runner.env from /shared/github-runner.env.example,
-fill in GH_RUNNER_URL and GH_RUNNER_TOKEN, then run:
+fill in GH_RUNNER_URL and either GH_RUNNER_API_TOKEN (preferred) or GH_RUNNER_TOKEN, then run:
vagrant provision github-runner --provision-with github-runner-register
EOF
fi