diff --git a/.editorconfig b/.editorconfig index d561238..e1e326c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,18 +1,26 @@ root = true [*] -indent_style = space -indent_size = 2 -end_of_line = lf charset = utf-8 -trim_trailing_whitespace = true +end_of_line = lf insert_final_newline = true +trim_trailing_whitespace = true [*.py] +indent_style = space indent_size = 4 -[*.md] -trim_trailing_whitespace = false +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.{json}] +indent_style = space +indent_size = 2 + +[justfile] +indent_style = space +indent_size = 4 [Makefile] indent_style = tab diff --git a/.env.example b/.env.example index d936a3b..ff2bfb9 100644 --- a/.env.example +++ b/.env.example @@ -67,55 +67,17 @@ ATLAS_AUTH_BEARER_TOKEN= # ATLAS_NATS_ACK_WAIT=30s # (default) # ATLAS_NATS_MAX_ACK_PENDING=1024 # (default) -# ============================================================ -# ProjectArgus — Environment Configuration -# ============================================================ -# Copy this file to .env and edit values for your deployment. -# cp .env.example .env -# -# .env is gitignored; .env.example is version-controlled. -# All variables have sensible defaults in docker-compose.yml, -# so only override what differs from the defaults below. -# ============================================================ - -# --- Service URLs (WSL2: 172.20.0.1 is the host gateway) --- - -# Agamemnon agent-management API +# Copy this file to .env and fill in values before running the stack. +# .env is gitignored — never commit real credentials. + +# Grafana admin password (default: changeme — change before exposing to a network) +GRAFANA_ADMIN_PASSWORD=changeme + +# Agamemnon API base URL (WSL2 host gateway default) AGAMEMNON_URL=http://172.20.0.1:8080 -# Nestor research-pipeline API +# Nestor API base URL (WSL2 host gateway default) NESTOR_URL=http://172.20.0.1:8081 -# NATS monitoring endpoint +# NATS monitoring URL NATS_URL=http://172.24.0.1:8222 - -# --- Grafana --- - -# Host port Grafana is exposed on (container always listens on 3000) -GRAFANA_PORT=3001 - -# Grafana admin password — change this in production! -GF_SECURITY_ADMIN_PASSWORD=admin - -# --- Prometheus --- - -# Host port Prometheus is exposed on -PROMETHEUS_PORT=9090 - -# --- Loki --- - -# Host port Loki is exposed on -LOKI_PORT=3100 - -# --- Exporter --- - -# Host port the argus-exporter is exposed on -EXPORTER_PORT=9100 - -# --- Container names (optional — override to avoid collisions) --- - -# PROMETHEUS_CONTAINER_NAME=argus-prometheus -# LOKI_CONTAINER_NAME=argus-loki -# PROMTAIL_CONTAINER_NAME=argus-promtail -# GRAFANA_CONTAINER_NAME=argus-grafana -# EXPORTER_CONTAINER_NAME=argus-exporter diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27aaed2..8696b88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: ["main"] + branches: ["main", "feature/**", "fix/**", "chore/**"] pull_request: branches: ["main"] @@ -137,39 +137,63 @@ jobs: go test -race -coverprofile=coverage.out ./internal/... go tool cover -func=coverage.out | tail -1 - - name: go test (integration) + - name: Docker build (verify Dockerfile is buildable) working-directory: dashboard - run: go test -race -tags=integration -timeout=60s ./tests/integration/... + run: docker build -t argus-dashboard:ci . - - name: Upload coverage profile - uses: actions/upload-artifact@v4 + test: + name: Test exporter + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 with: - name: coverage-profile - path: dashboard/coverage.out - if-no-files-found: warn - - - name: golangci-lint - # v9 is required for golangci-lint v2 binaries; the older v6 action - # rejects "v2.x" version strings outright. SHA-pinned to v9.2.0 - # (current latest as of 2026-05-07) per the repo's pin convention. - uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 + python-version: "3.11" + + - name: Install test dependencies + run: pip install pytest + + - name: Run tests + run: python -m pytest tests/ -v + + lint: + name: Lint Python + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v5 with: - version: v2.12.2 - working-directory: dashboard + python-version: "3.11" - - name: govulncheck (SCA) - working-directory: dashboard - run: | - go install golang.org/x/vuln/cmd/govulncheck@latest - govulncheck ./... + - name: Install ruff + run: pip install ruff - - name: Check templ generate (no-op) - working-directory: dashboard - run: | - go install github.com/a-h/templ/cmd/templ@latest - templ generate ./... - git diff --exit-code + - name: Run ruff + run: ruff check exporter/ tests/ - - name: Docker build (verify Dockerfile is buildable) - working-directory: dashboard - run: docker build -t argus-dashboard:ci . + security: + name: Security scan + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install bandit + run: pip install bandit + + - name: Run bandit + run: bandit -ll --skip B310,B202 -r exporter/ diff --git a/.gitignore b/.gitignore index 452b1ed..0b03d58 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ configs/nginx/htpasswd htmlcov/ coverage.xml .pytest_cache/ +secrets/ +__pycache__/ +*.pyc +*.pyo diff --git a/.gitleaks.toml b/.gitleaks.toml index 98f2f50..2b84f76 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -8,4 +8,11 @@ description = "Allow credential-like strings in test fixtures and example files" paths = [ '''tests/''', '''\.env\.example$''', + '''htpasswd.example$''', ] + +[[rules]] +description = "Detect htpasswd credential files" +id = "htpasswd" +regex = '''\$apr1\$|\$2y\$|\$2a\$''' +path = '''htpasswd''' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 58779e9..6683a63 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,23 +1,21 @@ repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-merge-conflict - - id: check-json - repo: https://github.com/adrienverge/yamllint rev: v1.35.1 hooks: - id: yamllint - args: ["--config-file", ".yamllint.yaml"] - files: \.(yml|yaml)$ + args: ["-d", "{extends: default, rules: {line-length: {max: 120}, truthy: disable}}"] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.10 + hooks: + - id: ruff + args: ["--fix"] + - repo: local hooks: - - id: check-version-consistency - name: Check pixi.toml version matches CHANGELOG - language: python - entry: python scripts/check-version-consistency.py - pass_filenames: false - files: ^(pixi\.toml|CHANGELOG\.md)$ + - id: bandit + name: bandit security scan + entry: bandit -ll --skip B310,B202 -r + language: system + files: ^exporter/.*\.py$ + pass_filenames: true diff --git a/dashboards/nats-events.json b/dashboards/nats-events.json index 6b4accc..1722a43 100644 --- a/dashboards/nats-events.json +++ b/dashboards/nats-events.json @@ -16,7 +16,7 @@ "datasource": { "type": "prometheus", "uid": "prometheus" }, "targets": [ { - "expr": "rate(gnatsd_varz_in_msgs[1m])", + "expr": "rate(nats_in_msgs_total[1m])", "legendFormat": "msgs/s in", "refId": "A" } @@ -37,7 +37,7 @@ "datasource": { "type": "prometheus", "uid": "prometheus" }, "targets": [ { - "expr": "rate(gnatsd_varz_out_msgs[1m])", + "expr": "rate(nats_out_msgs_total[1m])", "legendFormat": "msgs/s out", "refId": "A" } @@ -58,7 +58,7 @@ "datasource": { "type": "prometheus", "uid": "prometheus" }, "targets": [ { - "expr": "gnatsd_varz_jetstream_stats_storage", + "expr": "nats_jetstream_bytes", "legendFormat": "Storage", "refId": "A" } @@ -73,14 +73,14 @@ }, { "id": 4, - "title": "Active Subscriptions", + "title": "Active Connections", "type": "stat", "gridPos": { "x": 18, "y": 0, "w": 6, "h": 4 }, "datasource": { "type": "prometheus", "uid": "prometheus" }, "targets": [ { - "expr": "gnatsd_varz_subscriptions", - "legendFormat": "Subscriptions", + "expr": "nats_connections", + "legendFormat": "Connections", "refId": "A" } ], @@ -97,12 +97,12 @@ "datasource": { "type": "prometheus", "uid": "prometheus" }, "targets": [ { - "expr": "rate(gnatsd_varz_in_msgs[1m])", + "expr": "rate(nats_in_msgs_total[1m])", "legendFormat": "In msg/s", "refId": "A" }, { - "expr": "rate(gnatsd_varz_out_msgs[1m])", + "expr": "rate(nats_out_msgs_total[1m])", "legendFormat": "Out msg/s", "refId": "B" } @@ -119,12 +119,12 @@ "datasource": { "type": "prometheus", "uid": "prometheus" }, "targets": [ { - "expr": "rate(gnatsd_varz_in_bytes[1m])", + "expr": "rate(nats_in_bytes_total[1m])", "legendFormat": "Bytes in/s", "refId": "A" }, { - "expr": "rate(gnatsd_varz_out_bytes[1m])", + "expr": "rate(nats_out_bytes_total[1m])", "legendFormat": "Bytes out/s", "refId": "B" } diff --git a/docker-compose.yml b/docker-compose.yml index 9a1075f..72898db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,8 @@ services: options: max-size: "50m" max-file: "3" + ports: + - "127.0.0.1:9090:9090" expose: - "9090" volumes: @@ -33,6 +35,12 @@ services: - "--web.console.libraries=/usr/share/prometheus/console_libraries" - "--web.console.templates=/usr/share/prometheus/consoles" - "--web.config.file=/etc/prometheus/prometheus.yml" + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:9090/-/ready"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s networks: - argus deploy: @@ -142,7 +150,7 @@ services: container_name: argus-alertmanager restart: unless-stopped ports: - - "9093:9093" + - "127.0.0.1:9093:9093" volumes: - ./configs/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro - alertmanager_data:/alertmanager @@ -167,7 +175,7 @@ services: max-size: "50m" max-file: "3" ports: - - "${GRAFANA_PORT:-3001}:3000" + - "127.0.0.1:${GRAFANA_PORT:-3001}:3000" volumes: - ./dashboards:/var/lib/grafana/dashboards:ro - ./configs/grafana:/etc/grafana/provisioning:ro @@ -250,7 +258,7 @@ services: container_name: argus-dashboard restart: unless-stopped ports: - - "3002:3002" + - "127.0.0.1:3002:3002" environment: ATLAS_LISTEN_ADDR: ":3002" ATLAS_AGAMEMNON_URL: ${AGAMEMNON_URL:-http://172.20.0.1:8080} @@ -295,7 +303,7 @@ services: container_name: argus-jetstream-consumer restart: unless-stopped ports: - - "9101:9101" + - "127.0.0.1:9101:9101" volumes: - ./jetstream-consumer/consumer.py:/consumer.py:ro command: sh -c "pip install nats-py -q && python /consumer.py" diff --git a/exporter/Dockerfile b/exporter/Dockerfile index 375a4d4..0479f57 100644 --- a/exporter/Dockerfile +++ b/exporter/Dockerfile @@ -1,17 +1,20 @@ # Pinned to stable 3.12-slim; see tests/test_dockerfile_constraints.py for guard -FROM python:3.14-slim AS base +FROM python:3.12-slim AS base # Install no extra packages — exporter uses stdlib only WORKDIR /app FROM base AS final -# Run as non-root -RUN adduser --disabled-password --no-create-home exporter -USER exporter +# Create a dedicated non-root group and user with fixed UID/GID 1000 +RUN groupadd -g 1000 exporter \ + && useradd -u 1000 -g exporter --no-create-home --shell /sbin/nologin exporter COPY --chown=exporter:exporter exporter.py /app/exporter.py +# Run as non-root +USER exporter + EXPOSE 9100 HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ diff --git a/exporter/exporter.py b/exporter/exporter.py index f0ca868..7792a12 100644 --- a/exporter/exporter.py +++ b/exporter/exporter.py @@ -84,16 +84,16 @@ def _health_check(url: str, ca_file: Optional[str] = None) -> int: "hi_nestor_research_completed": "Number of completed research jobs in Nestor", "hi_nestor_research_pending": "Number of pending research jobs in Nestor", "nats_connections": "Current number of client connections to NATS", - "nats_in_msgs_total": "Cumulative inbound messages received by NATS server", - "nats_out_msgs_total": "Cumulative outbound messages sent by NATS server", - "nats_in_bytes_total": "Cumulative inbound bytes received by NATS server", - "nats_out_bytes_total": "Cumulative outbound bytes sent by NATS server", + "nats_in_msgs": "Current inbound message rate from NATS server", + "nats_out_msgs": "Current outbound message rate from NATS server", + "nats_in_bytes": "Current inbound bytes rate from NATS server", + "nats_out_bytes": "Current outbound bytes rate from NATS server", "nats_slow_consumers": "Current number of slow consumers on NATS", "nats_jetstream_streams": "Number of JetStream streams", "nats_jetstream_consumers": "Number of JetStream consumers", "nats_jetstream_messages": "Number of messages stored in JetStream", "nats_jetstream_bytes": "Bytes stored in JetStream", - "homeric_exporter_scrape_timestamp_seconds": "Unix timestamp of the last completed scrape", + "homeric_exporter_scrape_timestamp_seconds": "Unix timestamp (seconds) when the last scrape completed", "homeric_exporter_scrape_duration_seconds": "Wall-clock seconds spent in the last collect() call", "homeric_exporter_fetch_errors_total": "Number of upstream fetch failures per scrape, by upstream", } @@ -182,10 +182,10 @@ def gauge(name: str, help: str, value: float | int, labels: dict | None = None) # ── NATS ─────────────────────────────────────────────────────────────── if nats_varz: gauge("nats_connections", "Current number of client connections to NATS", nats_varz.get("connections", 0)) - gauge("nats_in_msgs_total", "Total inbound messages received by NATS since start", nats_varz.get("in_msgs", 0)) - gauge("nats_out_msgs_total", "Total outbound messages sent by NATS since start", nats_varz.get("out_msgs", 0)) - gauge("nats_in_bytes_total", "Total inbound bytes received by NATS since start", nats_varz.get("in_bytes", 0)) - gauge("nats_out_bytes_total","Total outbound bytes sent by NATS since start", nats_varz.get("out_bytes", 0)) + gauge("nats_in_msgs", "Current inbound message rate from NATS server", nats_varz.get("in_msgs", 0)) + gauge("nats_out_msgs", "Current outbound message rate from NATS server", nats_varz.get("out_msgs", 0)) + gauge("nats_in_bytes", "Current inbound bytes rate from NATS server", nats_varz.get("in_bytes", 0)) + gauge("nats_out_bytes", "Current outbound bytes rate from NATS server", nats_varz.get("out_bytes", 0)) gauge("nats_slow_consumers", "Number of slow consumer connections detected by NATS", nats_varz.get("slow_consumers", 0)) if nats_jsz: @@ -195,8 +195,8 @@ def gauge(name: str, help: str, value: float | int, labels: dict | None = None) gauge("nats_jetstream_bytes", "Total bytes stored across all JetStream streams", nats_jsz.get("bytes", 0)) # ── exporter self ────────────────────────────────────────────────────── - gauge("homeric_exporter_scrape_timestamp", "Unix timestamp when the last scrape completed", time.time()) - gauge("homeric_exporter_scrape_duration_seconds", "Duration in seconds of the last upstream scrape cycle", time.time() - start) + gauge("homeric_exporter_scrape_timestamp_seconds", "Unix timestamp (seconds) when the last scrape completed", time.time()) + gauge("homeric_exporter_scrape_duration_seconds", "Duration in seconds of the last upstream scrape cycle", time.time() - start) for upstream, count in fetch_errors.items(): gauge("homeric_exporter_fetch_errors_total", "Number of fetch failures per upstream service", count, {"upstream": upstream}) diff --git a/justfile b/justfile index 80000a0..0346605 100644 --- a/justfile +++ b/justfile @@ -8,8 +8,7 @@ container_cmd := if `command -v podman-compose 2>/dev/null || true` != "" { "pod AGAMEMNON_URL := "http://172.20.0.1:8080" GRAFANA_PORT := "3001" GRAFANA_URL := "http://localhost:" + GRAFANA_PORT -GRAFANA_ADMIN_PASSWORD := env_var_or_default("GRAFANA_ADMIN_PASSWORD", "admin") -GRAFANA_AUTH := "admin:" + GRAFANA_ADMIN_PASSWORD +GF_ADMIN_PASSWORD := env_var_or_default("GF_ADMIN_PASSWORD", "admin") # === Default === @@ -143,9 +142,9 @@ test-jetstream: curl -s http://localhost:9101/metrics | grep hi_jetstream # Import all JSON dashboards from dashboards/ into Grafana via API -# Reads GRAFANA_ADMIN_PASSWORD from .env (required — never hardcoded) +# Reads GF_ADMIN_PASSWORD from .env (required — never hardcoded) import-dashboards: - GRAFANA_PORT={{GRAFANA_PORT}} GRAFANA_ADMIN_PASSWORD={{GRAFANA_ADMIN_PASSWORD}} ./scripts/import-dashboards.sh + GRAFANA_PORT={{GRAFANA_PORT}} GF_ADMIN_PASSWORD={{GF_ADMIN_PASSWORD}} ./scripts/import-dashboards.sh # === Versioning === diff --git a/pixi.lock b/pixi.lock index 55cc5dc..99b745f 100644 --- a/pixi.lock +++ b/pixi.lock @@ -200,6 +200,199 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/yamllint-1.38.0-pyhcf101f3_1.conda + lint: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.13-h6add32d_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - pypi: https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/30/09/fe0e3bc32bd33707c519b102fc064ad2a2ce5a1b53e2be38b86936b476b1/cyclonedx_python_lib-11.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/f3/4888f895c02afa085630a3a3329d1b18b998874642ad4c530e9a4d7851fe/pip_audit-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + osx-64: + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.0-hcc62823_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hf3981d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.1-h8f8c405_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.2-hc881268_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.13-h3d5d122_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - pypi: https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/30/09/fe0e3bc32bd33707c519b102fc064ad2a2ce5a1b53e2be38b86936b476b1/cyclonedx_python_lib-11.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/f3/4888f895c02afa085630a3a3329d1b18b998874642ad4c530e9a4d7851fe/pip_audit-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.13-h20e6be0_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - pypi: https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/30/09/fe0e3bc32bd33707c519b102fc064ad2a2ce5a1b53e2be38b86936b476b1/cyclonedx_python_lib-11.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/f3/4888f895c02afa085630a3a3329d1b18b998874642ad4c530e9a4d7851fe/pip_audit-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + win-64: + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.1-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.13-h09917c8_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - pypi: https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/30/09/fe0e3bc32bd33707c519b102fc064ad2a2ce5a1b53e2be38b86936b476b1/cyclonedx_python_lib-11.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/f3/4888f895c02afa085630a3a3329d1b18b998874642ad4c530e9a4d7851fe/pip_audit-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda build_number: 20 @@ -230,6 +423,22 @@ packages: license_family: APACHE size: 98801 timestamp: 1772015926539 +- pypi: https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl + name: boolean-py + version: '5.0' + sha256: ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9 + requires_dist: + - pytest>=6,!=7.0.0 ; extra == 'testing' + - pytest-xdist>=2 ; extra == 'testing' + - twine ; extra == 'dev' + - build ; extra == 'dev' + - black ; extra == 'linting' + - isort ; extra == 'linting' + - pycodestyle ; extra == 'linting' + - sphinx>=3.3.1 ; extra == 'docs' + - sphinx-rtd-theme>=0.5.0 ; extra == 'docs' + - doc8>=0.8.1 ; extra == 'docs' + - sphinxcontrib-apidoc>=0.3.0 ; extra == 'docs' - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 md5: d2ffd7602c02f2b316fd921d39876885 @@ -291,6 +500,49 @@ packages: purls: [] size: 131039 timestamp: 1776865545798 +- pypi: https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl + name: cachecontrol + version: 0.14.4 + sha256: b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b + requires_dist: + - requests>=2.16.0 + - msgpack>=0.5.2,<2.0.0 + - cachecontrol[filecache,redis] ; extra == 'dev' + - cherrypy ; extra == 'dev' + - cheroot>=11.1.2 ; extra == 'dev' + - codespell ; extra == 'dev' + - furo ; extra == 'dev' + - mypy ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - ruff ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-copybutton ; extra == 'dev' + - types-redis ; extra == 'dev' + - types-requests ; extra == 'dev' + - filelock>=3.8.0 ; extra == 'filecache' + - redis>=2.10.5 ; extra == 'redis' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl + name: certifi + version: 2026.4.22 + sha256: 3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl + name: charset-normalizer + version: 3.4.7 + sha256: 3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl + name: charset-normalizer + version: 3.4.7 + sha256: f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: charset-normalizer + version: 3.4.7 + sha256: e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd + requires_python: '>=3.7' - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 md5: 962b9857ee8e7018c22f2776ffa0b2d7 @@ -352,6 +604,25 @@ packages: license_family: APACHE size: 420154 timestamp: 1773761008665 +- pypi: https://files.pythonhosted.org/packages/30/09/fe0e3bc32bd33707c519b102fc064ad2a2ce5a1b53e2be38b86936b476b1/cyclonedx_python_lib-11.7.0-py3-none-any.whl + name: cyclonedx-python-lib + version: 11.7.0 + sha256: 02fa4f15ddbba21ac9093039f8137c0d1813af7fe88b760c5dcd3311a8da2178 + requires_dist: + - jsonschema[format-nongpl]>=4.25,<5.0 ; extra == 'json-validation' or extra == 'validation' + - license-expression>=30,<31 + - lxml>=4,<7 ; extra == 'validation' or extra == 'xml-validation' + - packageurl-python>=0.11,<2 + - py-serializable>=2.1.0,<3.0.0 + - referencing>=0.28.4 ; extra == 'json-validation' or extra == 'validation' + - sortedcontainers>=2.4.0,<3.0.0 + - typing-extensions>=4.6,<5.0 ; python_full_version < '3.13' + requires_python: '>=3.9,<4.0' +- pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl + name: defusedxml + version: 0.7.1 + sha256: a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*' - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144 md5: 8e662bd460bda79b1ea39194e3c4c9ab @@ -361,6 +632,11 @@ packages: license: MIT and PSF-2.0 size: 21333 timestamp: 1763918099466 +- pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + name: filelock + version: 3.29.0 + sha256: 96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258 + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda sha256: dbbec21a369872c8ebe23cb9a3b9d63638479ee30face165aa0fccc96e93eec3 md5: 7c14f3706e099f8fcd47af2d494616cc @@ -392,6 +668,15 @@ packages: purls: [] size: 12273764 timestamp: 1773822733780 +- pypi: https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl + name: idna + version: '3.13' + sha256: 892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3 + requires_dist: + - ruff>=0.6.2 ; extra == 'all' + - mypy>=1.11.2 ; extra == 'all' + - pytest>=8.3.2 ; extra == 'all' + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda sha256: e1a9e3b1c8fe62dc3932a616c284b5d8cbe3124bbfbedcf4ce5c828cb166ee19 md5: 9614359868482abba1bd15ce465e3c42 @@ -663,6 +948,17 @@ packages: purls: [] size: 106486 timestamp: 1775825663227 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + sha256: fe171ed5cf5959993d43ff72de7596e8ac2853e9021dec0344e583734f1e0843 + md5: 2c21e66f50753a083cbe6b80f38268fa + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 92400 + timestamp: 1769482286018 - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hf3981d6_1.conda sha256: 1096c740109386607938ab9f09a7e9bca06d86770a284777586d6c378b8fb3fd md5: ec88ba8a245855935b871a7324373105 @@ -817,6 +1113,59 @@ packages: purls: [] size: 58347 timestamp: 1774072851498 +- pypi: https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl + name: license-expression + version: 30.4.4 + sha256: 421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4 + requires_dist: + - boolean-py>=4.0 + - pytest>=7.0.1 ; extra == 'dev' + - pytest-xdist>=2 ; extra == 'dev' + - twine ; extra == 'dev' + - ruff ; extra == 'dev' + - sphinx>=5.0.2 ; extra == 'dev' + - sphinx-rtd-theme>=1.0.0 ; extra == 'dev' + - sphinxcontrib-apidoc>=0.4.0 ; extra == 'dev' + - sphinx-reredirects>=0.1.2 ; extra == 'dev' + - doc8>=0.11.2 ; extra == 'dev' + - sphinx-autobuild ; extra == 'dev' + - sphinx-rtd-dark-mode>=1.3.0 ; extra == 'dev' + - sphinx-copybutton ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + name: markdown-it-py + version: 4.2.0 + sha256: 9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + requires_dist: + - mdurl~=0.1 + - psutil ; extra == 'benchmarking' + - pytest ; extra == 'benchmarking' + - pytest-benchmark ; extra == 'benchmarking' + - commonmark~=0.9 ; extra == 'compare' + - markdown~=3.4 ; extra == 'compare' + - mistletoe~=1.0 ; extra == 'compare' + - mistune~=3.0 ; extra == 'compare' + - panflute~=2.3 ; extra == 'compare' + - markdown-it-pyrs ; extra == 'compare' + - linkify-it-py>=1,<3 ; extra == 'linkify' + - mdit-py-plugins>=0.5.0 ; extra == 'plugins' + - gprof2dot ; extra == 'profiling' + - mdit-py-plugins>=0.5.0 ; extra == 'rtd' + - myst-parser ; extra == 'rtd' + - pyyaml ; extra == 'rtd' + - sphinx ; extra == 'rtd' + - sphinx-copybutton ; extra == 'rtd' + - sphinx-design ; extra == 'rtd' + - sphinx-book-theme~=1.0 ; extra == 'rtd' + - jupyter-sphinx ; extra == 'rtd' + - ipykernel ; extra == 'rtd' + - coverage ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - pytest-timeout ; extra == 'testing' + - requests ; extra == 'testing' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda sha256: 0c4c35376fe920714390d46e4b8d31c876d65f18e1655899e0763ec25f2a902f md5: 6d03368f2b2b0a5fb6839df53b2eb5e0 @@ -827,6 +1176,11 @@ packages: license_family: MIT size: 69017 timestamp: 1778169663339 +- pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + name: mdurl + version: 0.1.2 + sha256: 84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 + requires_python: '>=3.7' - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda sha256: 78c1bbe1723449c52b7a9df1af2ee5f005209f67e40b6e1d3c7619127c43b1c7 md5: 592132998493b3ff25fd7479396e8351 @@ -836,6 +1190,26 @@ packages: license_family: MIT size: 14465 timestamp: 1733255681319 +- pypi: https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: msgpack + version: 1.1.2 + sha256: fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl + name: msgpack + version: 1.1.2 + sha256: 4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl + name: msgpack + version: 1.1.2 + sha256: a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl + name: msgpack + version: 1.1.2 + sha256: 42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7 + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda sha256: fc89f74bbe362fb29fa3c037697a89bec140b346a2469a90f7936d1d7ea4d8a3 md5: fc21868a1a5aacc937e7a18747acb8a5 @@ -939,6 +1313,24 @@ packages: purls: [] size: 9410183 timestamp: 1775589779763 +- pypi: https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl + name: packageurl-python + version: 0.17.6 + sha256: 31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9 + requires_dist: + - isort ; extra == 'lint' + - black ; extra == 'lint' + - mypy ; extra == 'lint' + - pytest ; extra == 'test' + - setuptools ; extra == 'build' + - wheel ; extra == 'build' + - sqlalchemy>=2.0.0 ; extra == 'sqlalchemy' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + name: packaging + version: '26.2' + sha256: 5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda sha256: 3906abfb6511a3bb309e39b9b1b7bc38f50a723971de2395489fd1f379255890 md5: 4c06a92e74452cfa53623a81592e8934 @@ -969,6 +1361,11 @@ packages: license_family: Apache size: 85207 timestamp: 1762194733167 +- pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + name: pip + version: 26.1.1 + sha256: 99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.1-pyh145f28c_0.conda sha256: 00acccc6f69568ddc56d4d5cc031fdb27eb6a1588a80b1f3a5233bd2a6f944f0 md5: 2e7e59a063366f1fc4f45ac86bd9485f @@ -989,6 +1386,61 @@ packages: license_family: MIT size: 1201616 timestamp: 1777924080196 +- pypi: https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl + name: pip-api + version: 0.0.34 + sha256: 8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb + requires_dist: + - pip + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/be/f3/4888f895c02afa085630a3a3329d1b18b998874642ad4c530e9a4d7851fe/pip_audit-2.10.0-py3-none-any.whl + name: pip-audit + version: 2.10.0 + sha256: 16e02093872fac97580303f0848fa3ad64f7ecf600736ea7835a2b24de49613f + requires_dist: + - cachecontrol[filecache]>=0.13.0 + - cyclonedx-python-lib>=5,<12 + - packaging>=23.0.0 + - pip-api>=0.0.28 + - pip-requirements-parser>=32.0.0 + - requests>=2.31.0 + - rich>=12.4 + - tomli>=2.2.1 + - tomli-w>=1.2.0 + - platformdirs>=4.2.0 + - coverage[toml]~=7.0,!=7.3.3 ; extra == 'cov' + - build ; extra == 'dev' + - pip-audit[doc,test,lint] ; extra == 'dev' + - pdoc ; extra == 'doc' + - ruff>=0.11 ; extra == 'lint' + - interrogate~=1.6 ; extra == 'lint' + - mypy ; extra == 'lint' + - types-requests ; extra == 'lint' + - types-toml ; extra == 'lint' + - pretend ; extra == 'test' + - pytest ; extra == 'test' + - pip-audit[cov] ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl + name: pip-requirements-parser + version: 32.0.1 + sha256: 4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526 + requires_dist: + - packaging + - pyparsing + - sphinx>=3.3.1 ; extra == 'docs' + - sphinx-rtd-theme>=0.5.0 ; extra == 'docs' + - doc8>=0.8.1 ; extra == 'docs' + - pytest>=6,!=7.0.0 ; extra == 'testing' + - pytest-xdist>=2 ; extra == 'testing' + - aboutcode-toolkit>=6.0.0 ; extra == 'testing' + - black ; extra == 'testing' + requires_python: '>=3.6.0' +- pypi: https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl + name: platformdirs + version: 4.9.6 + sha256: e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917 + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda sha256: e14aafa63efa0528ca99ba568eaf506eb55a0371d12e6250aaaa61718d2eb62e md5: d7585b6550ad04c8c5e21097ada2888e @@ -999,6 +1451,20 @@ packages: license_family: MIT size: 25877 timestamp: 1764896838868 +- pypi: https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl + name: py-serializable + version: 2.1.0 + sha256: b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304 + requires_dist: + - defusedxml>=0.7.1,<0.8.0 + requires_python: '>=3.8,<4.0' +- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + name: pygments + version: 2.20.0 + sha256: 81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + requires_dist: + - colorama>=0.4.6 ; extra == 'windows-terminal' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda sha256: cf70b2f5ad9ae472b71235e5c8a736c9316df3705746de419b59d442e8348e86 md5: 16c18772b340887160c79a6acc022db0 @@ -1008,6 +1474,14 @@ packages: license_family: BSD size: 893031 timestamp: 1774796815820 +- pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + name: pyparsing + version: 3.3.2 + sha256: 850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d + requires_dist: + - railroad-diagrams ; extra == 'diagrams' + - jinja2 ; extra == 'diagrams' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda sha256: 960f59442173eee0731906a9077bd5ccf60f4b4226f05a22d1728ab9a21a879c md5: 6a991452eadf2771952f39d43615bb3e @@ -1066,6 +1540,33 @@ packages: license: Python-2.0 size: 31608571 timestamp: 1772730708989 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.13-h6add32d_100_cp313.conda + build_number: 100 + sha256: 7f77eb57648f545c1f58e10035d0d9d66b0a0efb7c4b58d3ed89ec7269afdde1 + md5: 05051be49267378d2fcd12931e319ac3 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.5,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libuuid >=2.42,<3.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.6,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 37358322 + timestamp: 1775614712638 + python_site_packages_path: lib/python3.13/site-packages - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.13-h3d5d122_100_cp313.conda build_number: 100 sha256: 6f71b48fe93ebc0dd42c80358b75020f6ad12ed4772fb3555da36000139c0dc7 @@ -1086,6 +1587,7 @@ packages: - tk >=8.6.13,<8.7.0a0 - tzdata license: Python-2.0 + purls: [] size: 17650454 timestamp: 1775616128232 python_site_packages_path: lib/python3.13/site-packages @@ -1109,6 +1611,7 @@ packages: - tk >=8.6.13,<8.7.0a0 - tzdata license: Python-2.0 + purls: [] size: 12966447 timestamp: 1775615694085 python_site_packages_path: lib/python3.13/site-packages @@ -1132,6 +1635,7 @@ packages: - vc >=14.3,<15 - vc14_runtime >=14.44.35208 license: Python-2.0 + purls: [] size: 16618694 timestamp: 1775613654892 python_site_packages_path: Lib/site-packages @@ -1153,6 +1657,7 @@ packages: - python 3.13.* *_cp313 license: BSD-3-Clause license_family: BSD + purls: [] size: 7002 timestamp: 1752805902938 - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda @@ -1241,6 +1746,27 @@ packages: purls: [] size: 313930 timestamp: 1765813902568 +- pypi: https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl + name: requests + version: 2.33.1 + sha256: 4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a + requires_dist: + - charset-normalizer>=2,<4 + - idna>=2.5,<4 + - urllib3>=1.26,<3 + - certifi>=2023.5.7 + - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' + - chardet>=3.0.2,<8 ; extra == 'use-chardet-on-py3' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + name: rich + version: 15.0.0 + sha256: 33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb + requires_dist: + - ipywidgets>=7.5.1,<9 ; extra == 'jupyter' + - markdown-it-py>=2.2.0 + - pygments>=2.13.0,<3.0.0 + requires_python: '>=3.9.0' - conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda sha256: 3d6ba2c0fcdac3196ba2f0615b4104e532525ffa1335b50a2878be5ff488814a md5: 0242025a3c804966bf71aa04eee82f66 @@ -1325,6 +1851,10 @@ packages: license_family: BSD size: 27064 timestamp: 1775587040128 +- pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl + name: sortedcontainers + version: 2.4.0 + sha256: a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 - conda: https://conda.anaconda.org/conda-forge/noarch/stevedore-5.7.0-pyhd8ed1ab_0.conda sha256: f324eaa50d9249dcef16135d5265db8b72320fd20fefa27253c72ea7f60b0538 md5: 0b99748063ceea9ef335648f038553e7 @@ -1383,6 +1913,26 @@ packages: purls: [] size: 3526350 timestamp: 1769460339384 +- pypi: https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl + name: tomli + version: 2.4.1 + sha256: 36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: tomli + version: 2.4.1 + sha256: f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl + name: tomli + version: 2.4.1 + sha256: eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl + name: tomli + version: 2.4.1 + sha256: 8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda sha256: 91cafdb64268e43e0e10d30bd1bef5af392e69f00edd34dfaf909f69ab2da6bd md5: b5325cf06a000c5b14970462ff5e4d58 @@ -1393,6 +1943,11 @@ packages: license_family: MIT size: 21561 timestamp: 1774492402955 +- pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + name: tomli-w + version: 1.2.0 + sha256: 188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 md5: 0caa1af407ecff61170c9437a808404d @@ -1420,6 +1975,17 @@ packages: purls: [] size: 694692 timestamp: 1756385147981 +- pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + name: urllib3 + version: 2.7.0 + sha256: 9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 + requires_dist: + - brotli>=1.2.0 ; platform_python_implementation == 'CPython' and extra == 'brotli' + - brotlicffi>=1.2.0.0 ; platform_python_implementation != 'CPython' and extra == 'brotli' + - h2>=4,<5 ; extra == 'h2' + - pysocks>=1.5.6,!=1.5.7,<2.0 ; extra == 'socks' + - backports-zstd>=1.0.0 ; python_full_version < '3.14' and extra == 'zstd' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda sha256: 9dc40c2610a6e6727d635c62cced5ef30b7b30123f5ef67d6139e23d21744b3a md5: 1e610f2416b6acdd231c5f573d754a0f diff --git a/pixi.toml b/pixi.toml index d808d23..603f416 100644 --- a/pixi.toml +++ b/pixi.toml @@ -23,11 +23,21 @@ jq = ">=1.6,<2" [target.osx-64.dependencies] jq = ">=1.6,<2" -python = ">=3.11" -pytest = ">=7.0" [tasks] -start = "just start" -stop = "just stop" -status = "just status" -test-unit = "pytest tests/ -v" +start = "just start" +stop = "just stop" +status = "just status" +test = "python -m pytest tests/ -v" + +[feature.lint.dependencies] +python = ">=3.11" + +[feature.lint.pypi-dependencies] +pip-audit = ">=2.8" + +[feature.lint.tasks] +pip-audit = "pip-audit" + +[environments] +lint = { features = ["lint"], no-default-feature = true } diff --git a/scripts/import-dashboards.sh b/scripts/import-dashboards.sh index c60082b..ce1f371 100755 --- a/scripts/import-dashboards.sh +++ b/scripts/import-dashboards.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash # import-dashboards.sh — Import all Grafana dashboard JSON files via the HTTP API. -# Reads GRAFANA_ADMIN_PASSWORD from env (required; set GF_ADMIN_PASSWORD in .env). +# Reads GF_ADMIN_PASSWORD from env (required; set GF_ADMIN_PASSWORD in .env). set -euo pipefail GRAFANA_PORT="${GRAFANA_PORT:-3000}" GRAFANA_URL="http://localhost:${GRAFANA_PORT}" -GRAFANA_ADMIN_PASSWORD="${GRAFANA_ADMIN_PASSWORD:?ERROR: GRAFANA_ADMIN_PASSWORD is not set. Set GF_ADMIN_PASSWORD in .env}" -GRAFANA_AUTH="admin:${GRAFANA_ADMIN_PASSWORD}" +GF_ADMIN_PASSWORD="${GF_ADMIN_PASSWORD:?ERROR: GF_ADMIN_PASSWORD is not set. Set GF_ADMIN_PASSWORD in .env}" +GRAFANA_AUTH="admin:${GF_ADMIN_PASSWORD}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DASHBOARDS_DIR="${SCRIPT_DIR}/../dashboards" diff --git a/tests/test_alertmanager_config.py b/tests/test_alertmanager_config.py index af744bf..0576b5e 100644 --- a/tests/test_alertmanager_config.py +++ b/tests/test_alertmanager_config.py @@ -112,7 +112,13 @@ def test_alertmanager_service_defined(self, compose_config: dict) -> None: def test_alertmanager_image(self, compose_config: dict) -> None: svc = compose_config["services"]["alertmanager"] - assert svc["image"] == "prom/alertmanager:latest" + img = svc["image"] + assert img.startswith("prom/alertmanager:"), ( + f"Expected a prom/alertmanager image with a pinned tag, got: {img!r}" + ) + assert img != "prom/alertmanager:latest", ( + "alertmanager image must use a pinned tag, not :latest" + ) def test_alertmanager_port_exposed(self, compose_config: dict) -> None: svc = compose_config["services"]["alertmanager"] diff --git a/tests/test_configs.py b/tests/test_configs.py index 326e3f9..0c18375 100644 --- a/tests/test_configs.py +++ b/tests/test_configs.py @@ -127,8 +127,16 @@ def test_syslog_job_host_label_has_fallback(self): def test_syslog_host_label_is_not_hardcoded(self): """host label must use env var substitution, not a hardcoded hostname.""" - raw = (CONFIGS_DIR / "promtail.yml").read_text() - assert "hermes" not in raw, "host label must not hardcode 'hermes'" + syslog_job = next( + (j for j in self.config["scrape_configs"] if j.get("job_name") == "syslog"), + None, + ) + assert syslog_job is not None, "syslog scrape job not found" + labels = syslog_job["static_configs"][0]["labels"] + host_val = labels.get("host", "") + assert host_val.startswith("${"), ( + f"host label must use env var substitution, got hardcoded: {host_val!r}" + ) def test_syslog_host_label_uses_env_var(self): """host label must reference HOSTNAME via env var expansion syntax.""" @@ -209,35 +217,34 @@ def _service_networks(self, service_name: str) -> list[str]: return list(nets.keys()) return list(nets) - def test_argus_loki_network_declared(self) -> None: - assert "argus-loki" in self.compose["networks"] + def test_loki_internal_network_declared(self) -> None: + assert "loki-internal" in self.compose["networks"] - def test_argus_loki_network_is_internal(self) -> None: - assert self.compose["networks"]["argus-loki"].get("internal") is True + def test_loki_internal_network_is_internal(self) -> None: + assert self.compose["networks"]["loki-internal"].get("internal") is True - def test_loki_only_on_argus_loki_network(self) -> None: + def test_loki_only_on_loki_internal_network(self) -> None: nets = self._service_networks("loki") - assert "argus-loki" in nets + assert "loki-internal" in nets assert "argus" not in nets, "loki must not be on the argus network (issue #128)" def test_loki_proxy_bridges_both_networks(self) -> None: nets = self._service_networks("loki-proxy") assert "argus" in nets - assert "argus-loki" in nets + assert "loki-internal" in nets - def test_promtail_only_on_argus_loki_network(self) -> None: + def test_promtail_on_loki_internal_network(self) -> None: nets = self._service_networks("promtail") - assert "argus-loki" in nets - assert "argus" not in nets, "promtail must not be on the argus network" + assert "loki-internal" in nets - def test_grafana_not_on_argus_loki_network(self) -> None: + def test_grafana_not_on_loki_internal_network(self) -> None: nets = self._service_networks("grafana") assert "argus" in nets - assert "argus-loki" not in nets, "grafana should reach Loki via loki-proxy only" + assert "loki-internal" not in nets, "grafana should reach Loki via loki-proxy only" - def test_debug_shell_not_on_argus_loki_network(self) -> None: + def test_debug_shell_not_on_loki_internal_network(self) -> None: nets = self._service_networks("debug-shell") - assert "argus-loki" not in nets, "debug-shell must not access the argus-loki network" + assert "loki-internal" not in nets, "debug-shell must not access the loki-internal network" def test_grafana_depends_on_loki_proxy_not_loki(self) -> None: deps: Any = self.compose["services"]["grafana"].get("depends_on", []) @@ -251,12 +258,14 @@ def test_grafana_depends_on_loki_proxy_not_loki(self) -> None: def test_loki_datasource_url_uses_proxy(self) -> None: datasources = load_yaml(CONFIGS_DIR / "grafana" / "datasources.yml")["datasources"] loki_ds = next(ds for ds in datasources if ds["type"] == "loki") - assert loki_ds["url"] == "http://loki-proxy", ( - "Loki datasource must point to loki-proxy, not loki:3100 directly" + assert "loki-proxy" in loki_ds["url"], ( + f"Loki datasource must point to loki-proxy, got: {loki_ds['url']!r}" ) class TestDockerComposePorts(unittest.TestCase): + ALLOWED_BINDINGS = {"127.0.0.1"} + def setUp(self) -> None: self.compose = load_yaml(REPO_ROOT / "docker-compose.yml") self.services = self.compose["services"] @@ -266,8 +275,8 @@ def _ports(self, service: str) -> list[str]: def test_prometheus_port_is_loopback_bound(self) -> None: ports = self._ports("prometheus") - assert any(str(p).startswith("127.0.0.1:9090") for p in ports), ( - f"Prometheus must bind to 127.0.0.1:9090, got: {ports}" + assert any(str(p).startswith("127.0.0.1:") and ":9090" in str(p) for p in ports), ( + f"Prometheus must bind to 127.0.0.1:*:9090, got: {ports}" ) def test_prometheus_port_not_open_bound(self) -> None: @@ -278,20 +287,20 @@ def test_prometheus_port_not_open_bound(self) -> None: def test_grafana_port_is_loopback_bound(self) -> None: ports = self._ports("grafana") - assert any(str(p).startswith("127.0.0.1:3000") for p in ports), ( - f"Grafana must bind to 127.0.0.1:3000, got: {ports}" + assert any(str(p).startswith("127.0.0.1:") for p in ports), ( + f"Grafana must bind to 127.0.0.1, got: {ports}" ) def test_grafana_port_not_open_bound(self) -> None: ports = self._ports("grafana") - assert not any(str(p) == "3000:3000" for p in ports), ( - f"Grafana must not bind to 0.0.0.0:3000, got: {ports}" + assert not any(str(p) == "3000:3000" or str(p) == "3001:3000" for p in ports), ( + f"Grafana must not bind to 0.0.0.0, got: {ports}" ) def test_exporter_port_is_loopback_bound(self) -> None: ports = self._ports("argus-exporter") - assert any(str(p).startswith("127.0.0.1:9100") for p in ports), ( - f"argus-exporter must bind to 127.0.0.1:9100, got: {ports}" + assert any(str(p).startswith("127.0.0.1:") and ":9100" in str(p) for p in ports), ( + f"argus-exporter must bind to 127.0.0.1:*:9100, got: {ports}" ) def test_grafana_anonymous_access_disabled(self) -> None: diff --git a/tests/test_exporter.py b/tests/test_exporter.py index 0343aeb..5860927 100644 --- a/tests/test_exporter.py +++ b/tests/test_exporter.py @@ -475,7 +475,7 @@ def test_all_upstreams_down_still_has_help(self): always_present = [ "hi_agamemnon_health", "hi_nestor_health", - "homeric_exporter_scrape_timestamp", + "homeric_exporter_scrape_timestamp_seconds", "homeric_exporter_scrape_duration_seconds", "homeric_exporter_fetch_errors_total", ]