From d7a28d6bebcf25487386b9344a585c56e9cf6e69 Mon Sep 17 00:00:00 2001 From: Jan Kadlec Date: Tue, 1 Sep 2026 14:00:35 +0200 Subject: [PATCH 1/3] build: harden uv workspace configuration Fixes several ways the uv setup was already broken or could break silently. Workspace members are now listed explicitly instead of matching `packages/*`. Any stray directory under packages/ -- tool output, scratch, build leftovers -- matched that glob and, having no pyproject.toml, broke every uv command in the repo with "missing a pyproject.toml". The Docker image installs the tox group from uv.lock via `uv export` instead of `uv pip install --group`, which re-resolves fresh from the index and ignores the lock. That re-resolution is why the tox group carried its own `uv~=0.12.0` pin, duplicating [tool.uv] required-version; the pin is removed, so the export and the removal must stay in the same commit. Lock hashes are no longer stripped, so the install is hash-verified at no measurable cost. `constraint-dependencies` now bounds the locked uv. tox-uv depends on the uv PyPI package with no upper bound, and the image takes uv's version straight from the lock, so a plain `uv lock --upgrade` could have put uv 0.13.x in the image and tripped required-version at runtime -- the same failure the removed pin guarded, reached by a different route. UV_PROJECT_ENVIRONMENT redirects the in-container project environment. The repo is bind-mounted at /data, so `uv run` inside the test container was rebuilding the developer's host .venv against the container's Linux interpreter, leaving the host with a venv pointing at a non-existent python. UV_LOCKED additionally stops any in-container uv command rewriting the mounted uv.lock. `TOX ?= uv run tox` in project_common.mk, overridden to bare `tox` in the image, so container runs use the lock-pinned system tox already installed there instead of first syncing the whole workspace into a throwaway env to obtain the same tox (measured: 58 packages, ~7s per container). default-groups covers dev/lint/type/test/tox so a bare `uv sync` produces a venv that can run every make target. Previously only `dev` was installed, so lint/format/type-check failed outright and `make test` silently fell through to whatever tox was on PATH -- a system tox without the tox-uv plugin then dies on "runner 'uv-venv-lock-runner' is not available". Accepted cost: the three CI steps that use `uv sync --group ...` widen from two groups to five, about 9 packages and 86MB per job; the `--only-group` steps are unaffected. The root test group gains jsonschema and drops the version bounds that each member's own test group already declares. Without jsonschema, gooddata-flexconnect's tests could not even be collected from the root venv, which is the environment scripts/validate_python.sh uses for filtered runs. gooddata-pipelines' test group loses its Poetry-style specifier syntax while keeping its major-version ceilings. The staging make targets use `uv run --locked python` rather than bare `python`; the staging workflow syncs .venv but never activates it, so those two scripts ran without orjson, pyyaml or requests. Dead `wheel_build_env` is removed from the eight package tox.ini files. tox itself reported it as an unused key: uv-venv-lock-runner installs from the lock and never builds a package env, and no .pkg env is created in practice. pre-commit's ruff is pinned to v0.15.20 to match the lint group, which was already resolving to 0.15.20 while the hook ran 0.15.1. jira: trivial risk: low --- .pre-commit-config.yaml | 2 +- Dockerfile | 31 ++++++++--- Makefile | 4 +- packages/gooddata-dbt/tox.ini | 1 - packages/gooddata-eval/tox.ini | 1 - packages/gooddata-fdw/tox.ini | 1 - packages/gooddata-flexconnect/tox.ini | 1 - packages/gooddata-flight-server/tox.ini | 1 - packages/gooddata-pandas/tox.ini | 1 - packages/gooddata-pipelines/pyproject.toml | 8 +-- packages/gooddata-pipelines/tox.ini | 1 - packages/gooddata-sdk/tox.ini | 1 - project_common.mk | 8 ++- pyproject.toml | 63 ++++++++++++++-------- uv.lock | 29 +++++----- 15 files changed, 93 insertions(+), 60 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fbca85b87..940e28c65 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: args: [ '--maxkb=890' ] - id: check-case-conflict - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.1 + rev: v0.15.20 hooks: # Run the linter. - id: ruff diff --git a/Dockerfile b/Dockerfile index 5091ae954..707d178d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,16 +40,35 @@ WORKDIR /data COPY pyproject.toml uv.lock ./ # Install tox and tox-uv as system packages so they're available globally. -# NOTE: `uv pip install --group` reads the group's requirements from pyproject.toml but -# resolves them FRESH from the index -- it does NOT read uv.lock. Every version that must -# stay fixed therefore needs an explicit bound in the group itself; in particular `uv`, -# whose console script installs over the binary copied above. +# Via `uv export` and not `uv pip install --group`: the latter re-resolves fresh from the +# index, while export reads uv.lock, so the image gets exactly the pinned versions. +# NOTE: tox-uv's `uv` dependency installs a console script over the binary COPYed above; +# [tool.uv] constraint-dependencies keeps the locked version inside required-version. # Clean up dependency files after installation to reduce image size RUN set -x \ - && uv pip install --system --group tox \ - && rm -f pyproject.toml uv.lock \ + && uv export --frozen --only-group tox -o /tmp/tox-requirements.txt \ + && uv pip install --system -r /tmp/tox-requirements.txt \ + && rm -f pyproject.toml uv.lock /tmp/tox-requirements.txt \ && true +# Any uv command here must not REWRITE the bind-mounted host uv.lock if it thinks it is +# stale -- fail instead. Not UV_FROZEN: tox-uv reads that and downgrades its own --locked +# to --frozen, silently accepting a stale lock. Must be set AFTER the export above, which +# is rejected in combination with UV_LOCKED and has to stay --frozen because only the root +# pyproject.toml and uv.lock exist at that layer for --locked to validate against. +ENV UV_LOCKED=1 + +# Use the lock-pinned tox installed system-wide above rather than project_common.mk's +# default `uv run tox`, which would first sync the whole workspace into a throwaway +# in-container project env (measured: 58 packages, ~7s) just to obtain the same tox. +ENV TOX=tox + +# The repo is bind-mounted at /data, so the default project environment (/data/.venv) is +# the developer's host venv; a `uv run` here would rebuild it against this image's Linux +# interpreter. Redirect it somewhere container-local (/tmp, not a home dir: the runtime +# user is created by entrypoint.sh, so no home exists when this ENV is evaluated). +ENV UV_PROJECT_ENVIRONMENT=/tmp/uv-project-venv + COPY .docker/entrypoint.sh /entrypoint.sh LABEL image_name="GoodData Python SDK test image with python, tox and make" diff --git a/Makefile b/Makefile index 7d5139fab..335453c62 100644 --- a/Makefile +++ b/Makefile @@ -104,13 +104,13 @@ test-staging: clean-staging: @test -n "$(STAGING_ADMIN_TOKEN)" || (echo "ERROR: STAGING_ADMIN_TOKEN is required. Set it in .env or pass on CLI." && exit 1) @test -n "$(STAGING_DS_PASSWORD)" || (echo "ERROR: STAGING_DS_PASSWORD is required. Set it in .env or pass on CLI." && exit 1) - cd packages/tests-support && STAGING=1 TOKEN="$(STAGING_ADMIN_TOKEN)" DS_PASSWORD="$(STAGING_DS_PASSWORD)" python clean_staging.py + cd packages/tests-support && STAGING=1 TOKEN="$(STAGING_ADMIN_TOKEN)" DS_PASSWORD="$(STAGING_DS_PASSWORD)" uv run --locked python clean_staging.py .PHONY: load-staging load-staging: @test -n "$(STAGING_ADMIN_TOKEN)" || (echo "ERROR: STAGING_ADMIN_TOKEN is required. Set it in .env or pass on CLI." && exit 1) @test -n "$(STAGING_DS_PASSWORD)" || (echo "ERROR: STAGING_DS_PASSWORD is required. Set it in .env or pass on CLI." && exit 1) - cd packages/tests-support && STAGING=1 TOKEN="$(STAGING_ADMIN_TOKEN)" DS_PASSWORD="$(STAGING_DS_PASSWORD)" python upload_demo_layout.py + cd packages/tests-support && STAGING=1 TOKEN="$(STAGING_ADMIN_TOKEN)" DS_PASSWORD="$(STAGING_DS_PASSWORD)" uv run --locked python upload_demo_layout.py .PHONY: release release: diff --git a/packages/gooddata-dbt/tox.ini b/packages/gooddata-dbt/tox.ini index e448632ad..60e6f16db 100644 --- a/packages/gooddata-dbt/tox.ini +++ b/packages/gooddata-dbt/tox.ini @@ -5,7 +5,6 @@ envlist = py3{10,11,12,13,14} [testenv] runner = uv-venv-lock-runner package = wheel -wheel_build_env = .pkg dependency_groups = test setenv = diff --git a/packages/gooddata-eval/tox.ini b/packages/gooddata-eval/tox.ini index f82a78049..27694bc9e 100644 --- a/packages/gooddata-eval/tox.ini +++ b/packages/gooddata-eval/tox.ini @@ -5,7 +5,6 @@ envlist = py3{10,11,12,13,14} [testenv] runner = uv-venv-lock-runner package = wheel -wheel_build_env = .pkg extras = llm-judge dependency_groups = diff --git a/packages/gooddata-fdw/tox.ini b/packages/gooddata-fdw/tox.ini index f408517d5..254cffb58 100644 --- a/packages/gooddata-fdw/tox.ini +++ b/packages/gooddata-fdw/tox.ini @@ -5,7 +5,6 @@ envlist = py3{10,11,12,13,14} [testenv] runner = uv-venv-lock-runner package = wheel -wheel_build_env = .pkg dependency_groups = test setenv = diff --git a/packages/gooddata-flexconnect/tox.ini b/packages/gooddata-flexconnect/tox.ini index a92cdcbb3..7bad33f51 100644 --- a/packages/gooddata-flexconnect/tox.ini +++ b/packages/gooddata-flexconnect/tox.ini @@ -5,7 +5,6 @@ envlist = py3{10,11,12,13,14} [testenv] runner = uv-venv-lock-runner package = wheel -wheel_build_env = .pkg dependency_groups = test setenv = diff --git a/packages/gooddata-flight-server/tox.ini b/packages/gooddata-flight-server/tox.ini index a92cdcbb3..7bad33f51 100644 --- a/packages/gooddata-flight-server/tox.ini +++ b/packages/gooddata-flight-server/tox.ini @@ -5,7 +5,6 @@ envlist = py3{10,11,12,13,14} [testenv] runner = uv-venv-lock-runner package = wheel -wheel_build_env = .pkg dependency_groups = test setenv = diff --git a/packages/gooddata-pandas/tox.ini b/packages/gooddata-pandas/tox.ini index f408517d5..254cffb58 100644 --- a/packages/gooddata-pandas/tox.ini +++ b/packages/gooddata-pandas/tox.ini @@ -5,7 +5,6 @@ envlist = py3{10,11,12,13,14} [testenv] runner = uv-venv-lock-runner package = wheel -wheel_build_env = .pkg dependency_groups = test setenv = diff --git a/packages/gooddata-pipelines/pyproject.toml b/packages/gooddata-pipelines/pyproject.toml index 897f63eb9..be6cd253a 100644 --- a/packages/gooddata-pipelines/pyproject.toml +++ b/packages/gooddata-pipelines/pyproject.toml @@ -25,12 +25,12 @@ line-length = 80 [dependency-groups] test = [ - "pytest (>=8.3.5,<9.0.0)", + "pytest~=8.3.4", "pytest-cov~=6.0.0", "pytest-json-report==1.5.0", - "pytest-mock (>=3.14.0,<4.0.0)", - "moto (>=5.1.6,<6.0.0)", - "orjson (>=3.11.3,<4.0.0)", + "pytest-mock>=3.14.0,<4.0.0", + "moto>=5.1.6,<6.0.0", + "orjson>=3.11.3,<4.0.0", ] [tool.ty.analysis] diff --git a/packages/gooddata-pipelines/tox.ini b/packages/gooddata-pipelines/tox.ini index f66e67bb5..849f3202a 100644 --- a/packages/gooddata-pipelines/tox.ini +++ b/packages/gooddata-pipelines/tox.ini @@ -5,7 +5,6 @@ envlist = py3{10,11,12,13,14} [testenv] runner = uv-venv-lock-runner package = wheel -wheel_build_env = .pkg dependency_groups = test setenv = diff --git a/packages/gooddata-sdk/tox.ini b/packages/gooddata-sdk/tox.ini index 0313f62ed..986f85943 100644 --- a/packages/gooddata-sdk/tox.ini +++ b/packages/gooddata-sdk/tox.ini @@ -5,7 +5,6 @@ envlist = py3{10,11,12,13,14} [testenv] runner = uv-venv-lock-runner package = wheel -wheel_build_env = .pkg dependency_groups = test pass_env = diff --git a/project_common.mk b/project_common.mk index f74e344b5..2b27aa0f5 100644 --- a/project_common.mk +++ b/project_common.mk @@ -5,6 +5,10 @@ ROOT_DIR = ../.. RUFF = ./.venv/bin/ruff # ty needs uv run (unlike ruff) because it resolves imports from installed packages TY = uv run ty +# `uv run` so a host dev gets tox+tox-uv from the lock without activating the venv. The +# test image overrides this to bare `tox` (it installs the same lock-pinned versions +# system-wide), which skips an entire redundant project sync inside every container. +TOX ?= uv run tox PKG_PATH = packages/$(CURR_DIR_BASE_NAME) TOX_FLAGS = @@ -59,7 +63,7 @@ types: type-check .PHONY: test test: - uv run tox -v $(TOX_FLAGS) $(LOCAL_TEST_ENVS) $(LOCAL_ADD_ARGS) + $(TOX) -v $(TOX_FLAGS) $(LOCAL_TEST_ENVS) $(LOCAL_ADD_ARGS) .PHONY: test-ci test-ci: @@ -69,7 +73,7 @@ test-ci: .PHONY: test-staging test-staging: @test -n "$(TOKEN)" || (echo "ERROR: TOKEN is required." && exit 1) - TOKEN=$(TOKEN) DS_PASSWORD=$(DS_PASSWORD) GD_TEST_ENV=staging uv run tox -v $(TOX_FLAGS) $(LOCAL_TEST_ENVS) $(LOCAL_ADD_ARGS) + TOKEN=$(TOKEN) DS_PASSWORD=$(DS_PASSWORD) GD_TEST_ENV=staging $(TOX) -v $(TOX_FLAGS) $(LOCAL_TEST_ENVS) $(LOCAL_ADD_ARGS) # this is effective for gooddata-sdk only now - it should be part of test fixtures # remove this target once implemented in pytest global fixture diff --git a/pyproject.toml b/pyproject.toml index 41edef734..4728c60c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,14 @@ dependencies = [ [tool.uv] required-version = "~=0.12.0" +# The root only aggregates the members; it is never built or published. +package = false +# So a bare `uv sync` can run every make target (ruff, ty, pytest, tox). Not "all": +# `release` would then be installed by every CI `uv sync --group ...` too. +default-groups = ["dev", "lint", "type", "test", "tox"] +# tox-uv depends on `uv` with no upper bound and its console script replaces the uv binary +# in the Docker image; keep the locked uv inside required-version above. +constraint-dependencies = ["uv~=0.12.0"] [tool.uv.sources] gooddata-sdk = { workspace = true } @@ -34,7 +42,20 @@ gooddata-api-client = { path = "gooddata-api-client", editable = true } [tool.uv.workspace] # note gooddata-api-client is not a workspace package, it is a dependency of other packages -members = ["packages/*"] +# Listed explicitly, not as a "packages/*" glob: any stray directory under packages/ would +# match and, lacking a pyproject.toml, break every uv command in the repo. A new package +# needs three entries -- here, [project].dependencies above, and [tool.uv.sources]. +members = [ + "packages/gooddata-dbt", + "packages/gooddata-eval", + "packages/gooddata-fdw", + "packages/gooddata-flexconnect", + "packages/gooddata-flight-server", + "packages/gooddata-pandas", + "packages/gooddata-pipelines", + "packages/gooddata-sdk", + "packages/tests-support", +] [dependency-groups] dev = [ @@ -48,27 +69,29 @@ type = [ "ty~=0.0.55", ] test = [ - # Common test dependencies used across all workspace packages + # Declared only here: consumed by the scripts/docs/ tests (`make test-docs-scripts`). "pytest~=8.3.4", - # Required by scripts/docs/ tests "toml~=0.10.2", "griffe>=1.0", "docstring_parser~=0.15", "jinja2~=3.1", - "pytest-cov~=6.0.0", - "pytest-json-report==1.5.0", - # Additional test dependencies used by multiple packages - "pytest-snapshot==0.9.0", - "pytest-order~=1.3.0", - "vcrpy~=8.2.1", - "urllib3~=2.6.0", - "python-dotenv~=1.0.0", - "deepdiff~=8.5.0", - "pytest-mock>=3.14.0", - # Package-specific dependencies (only needed by some packages) - # but included here for convenience when working on the full workspace - "moto>=5.1.6", - "orjson>=3.11.3", + # Mirror of the members' `test` groups so the root venv can run any package's tests + # (`uv run pytest -k ...`, see scripts/validate_python.sh). Add to it when a member + # adds a test dependency, or that member's tests stop collecting from the root venv. + # Unbounded on purpose: the authoritative bounds live in each member's own group, and + # the single workspace lock resolves one version for everything anyway. + "pytest-cov", + "jsonschema", + "pytest-json-report", + "pytest-snapshot", + "pytest-order", + "pytest-mock", + "vcrpy", + "urllib3", + "python-dotenv", + "deepdiff", + "moto", + "orjson", ] release = [ "tbump~=6.11.0", @@ -77,12 +100,6 @@ release = [ tox = [ "tox~=4.56.1", "tox-uv~=1.35.2", - # tox-uv depends on the uv PyPI package without a version bound, and the Dockerfile - # installs this group with `uv pip install`, which resolves fresh instead of reading - # uv.lock. Without this bound the resolver picks the newest uv, whose console script - # then shadows the pinned binary in the image and trips required-version at runtime. - # Keep in sync with [tool.uv] required-version above. - "uv~=0.12.0", ] [tool.ruff] diff --git a/uv.lock b/uv.lock index b053de143..d02699352 100644 --- a/uv.lock +++ b/uv.lock @@ -20,6 +20,7 @@ members = [ "gooddata-sdk", "tests-support", ] +constraints = [{ name = "uv", specifier = "~=0.12.0" }] [[package]] name = "annotated-types" @@ -1092,7 +1093,7 @@ requires-dist = [ test = [ { name = "moto", specifier = ">=5.1.6,<6.0.0" }, { name = "orjson", specifier = ">=3.11.3,<4.0.0" }, - { name = "pytest", specifier = ">=8.3.5,<9.0.0" }, + { name = "pytest", specifier = "~=8.3.4" }, { name = "pytest-cov", specifier = "~=6.0.0" }, { name = "pytest-json-report", specifier = "==1.5.0" }, { name = "pytest-mock", specifier = ">=3.14.0,<4.0.0" }, @@ -1132,6 +1133,7 @@ test = [ { name = "docstring-parser" }, { name = "griffe" }, { name = "jinja2" }, + { name = "jsonschema" }, { name = "moto" }, { name = "orjson" }, { name = "pytest" }, @@ -1148,7 +1150,6 @@ test = [ tox = [ { name = "tox" }, { name = "tox-uv" }, - { name = "uv" }, ] type = [ { name = "ty" }, @@ -1179,27 +1180,27 @@ release = [ { name = "tomlkit", specifier = ">=0.11" }, ] test = [ - { name = "deepdiff", specifier = "~=8.5.0" }, + { name = "deepdiff" }, { name = "docstring-parser", specifier = "~=0.15" }, { name = "griffe", specifier = ">=1.0" }, { name = "jinja2", specifier = "~=3.1" }, - { name = "moto", specifier = ">=5.1.6" }, - { name = "orjson", specifier = ">=3.11.3" }, + { name = "jsonschema" }, + { name = "moto" }, + { name = "orjson" }, { name = "pytest", specifier = "~=8.3.4" }, - { name = "pytest-cov", specifier = "~=6.0.0" }, - { name = "pytest-json-report", specifier = "==1.5.0" }, - { name = "pytest-mock", specifier = ">=3.14.0" }, - { name = "pytest-order", specifier = "~=1.3.0" }, - { name = "pytest-snapshot", specifier = "==0.9.0" }, - { name = "python-dotenv", specifier = "~=1.0.0" }, + { name = "pytest-cov" }, + { name = "pytest-json-report" }, + { name = "pytest-mock" }, + { name = "pytest-order" }, + { name = "pytest-snapshot" }, + { name = "python-dotenv" }, { name = "toml", specifier = "~=0.10.2" }, - { name = "urllib3", specifier = "~=2.6.0" }, - { name = "vcrpy", specifier = "~=8.2.1" }, + { name = "urllib3" }, + { name = "vcrpy" }, ] tox = [ { name = "tox", specifier = "~=4.56.1" }, { name = "tox-uv", specifier = "~=1.35.2" }, - { name = "uv", specifier = "~=0.12.0" }, ] type = [{ name = "ty", specifier = "~=0.0.55" }] From a1b29d917d1bfb916a4d4e1bf445e9e430c1a9f8 Mon Sep 17 00:00:00 2001 From: Jan Kadlec Date: Tue, 1 Sep 2026 16:07:13 +0200 Subject: [PATCH 2/3] build: use tox-uv-bare and drop the 58MB uv wheel from the test image tox-uv is a thin wrapper: the plugin itself lives in tox-uv-bare, and the only thing the wrapper adds is a dependency on the `uv` PyPI package. That package is a 58MB binary the test image already has (COPYed from ghcr.io/astral-sh/uv) and the host already has on PATH, so installing it again bought nothing except an overwrite of the COPYed binary. Dropping it removes two distributions from the lock and shrinks each test image from 378MB to 302MB. `tox --version` still reports the plugin registered, since tox-uv-bare was always the thing providing it. Because the wrapper's `uv` dependency is gone, [tool.uv] constraint-dependencies has nothing left to constrain and is removed. That also inverts the earlier argument for leaving the base image tag floating: the COPY is now the image's only source of uv rather than something the lock overwrote, so it is pinned to an exact 0.12.5 instead of the 0.12 tag. Also adds a .gitignore to gooddata-eval, the only package without one. Its sibling packages all ignore .tox/, .coverage, coverage.xml and the json report; without them, `make test` in that package leaves the artifacts staged for an accidental commit. jira: trivial risk: low --- Dockerfile | 6 ++--- packages/gooddata-eval/.gitignore | 8 ++++++ pyproject.toml | 8 +++--- uv.lock | 43 ++----------------------------- 4 files changed, 17 insertions(+), 48 deletions(-) create mode 100644 packages/gooddata-eval/.gitignore diff --git a/Dockerfile b/Dockerfile index 707d178d9..9bd721f33 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # (C) 2021 GoodData Corporation ARG PY_TAG -FROM ghcr.io/astral-sh/uv:0.12 AS uv +FROM ghcr.io/astral-sh/uv:0.12.5 AS uv FROM python:${PY_TAG} ARG PY_TAG @@ -42,8 +42,8 @@ COPY pyproject.toml uv.lock ./ # Install tox and tox-uv as system packages so they're available globally. # Via `uv export` and not `uv pip install --group`: the latter re-resolves fresh from the # index, while export reads uv.lock, so the image gets exactly the pinned versions. -# NOTE: tox-uv's `uv` dependency installs a console script over the binary COPYed above; -# [tool.uv] constraint-dependencies keeps the locked version inside required-version. +# The group uses tox-uv-bare, so nothing here installs a `uv` console script over the +# binary COPYed above -- that COPY is the image's only uv, hence its exact pin. # Clean up dependency files after installation to reduce image size RUN set -x \ && uv export --frozen --only-group tox -o /tmp/tox-requirements.txt \ diff --git a/packages/gooddata-eval/.gitignore b/packages/gooddata-eval/.gitignore new file mode 100644 index 000000000..53f427c46 --- /dev/null +++ b/packages/gooddata-eval/.gitignore @@ -0,0 +1,8 @@ +# (C) 2026 GoodData Corporation + +# Unit test / coverage reports +.tox/ +.coverage +.coverage.* +coverage.xml +.json-report-*.json diff --git a/pyproject.toml b/pyproject.toml index 4728c60c4..9848798dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,9 +24,6 @@ package = false # So a bare `uv sync` can run every make target (ruff, ty, pytest, tox). Not "all": # `release` would then be installed by every CI `uv sync --group ...` too. default-groups = ["dev", "lint", "type", "test", "tox"] -# tox-uv depends on `uv` with no upper bound and its console script replaces the uv binary -# in the Docker image; keep the locked uv inside required-version above. -constraint-dependencies = ["uv~=0.12.0"] [tool.uv.sources] gooddata-sdk = { workspace = true } @@ -99,7 +96,10 @@ release = [ ] tox = [ "tox~=4.56.1", - "tox-uv~=1.35.2", + # tox-uv-bare carries the actual plugin; the `tox-uv` wrapper only adds a dependency on + # the 58MB `uv` PyPI package, which the image already has as a binary and the host has + # on PATH. Requires `uv` to be on PATH, which holds in both places. + "tox-uv-bare~=1.35.2", ] [tool.ruff] diff --git a/uv.lock b/uv.lock index d02699352..f0b31b521 100644 --- a/uv.lock +++ b/uv.lock @@ -20,7 +20,6 @@ members = [ "gooddata-sdk", "tests-support", ] -constraints = [{ name = "uv", specifier = "~=0.12.0" }] [[package]] name = "annotated-types" @@ -1149,7 +1148,7 @@ test = [ ] tox = [ { name = "tox" }, - { name = "tox-uv" }, + { name = "tox-uv-bare" }, ] type = [ { name = "ty" }, @@ -1200,7 +1199,7 @@ test = [ ] tox = [ { name = "tox", specifier = "~=4.56.1" }, - { name = "tox-uv", specifier = "~=1.35.2" }, + { name = "tox-uv-bare", specifier = "~=1.35.2" }, ] type = [{ name = "ty", specifier = "~=0.0.55" }] @@ -2970,18 +2969,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/97/560a5dfde154619d9643b1e208119dddc29bbb35a38a4ce4d095c16cf8f0/tox-4.56.1-py3-none-any.whl", hash = "sha256:4d06b925c4dd67872099b39c5a46fba79a2169c5f6e32060f95a8b1181f0ef55", size = 216469, upload-time = "2026-06-25T06:18:35.229Z" }, ] -[[package]] -name = "tox-uv" -version = "1.35.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tox-uv-bare" }, - { name = "uv" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/dc/6e9994c799bdbb309f829dd6b8d98764dd0757302f3433c380438a3a127b/tox_uv-1.35.2-py3-none-any.whl", hash = "sha256:2d99b0e3c782ba49e7cbe521c8d344758595961b17a3633738d67096641c1bde", size = 6565, upload-time = "2026-05-05T01:34:16.07Z" }, -] - [[package]] name = "tox-uv-bare" version = "1.35.2" @@ -3120,32 +3107,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] -[[package]] -name = "uv" -version = "0.12.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/b0/3085b844fe59aa319a3f94a5cca9938fffecc82705aa9c2762a749f7095c/uv-0.12.5.tar.gz", hash = "sha256:442a21d181faae21742aaaf6d2091a0d27755d3eac344061a9a00c90169b7524", size = 7101936, upload-time = "2026-08-14T19:56:57.693Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/4c/6412d4a618230db699118b362ec41c54795f93992b43c53e225bd0213501/uv-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:2bd62134e56af35b9cf017aaf8ae41a605d6501dd49afc35b70b544a45dd8354", size = 23310055, upload-time = "2026-08-14T19:55:51.357Z" }, - { url = "https://files.pythonhosted.org/packages/bd/ec/d76387b388fa21620088b89b9c67f2596a707add585104e0cb5e8abf55f2/uv-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1a06c8bc4d43b5f6c1e3f2ae3d0f6455b07515f762516f95e52e6c0cbccedf15", size = 21401335, upload-time = "2026-08-14T19:55:55.371Z" }, - { url = "https://files.pythonhosted.org/packages/6d/bc/81ab953b7261ae6be40874b1f283a10873871e02eb353d354614dd8da96b/uv-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d87156bc174d94fae890bb7a261e2867140abb9fe1e9de81a5295e582fb9d0f5", size = 19290641, upload-time = "2026-08-14T19:55:58.998Z" }, - { url = "https://files.pythonhosted.org/packages/7d/13/07585043c10e648820bf826474dac46864ce6691da5dc52fee43c5c7523a/uv-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2d65b7b3bc3fd28678f62aa7fb5d90f106ad9782c1354af60b6cecdf9ea9ecd9", size = 22245569, upload-time = "2026-08-14T19:56:02.729Z" }, - { url = "https://files.pythonhosted.org/packages/3e/6d/310f8f56f8d001b4000112a09d7b7de80fb2024a90208fabb9ddc457c123/uv-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:712624b62e25c84e5a10fc6aa144d8a81b685fdc067a54a7ca4367d75d2cf791", size = 22745152, upload-time = "2026-08-14T19:56:06.426Z" }, - { url = "https://files.pythonhosted.org/packages/92/da/7922b67eec5ee03e94333c5841b682c335033ee80acac17c3417bd752656/uv-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9656ac7a00fd4314980fb0f790df1c1f3fa9cbcf9af9c6f611b19448b9da687", size = 22787947, upload-time = "2026-08-14T19:56:10.149Z" }, - { url = "https://files.pythonhosted.org/packages/62/55/5dbaed832a4b36809ef8a07c8e56e9fee0dedb0aa0454f6d232b6e468f2c/uv-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:568485b44e848eb3693f85d6b00299ccd8fc4d26902030dbf24f549c276db9ca", size = 23367616, upload-time = "2026-08-14T19:56:13.768Z" }, - { url = "https://files.pythonhosted.org/packages/11/77/baf761d12bb66efb01706e3bbb5926ed0d13cb0a40539a661fcfffd46de4/uv-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd08c82831b0033330f8eeeb0d90f938a4d999f25569bee68a975c736142d795", size = 24586263, upload-time = "2026-08-14T19:56:17.57Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a8/76c1031c4834c959bb8a8059c9feabeaa77488ce8b6a3529d6d929ae81cf/uv-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:edd9ff6154b891146a342c143cd29b330ad97ac6a4b20ff4a99a20a4da84ceca", size = 24160655, upload-time = "2026-08-14T19:56:21.568Z" }, - { url = "https://files.pythonhosted.org/packages/93/22/dacc9a0bc8604187a1ba954a3aef8329e4104eb0af772d2c3c634893bd9b/uv-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e195ccf1ed60c8bb24a6447ce306441a4181d54b602407e09bc56e963911c15", size = 23657089, upload-time = "2026-08-14T19:56:25.144Z" }, - { url = "https://files.pythonhosted.org/packages/39/98/e8f9c071622f2cb4072d8b587d27b27d23cf0d3ebf8b3687f5af6030f587/uv-0.12.5-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:58abfb0f658b39a834307a11223bc170294ea214263b4c99ecc7663720d43544", size = 22379954, upload-time = "2026-08-14T19:56:28.789Z" }, - { url = "https://files.pythonhosted.org/packages/73/95/4c3f060e95f7cbe9177b4ab361f0cbfc4ae22e5a49b22e73eee9f0d0a6ca/uv-0.12.5-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:6ad2c455f1fe4d2962f6fd7ccb3b1f61c61856681c9d99f40e170b2074353fa3", size = 23318163, upload-time = "2026-08-14T19:56:32.504Z" }, - { url = "https://files.pythonhosted.org/packages/a0/96/ca0497ef8912ef48dbbc9982a8b4212260c34d56bfd0d45fe67b31942121/uv-0.12.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a05b497c2a948c8600f4c831a89852b4d2514b7f561074225cc9edd0cc4811e2", size = 23470437, upload-time = "2026-08-14T19:56:36.525Z" }, - { url = "https://files.pythonhosted.org/packages/60/e7/8bdc37669a6cd2b46a2ec08ccbb58c61395ec84a073e199f5a4a64bb998f/uv-0.12.5-py3-none-musllinux_1_1_i686.whl", hash = "sha256:7817f8e957960f9ddc452ea353f283c0d6393e2e31b400276485adced5b1f371", size = 22545803, upload-time = "2026-08-14T19:56:40.606Z" }, - { url = "https://files.pythonhosted.org/packages/37/cc/01e39e1dbeb838a6b3c26bf97c867d6f366459b22a38bea691af8c6c94c0/uv-0.12.5-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:dc14e4f81a99b585a891350c60d1ff4557d54cb3c3c81fa45fd4e0dd512ba752", size = 23874113, upload-time = "2026-08-14T19:56:44.193Z" }, - { url = "https://files.pythonhosted.org/packages/0a/38/9053599a73a351d1cd34195c7a48c1db4d4d51b57b543607fad7ecf9354c/uv-0.12.5-py3-none-win32.whl", hash = "sha256:39bb102766c95571781a7b4c611675ea213e08df5c680f3936279b3c0d1f6c3c", size = 20744641, upload-time = "2026-08-14T19:56:47.689Z" }, - { url = "https://files.pythonhosted.org/packages/ce/f6/a9af9311c7f5640ca2bfcfdedb7aca37fa6d1d9f5c981fb50c5be02b7477/uv-0.12.5-py3-none-win_amd64.whl", hash = "sha256:455c3e57602e2141e66e2f0bf685898c9c5e5a70377d14c9a71554a3baf3ddbf", size = 21621812, upload-time = "2026-08-14T19:56:51.126Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fb/e1266399f755f97a0783de379f2fed6dae0a2a240db32fe5a2eb976fec8a/uv-0.12.5-py3-none-win_arm64.whl", hash = "sha256:bea86f27a027e0e3af908db4bdd4f1ceef3ca2bd47673b5ccca7f550e325b1b4", size = 20381876, upload-time = "2026-08-14T19:56:54.883Z" }, -] - [[package]] name = "vcrpy" version = "8.2.1" From b839f9376e7463573e2d268c46de102b495338cc Mon Sep 17 00:00:00 2001 From: Jan Kadlec Date: Tue, 1 Sep 2026 16:09:02 +0200 Subject: [PATCH 3/3] ci: enforce `make lint` and fix the three violations it was hiding The job named `lint-and-format-check` only ever ran `make format`, so ruff's linter was never enforced anywhere except pre-commit -- which runs on changed files only. Three violations reached master as a result, and `make lint` has been failing on a clean checkout. Fixes all three, then adds the missing step so the job does what its name says: - sse_client.py `_is_retryable_exc` ended with an `if isinstance(...): return True` / `return False` pair (SIM103). Collapsed to returning the isinstance directly; the comment explaining why RemoteProtocolError is retryable moves above the return and is unchanged. - test_agentic_runner.py imported `AGENTIC_TEST_KINDS` and `AgenticEvalOutcome` inside two test bodies (PLC0415, twice). Both names were already imported at module top -- `AgenticEvalOutcome` identically, `AGENTIC_TEST_KINDS` from a module the file already imports from -- so the local imports were redundant, not deliberate deferrals. Removed, and AGENTIC_TEST_KINDS added to the existing top-level import. No behaviour change: the isinstance chain returns the same value for every input, and the moved imports resolve to the same objects. jira: trivial risk: low --- .github/workflows/rw-python-tests.yaml | 3 +++ .../src/gooddata_eval/core/chat/sse_client.py | 12 +++++------- packages/gooddata-eval/tests/test_agentic_runner.py | 6 +----- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/workflows/rw-python-tests.yaml b/.github/workflows/rw-python-tests.yaml index 7ed62e817..3bbe39190 100644 --- a/.github/workflows/rw-python-tests.yaml +++ b/.github/workflows/rw-python-tests.yaml @@ -44,6 +44,9 @@ jobs: - name: pep8 and formatting check run: | make format + - name: lint check + run: | + make lint docs-scripts-tests: runs-on: ubuntu-latest if: ${{inputs.changed-python-modules == 'true'}} diff --git a/packages/gooddata-eval/src/gooddata_eval/core/chat/sse_client.py b/packages/gooddata-eval/src/gooddata_eval/core/chat/sse_client.py index cb18d3804..7d52dad3f 100644 --- a/packages/gooddata-eval/src/gooddata_eval/core/chat/sse_client.py +++ b/packages/gooddata-eval/src/gooddata_eval/core/chat/sse_client.py @@ -89,13 +89,11 @@ def _is_retryable_exc(exc: Exception) -> bool: return True if isinstance(exc, httpx.HTTPStatusError): return exc.response.status_code in _RETRYABLE_STATUS_CODES - if isinstance(exc, httpx.RemoteProtocolError): - # Mid-stream disconnect ("peer closed connection without sending complete - # message body") -- pure network flake, not a real agent/content failure. - # Confirmed live: contaminated ~1-4% of visualization runs with a hard - # fail and zero retry attempts. - return True - return False + # Mid-stream disconnect ("peer closed connection without sending complete + # message body") -- pure network flake, not a real agent/content failure. + # Confirmed live: contaminated ~1-4% of visualization runs with a hard + # fail and zero retry attempts. + return isinstance(exc, httpx.RemoteProtocolError) def _retry_transient(operation: Callable[[], T], *, is_retryable: Callable[[Exception], bool]) -> T: diff --git a/packages/gooddata-eval/tests/test_agentic_runner.py b/packages/gooddata-eval/tests/test_agentic_runner.py index b71ce135c..3d9959eba 100644 --- a/packages/gooddata-eval/tests/test_agentic_runner.py +++ b/packages/gooddata-eval/tests/test_agentic_runner.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from gooddata_eval.cli.agentic_runner import _dispatch_agentic, run_agentic_items +from gooddata_eval.cli.agentic_runner import AGENTIC_TEST_KINDS, _dispatch_agentic, run_agentic_items from gooddata_eval.core.agentic.alert_skill import AlertSkillAssertionError from gooddata_eval.core.models import AgenticEvalOutcome, DatasetItem @@ -81,8 +81,6 @@ def test_all_agentic_kind_cases_covers_every_registered_kind(): """Guards the two parametrized tests below against silently going stale: a kind added to AGENTIC_TEST_KINDS without a matching case here would otherwise just not get tested, not fail loudly.""" - from gooddata_eval.cli.agentic_runner import AGENTIC_TEST_KINDS - covered = {kind for kind, _, _ in _ALL_AGENTIC_KIND_CASES} assert covered == set(AGENTIC_TEST_KINDS) @@ -194,8 +192,6 @@ def test_dispatch_agentic_returns_a_real_outcome_for_every_kind(kind, expected_o evaluator produced -- not None, not the outcome's reasoning_steps list alone, not any other bare value the old `isinstance(outcome, tuple)`/`isinstance(outcome, AgenticEvalOutcome)` fallback could silently swallow.""" - from gooddata_eval.core.models import AgenticEvalOutcome - item = DatasetItem( id="q1", dataset_name="ds",