From 808c428810edfa4da338912cec06cd8ae874e4df Mon Sep 17 00:00:00 2001 From: Abhinav Gyawali <22275402+abhizer@users.noreply.github.com> Date: Fri, 12 Jun 2026 12:11:29 +0545 Subject: [PATCH 1/2] ci: post-release: release felderize Release felderize with every feldera release. Signed-off-by: Abhinav Gyawali <22275402+abhizer@users.noreply.github.com> --- .github/workflows/ci-post-release.yml | 53 +++++++++++++++++++++++++++ .github/workflows/publish-python.yml | 35 ++++++++++++++++++ python/felderize/pyproject.toml | 24 +++++++++++- 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-post-release.yml b/.github/workflows/ci-post-release.yml index e00d9d9c577..e81cf93b472 100644 --- a/.github/workflows/ci-post-release.yml +++ b/.github/workflows/ci-post-release.yml @@ -111,6 +111,55 @@ jobs: working-directory: ./python/dbt-feldera run: uv cache prune --ci + # Ideally this would just invoke `publish-python.yml` + # + # But not yet supported: + # https://docs.pypi.org/trusted-publishers/troubleshooting/#reusable-workflows-on-github + # https://github.com/pypa/gh-action-pypi-publish/issues/166 + # https://github.com/pypi/warehouse/issues/11096 + # + # When this is solved, do this again: + # - name: "" + # uses: ./.github/workflows/publish-python.yml + # secrets: inherit + publish-felderize: + runs-on: ubuntu-latest-amd64 + environment: + name: release + url: https://pypi.org/p/felderize + permissions: + contents: read + id-token: write + defaults: + run: + shell: bash + working-directory: ./python + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - name: Install uv + uses: astral-sh/setup-uv@6dfebec6ddbcd197e02256fbdf54deb334fb7f06 # v2 + with: + version: "0.11.3" + enable-cache: true + - name: "Set up Python" + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.10" + - name: Install and build felderize + working-directory: ./python/felderize + run: | + uv venv + uv pip install -e . + uv build + - name: Publish felderize + if: ${{ vars.RELEASE_DRY_RUN == 'false' }} + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e + with: + packages-dir: ./python/felderize/dist + - name: Minimize uv cache + working-directory: ./python/felderize + run: uv cache prune --ci + publish-crates: name: "" uses: ./.github/workflows/publish-crates.yml @@ -162,6 +211,10 @@ jobs: run: | sed -i "s/version = \"${{ env.CURRENT_VERSION }}\"/version = \"${{ env.NEXT_VERSION }}\"/g" pyproject.toml sed -i "s/version: '${{ env.CURRENT_VERSION }}'/version: '${{ env.NEXT_VERSION }}'/g" dbt/include/feldera/dbt_project.yml + - name: Adjust felderize version + working-directory: ./python/felderize + run: | + sed -i "s/version = \"${{ env.CURRENT_VERSION }}\"/version = \"${{ env.NEXT_VERSION }}\"/g" pyproject.toml - name: Adjust sql compiler version working-directory: ./sql-to-dbsp-compiler/SQL-compiler run: | diff --git a/.github/workflows/publish-python.yml b/.github/workflows/publish-python.yml index 60016640865..5d7bec03033 100644 --- a/.github/workflows/publish-python.yml +++ b/.github/workflows/publish-python.yml @@ -91,3 +91,38 @@ jobs: - name: Minimize uv cache working-directory: ./python/dbt-feldera run: uv cache prune --ci + + deploy-felderize: + runs-on: ubuntu-latest-amd64 + environment: + name: release + url: https://pypi.org/p/felderize + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + ref: ${{ inputs.tag || github.ref }} + - name: Install uv + uses: astral-sh/setup-uv@6dfebec6ddbcd197e02256fbdf54deb334fb7f06 # v2 + with: + version: "0.11.3" + enable-cache: true + - name: "Set up Python" + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.10" + - name: Install and build felderize + working-directory: ./python/felderize + run: | + uv venv + uv pip install -e . + uv build + - name: Publish felderize + if: ${{ vars.RELEASE_DRY_RUN == 'false' }} + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e + with: + packages-dir: ./python/felderize/dist + + - name: Minimize uv cache + working-directory: ./python/felderize + run: uv cache prune --ci diff --git a/python/felderize/pyproject.toml b/python/felderize/pyproject.toml index 5d444b795fc..f8c54f2eb24 100644 --- a/python/felderize/pyproject.toml +++ b/python/felderize/pyproject.toml @@ -4,9 +4,25 @@ build-backend = "setuptools.build_meta" [project] name = "felderize" -version = "0.1.0" +readme = "README.md" description = "SQL dialect to Feldera SQL translator agent" +version = "0.308.0" +license = "MIT" requires-python = ">=3.10" +authors = [ + { "name" = "Feldera Team", "email" = "dev@feldera.com" }, +] +keywords = [ + "feldera", + "spark", + "sql", + "translator", + "llm", +] +classifiers = [ + "Programming Language :: Python :: 3.10", + "Operating System :: OS Independent", +] dependencies = [ "anthropic>=0.39.0", "httpx>=0.27", # llm.py catches httpx errors in the stream retry loop @@ -38,3 +54,9 @@ felderize = [ [project.scripts] felderize = "felderize.cli:cli" + +[project.urls] +Homepage = "https://www.feldera.com" +Documentation = "https://docs.feldera.com" +Repository = "https://github.com/feldera/feldera" +Issues = "https://github.com/feldera/feldera/issues" From 9eb90f4056c431f35fa206e57cd68e71440d0eb0 Mon Sep 17 00:00:00 2001 From: Abhinav Gyawali <22275402+abhizer@users.noreply.github.com> Date: Fri, 12 Jun 2026 12:39:02 +0545 Subject: [PATCH 2/2] felderize: auto-download the latest SQL compiler When `--validate` is set, auto-download the latest SQL compiler. Can be disabled by setting `FELDERIZE_AUTO_DOWNLOAD=0`. Signed-off-by: Abhinav Gyawali <22275402+abhizer@users.noreply.github.com> [ci] apply automatic fixes Signed-off-by: feldera-bot --- python/felderize/README.md | 22 ++++- python/felderize/felderize/cli.py | 58 ++++++------- python/felderize/felderize/config.py | 5 ++ .../felderize/install_feldera_sql_compiler.py | 69 ++++++++++++++- python/felderize/pyproject.toml | 2 +- python/felderize/tests/unit/test_cli.py | 59 +++++++++++++ python/felderize/tests/unit/test_compiler.py | 83 +++++++++++++++++++ python/felderize/tests/unit/test_config.py | 21 +++++ 8 files changed, 283 insertions(+), 36 deletions(-) create mode 100644 python/felderize/tests/unit/test_config.py diff --git a/python/felderize/README.md b/python/felderize/README.md index d25d06b799f..b3ae2de184d 100644 --- a/python/felderize/README.md +++ b/python/felderize/README.md @@ -15,13 +15,26 @@ pip install -e . > **Note:** `pip install -e .` is required before running `felderize`. It registers the package and CLI command. -**Download the Feldera SQL compiler JAR** (requires Java 19–21 installed): +**The Feldera SQL compiler JAR** (used only for `--validate`; requires Java 19–21 installed): + +felderize downloads it for you. The first time you run a command with `--validate` +and no compiler configured, felderize fetches the latest +`sql2dbsp-jar-with-dependencies-*.jar` from +[GitHub Releases](https://github.com/feldera/feldera/releases) into `~/.felderize/` +and reuses it on later runs. To opt out (e.g. in CI or offline), set +`FELDERIZE_AUTO_DOWNLOAD=0`; validation is then skipped unless you point +`FELDERA_COMPILER` / `--compiler` at a JAR. + +To fetch or update it explicitly: ```bash felderize download-compiler ``` -This fetches the latest `sql2dbsp-jar-with-dependencies-*.jar` from [GitHub Releases](https://github.com/feldera/feldera/releases) and saves it to `~/.felderize/`. The command prints the exact path — copy it for the next step. Re-run it any time to pick up a newer release; it reports whether you are already on the latest one. +This saves the latest JAR to `~/.felderize/` and prints its path. Re-run it any +time to pick up a newer release; it reports whether you are already on the latest +one. felderize automatically uses the newest JAR cached in `~/.felderize/`, so you +do not need to set `FELDERA_COMPILER` unless you want a specific JAR. > **Requirement:** felderize needs compiler **v0.304.0 or newer** — earlier releases lack SQL features felderize relies on (e.g. `div_null`, `MAKE_DATE`). `download-compiler` always fetches the latest release, and felderize warns at validation time if the configured compiler is older than v0.304.0. @@ -35,7 +48,7 @@ FELDERA_COMPILER=~/.felderize/sql2dbsp-jar-with-dependencies-vX.Y.Z.jar FELDERIZE_MODEL=claude-sonnet-4-6 ``` -All three variables are required. `FELDERA_COMPILER` is used only for validation — translation still works without it, but output SQL is not verified. You can also pass `--compiler PATH` and `--model MODEL` per command. +`ANTHROPIC_API_KEY` and `FELDERIZE_MODEL` are required. `FELDERA_COMPILER` is optional: it is used only for validation, and when unset felderize auto-downloads (and caches) the compiler on first `--validate`. Set it to pin a specific JAR. You can also pass `--compiler PATH` and `--model MODEL` per command. > **Note:** felderize currently requires an Anthropic API key — only Claude models are supported. @@ -202,7 +215,8 @@ Environment variables (set in `.env`): |---|---|---| | `ANTHROPIC_API_KEY` | Anthropic API key | (required) | | `FELDERIZE_MODEL` | LLM model to use (can also be set with `--model`) | (required, set in `.env`) | -| `FELDERA_COMPILER` | Path to sql-to-dbsp compiler (can also be set with `--compiler`) | (required for validation) | +| `FELDERA_COMPILER` | Path to sql-to-dbsp compiler (can also be set with `--compiler`) | (optional; auto-downloaded when unset) | +| `FELDERIZE_AUTO_DOWNLOAD` | Auto-download the compiler on first `--validate` when none is configured. Set to `0`/`false` to disable. | `1` | | `ANTHROPIC_BASE_URL` | Override Anthropic API base URL (for proxies or alternate endpoints) | (optional) | ## Customizing translation diff --git a/python/felderize/felderize/cli.py b/python/felderize/felderize/cli.py index 83a13921f28..088ac08bd2b 100644 --- a/python/felderize/felderize/cli.py +++ b/python/felderize/felderize/cli.py @@ -9,6 +9,7 @@ from felderize.constants import MINIMUM_COMPILER_VERSION from felderize.install_feldera_sql_compiler import ( download_compiler, + ensure_compiler, is_supported_version, jar_version, ) @@ -37,6 +38,31 @@ def _warn_if_unsupported_compiler(compiler_path: str | None) -> None: ) +def _prepare_config(compiler: str | None, model: str | None, validate: bool) -> Config: + """Build the run config from env, apply CLI overrides, and resolve a compiler. + + An explicit ``--compiler`` (or ``FELDERA_COMPILER``) always wins. Otherwise, + when validating, felderize reuses a compiler JAR cached in ``~/.felderize/`` + or downloads the latest release — unless auto-download is disabled via + ``FELDERIZE_AUTO_DOWNLOAD=0``. Download progress goes to stderr so ``--json-output`` + stays clean. + """ + config = Config.from_env() + if compiler: + config.feldera_compiler = compiler + if model: + config.model = model + if validate: + if not config.feldera_compiler: + resolved = ensure_compiler( + auto_download=config.auto_download_compiler, logs=sys.stderr + ) + if resolved is not None: + config.feldera_compiler = str(resolved) + _warn_if_unsupported_compiler(config.feldera_compiler) + return config + + def _split_examples(paths: tuple[str, ...]) -> tuple[list[Path], list[Path]]: """Split --examples paths into (dirs, files).""" dirs, files = [], [] @@ -192,13 +218,7 @@ def translate( "Warning: running without validation — output SQL is not verified against the Feldera compiler.", err=True, ) - config = Config.from_env() - if compiler: - config.feldera_compiler = compiler - if model: - config.model = model - if validate: - _warn_if_unsupported_compiler(config.feldera_compiler) + config = _prepare_config(compiler, model, validate) schema_sql = _read_text(schema_file) query_sql = _read_text(query_file) @@ -277,13 +297,7 @@ def translate_file( "Warning: running without validation — output SQL is not verified against the Feldera compiler.", err=True, ) - config = Config.from_env() - if compiler: - config.feldera_compiler = compiler - if model: - config.model = model - if validate: - _warn_if_unsupported_compiler(config.feldera_compiler) + config = _prepare_config(compiler, model, validate) combined_sql = _read_text(sql_file) schema_sql, query_sql = split_combined_sql(combined_sql) @@ -371,13 +385,7 @@ def translate_batch( "Warning: running without validation — output SQL is not verified against the Feldera compiler.", err=True, ) - config = Config.from_env() - if compiler: - config.feldera_compiler = compiler - if model: - config.model = model - if validate: - _warn_if_unsupported_compiler(config.feldera_compiler) + config = _prepare_config(compiler, model, validate) schema_sql = _read_text(schema_file) schema_errors = validate_schema(schema_sql) @@ -525,13 +533,7 @@ def example( "Warning: running without validation — output SQL is not verified against the Feldera compiler.", err=True, ) - config = Config.from_env() - if compiler: - config.feldera_compiler = compiler - if model: - config.model = model - if validate: - _warn_if_unsupported_compiler(config.feldera_compiler) + config = _prepare_config(compiler, model, validate) result = translate_spark_to_feldera( schema_sql, query_sql, diff --git a/python/felderize/felderize/config.py b/python/felderize/felderize/config.py index 7a9a25475aa..78f3094970c 100644 --- a/python/felderize/felderize/config.py +++ b/python/felderize/felderize/config.py @@ -19,6 +19,7 @@ class Config: feldera_compiler: str = "" max_tokens: int = DEFAULT_MAX_TOKENS docs_base_url: str = DEFAULT_DOCS_BASE_URL + auto_download_compiler: bool = True @property def compiler_path(self) -> str | None: @@ -32,6 +33,9 @@ def from_env(cls) -> Config: load_dotenv(env_path) raw_max_tokens = os.environ.get("FELDERIZE_MAX_TOKENS") + raw_auto_download = ( + os.environ.get("FELDERIZE_AUTO_DOWNLOAD", "1").strip().lower() + ) return cls( model=os.environ.get("FELDERIZE_MODEL", ""), api_key=os.environ.get("ANTHROPIC_API_KEY"), @@ -41,4 +45,5 @@ def from_env(cls) -> Config: docs_base_url=os.environ.get( "FELDERA_DOCS_BASE_URL", DEFAULT_DOCS_BASE_URL ), + auto_download_compiler=raw_auto_download not in ("0", "false", "no", "off"), ) diff --git a/python/felderize/felderize/install_feldera_sql_compiler.py b/python/felderize/felderize/install_feldera_sql_compiler.py index 3a3afcbe25a..aee57d2bd73 100644 --- a/python/felderize/felderize/install_feldera_sql_compiler.py +++ b/python/felderize/felderize/install_feldera_sql_compiler.py @@ -61,6 +61,7 @@ def download_compiler( output_dir: Path | None = None, version: str | None = None, force: bool = False, + logs=None, ) -> Path: """Download sql2dbsp JAR from GitHub releases. Returns the path to the JAR. @@ -68,7 +69,10 @@ def download_compiler( output_dir: Directory to save the JAR (default: ~/.felderize/). version: Release tag (e.g. "v0.291.0"); defaults to latest. force: Overwrite existing file if present. + logs: Where to write progress messages (default: stdout). Pass + sys.stderr when stdout must stay machine-clean (e.g. --json-output). """ + out = logs or sys.stdout dest_dir = output_dir or FELDERIZE_DIR dest_dir.mkdir(parents=True, exist_ok=True) @@ -90,7 +94,7 @@ def download_compiler( if dest.exists() and not force: status = "the latest release" if is_latest else "installed" - print(f"Already on {status}: {name} ({tag})") + print(f"Already on {status}: {name} ({tag})", file=out) return dest last_pct = [-1] @@ -108,11 +112,70 @@ def _progress(block_num: int, block_size: int, total_size: int) -> None: f"\r [{bar:<20}] {pct:3d}% {downloaded / 1_048_576:.1f}/{total_size / 1_048_576:.1f} MB", end="", flush=True, + file=out, ) latest_note = " (latest release)" if is_latest else "" - print(f"Downloading {name} ({tag}){latest_note}...") + print(f"Downloading {name} ({tag}){latest_note}...", file=out) urllib.request.urlretrieve(url, dest, reporthook=_progress) - print() # newline after progress bar + print(file=out) # newline after progress bar return dest + + +def find_local_compiler(search_dir: Path | None = None) -> Path | None: + """Return the newest compiler JAR already cached in search_dir. + + Looks for ``sql2dbsp-jar-with-dependencies-*.jar`` files in search_dir + (default ``~/.felderize/``). Prefers versions felderize supports; among + those, the highest version. Falls back to the highest unsupported version + when no supported one is cached (validation then warns). Returns None when + the directory holds no compiler JAR. + """ + directory = search_dir or FELDERIZE_DIR + if not directory.is_dir(): + return None + + jars = [p for p in directory.glob(f"{COMPILER_JAR_PREFIX}*.jar") if p.is_file()] + if not jars: + return None + + def version_key(path: Path) -> tuple[int, ...]: + tag = jar_version(path.name) + return _parse_version(tag) if tag else () + + supported = [ + p for p in jars if (tag := jar_version(p.name)) and is_supported_version(tag) + ] + return max(supported or jars, key=version_key) + + +def ensure_compiler( + search_dir: Path | None = None, + auto_download: bool = True, + logs=None, +) -> Path | None: + """Return a usable compiler JAR, downloading the latest one if needed. + + Resolution order: + 1. The newest compiler JAR already cached in search_dir (~/.felderize/). + 2. Otherwise, when auto_download is set, download the latest release. + + Returns None when nothing is cached and downloading is disabled or fails; + callers then fall back to their "compiler not found" handling. + """ + local = find_local_compiler(search_dir) + if local is not None: + return local + if not auto_download: + return None + try: + return download_compiler(output_dir=search_dir, logs=logs) + except Exception as e: # network/API failure — degrade gracefully + print( + f"Warning: could not auto-download the Feldera compiler ({e}); " + "validation will be skipped. Run 'felderize download-compiler' or set " + "FELDERA_COMPILER to validate.", + file=sys.stderr, + ) + return None diff --git a/python/felderize/pyproject.toml b/python/felderize/pyproject.toml index f8c54f2eb24..c2381049e67 100644 --- a/python/felderize/pyproject.toml +++ b/python/felderize/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "felderize" readme = "README.md" description = "SQL dialect to Feldera SQL translator agent" -version = "0.308.0" +version = "0.310.0" license = "MIT" requires-python = ">=3.10" authors = [ diff --git a/python/felderize/tests/unit/test_cli.py b/python/felderize/tests/unit/test_cli.py index 48346f460f9..4f6fb4f2ea0 100644 --- a/python/felderize/tests/unit/test_cli.py +++ b/python/felderize/tests/unit/test_cli.py @@ -46,6 +46,16 @@ def _query(tmp_path: Path, content: str = "CREATE VIEW v AS SELECT x FROM t;") - return p +@pytest.fixture(autouse=True) +def _no_auto_download(monkeypatch): + """Keep CLI tests hermetic: never reach out to GitHub to fetch a compiler. + + With no --compiler/FELDERA_COMPILER configured, --validate would otherwise + auto-download the compiler; stub it to "not found" so tests stay offline. + """ + monkeypatch.setattr("felderize.cli.ensure_compiler", lambda *a, **k: None) + + # --------------------------------------------------------------------------- # _split_examples # --------------------------------------------------------------------------- @@ -196,6 +206,55 @@ def test_success_text_output(self, tmp_path): assert "-- Schema --" in result.output assert "-- Query --" in result.output + def test_validate_auto_resolves_compiler(self, tmp_path, monkeypatch): + """With --validate and no --compiler, the auto-resolved JAR is passed + through to the translator via the config.""" + fake_jar = tmp_path / "sql2dbsp-jar-with-dependencies-v0.310.0.jar" + monkeypatch.setattr("felderize.cli.ensure_compiler", lambda *a, **k: fake_jar) + runner = CliRunner() + with patch( + "felderize.cli.translate_spark_to_feldera", return_value=_SUCCESS + ) as mock_fn: + self._invoke(runner, _schema(tmp_path), _query(tmp_path), "--validate") + config = mock_fn.call_args.args[2] + assert config.feldera_compiler == str(fake_jar) + + def test_explicit_compiler_skips_auto_resolution(self, tmp_path, monkeypatch): + """An explicit --compiler must win; auto-download is never consulted.""" + called = [] + monkeypatch.setattr( + "felderize.cli.ensure_compiler", + lambda *a, **k: called.append(True), + ) + runner = CliRunner() + with patch("felderize.cli.translate_spark_to_feldera", return_value=_SUCCESS): + self._invoke( + runner, + _schema(tmp_path), + _query(tmp_path), + "--validate", + "--compiler", + "/opt/my-compiler.jar", + ) + assert called == [] + + def test_env_compiler_skips_auto_resolution(self, tmp_path, monkeypatch): + """A configured FELDERA_COMPILER must be used as-is; no auto-download.""" + called = [] + monkeypatch.setattr( + "felderize.cli.ensure_compiler", + lambda *a, **k: called.append(True), + ) + monkeypatch.setenv("FELDERA_COMPILER", "/opt/env-compiler.jar") + runner = CliRunner() + with patch( + "felderize.cli.translate_spark_to_feldera", return_value=_SUCCESS + ) as mock_fn: + self._invoke(runner, _schema(tmp_path), _query(tmp_path), "--validate") + assert called == [] + config = mock_fn.call_args.args[2] + assert config.feldera_compiler == "/opt/env-compiler.jar" + def test_success_json_output(self, tmp_path): runner = CliRunner() with patch("felderize.cli.translate_spark_to_feldera", return_value=_SUCCESS): diff --git a/python/felderize/tests/unit/test_compiler.py b/python/felderize/tests/unit/test_compiler.py index fe3f2b3920f..bcd53ec5f36 100644 --- a/python/felderize/tests/unit/test_compiler.py +++ b/python/felderize/tests/unit/test_compiler.py @@ -9,11 +9,20 @@ _find_jar_asset, _parse_version, download_compiler, + ensure_compiler, + find_local_compiler, is_supported_version, jar_version, ) +def _make_jar(directory: Path, version: str) -> Path: + """Create a fake compiler JAR named for the given version tag.""" + jar = directory / f"sql2dbsp-jar-with-dependencies-{version}.jar" + jar.write_bytes(b"fake jar content") + return jar + + _FAKE_RELEASE = { "tag_name": "v0.291.0", "assets": [ @@ -119,6 +128,80 @@ def test_version_forwarded_to_fetch(self, tmp_path): mock_fetch.assert_called_once_with("v0.291.0") +class TestFindLocalCompiler: + def test_none_when_dir_missing(self, tmp_path): + assert find_local_compiler(tmp_path / "does-not-exist") is None + + def test_none_when_no_jars(self, tmp_path): + (tmp_path / "notes.txt").write_text("not a jar") + assert find_local_compiler(tmp_path) is None + + def test_picks_highest_supported_version(self, tmp_path): + _make_jar(tmp_path, "v0.304.0") + newest = _make_jar(tmp_path, "v0.310.0") + _make_jar(tmp_path, "v0.305.0") + assert find_local_compiler(tmp_path) == newest + + def test_prefers_supported_over_higher_unsupported(self, tmp_path): + # An unsupported version must not win even when numerically higher would + # never happen; here the supported one is also the only acceptable choice. + supported = _make_jar(tmp_path, "v0.304.0") + _make_jar(tmp_path, "v0.300.0") # below MINIMUM_COMPILER_VERSION + assert find_local_compiler(tmp_path) == supported + + def test_falls_back_to_unsupported_when_no_supported(self, tmp_path): + _make_jar(tmp_path, "v0.300.0") + newest_unsupported = _make_jar(tmp_path, "v0.303.0") + assert find_local_compiler(tmp_path) == newest_unsupported + + def test_ignores_unrelated_jars(self, tmp_path): + (tmp_path / "some-other.jar").write_bytes(b"x") + wanted = _make_jar(tmp_path, "v0.304.0") + assert find_local_compiler(tmp_path) == wanted + + +class TestEnsureCompiler: + def test_returns_cached_without_downloading(self, tmp_path): + cached = _make_jar(tmp_path, "v0.304.0") + with patch( + "felderize.install_feldera_sql_compiler.download_compiler" + ) as mock_dl: + result = ensure_compiler(search_dir=tmp_path) + assert result == cached + mock_dl.assert_not_called() + + def test_downloads_when_none_cached(self, tmp_path): + def fake_retrieve(url, dest, reporthook=None): + Path(dest).write_bytes(b"fake jar content") + + with ( + patch( + "felderize.install_feldera_sql_compiler._fetch_release", + return_value=_FAKE_RELEASE, + ), + patch( + "felderize.install_feldera_sql_compiler.urllib.request.urlretrieve", + side_effect=fake_retrieve, + ), + ): + result = ensure_compiler(search_dir=tmp_path) + assert result is not None + assert result.exists() + assert result.name == "sql2dbsp-jar-with-dependencies-v0.291.0.jar" + + def test_none_when_download_disabled_and_no_cache(self, tmp_path): + assert ensure_compiler(search_dir=tmp_path, auto_download=False) is None + + def test_returns_none_and_warns_on_download_failure(self, tmp_path, capsys): + with patch( + "felderize.install_feldera_sql_compiler.download_compiler", + side_effect=RuntimeError("network down"), + ): + result = ensure_compiler(search_dir=tmp_path) + assert result is None + assert "could not auto-download" in capsys.readouterr().err + + class TestVersionHelpers: def test_parse_version(self): assert _parse_version("v0.304.0") == (0, 304, 0) diff --git a/python/felderize/tests/unit/test_config.py b/python/felderize/tests/unit/test_config.py new file mode 100644 index 00000000000..a92f38bdb5c --- /dev/null +++ b/python/felderize/tests/unit/test_config.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import pytest + +from felderize.config import Config + + +class TestAutoDownloadCompilerFlag: + def test_defaults_to_enabled(self, monkeypatch): + monkeypatch.delenv("FELDERIZE_AUTO_DOWNLOAD", raising=False) + assert Config.from_env().auto_download_compiler is True + + @pytest.mark.parametrize("value", ["0", "false", "FALSE", "no", "off", "Off"]) + def test_disabled_values(self, monkeypatch, value): + monkeypatch.setenv("FELDERIZE_AUTO_DOWNLOAD", value) + assert Config.from_env().auto_download_compiler is False + + @pytest.mark.parametrize("value", ["1", "true", "yes", "on", "anything-else"]) + def test_enabled_values(self, monkeypatch, value): + monkeypatch.setenv("FELDERIZE_AUTO_DOWNLOAD", value) + assert Config.from_env().auto_download_compiler is True