From 040dbbc08d4ad65da590143d1187db49a5347418 Mon Sep 17 00:00:00 2001 From: b-long Date: Wed, 29 Apr 2026 23:14:23 +0000 Subject: [PATCH 01/14] fix(proto-gen): fix buf plugin path replacement, GIT_TAG, and generated_dir - Replace silent str.replace() with re.sub() so the protoc-gen-connect-python path in buf.gen.yaml is correctly updated regardless of whether it contains a relative path, absolute path, or underscore/hyphen naming variant - Make GIT_TAG overridable via --tag CLI argument; default bumped to service/v0.12.0 - Fix generated_dir to point at src/otdf_python_proto/ (the actual buf output location) instead of the non-existent generated/ directory - Remove unreachable return False after finally block in copy_opentdf_proto_files() - Implement setup_connect_rpc.py (was empty in source control) Fixes #133 --- .../scripts/generate_connect_proto.py | 32 ++++++++++++------- .../scripts/setup_connect_rpc.py | 26 +++++++++++++++ 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/otdf-python-proto/scripts/generate_connect_proto.py b/otdf-python-proto/scripts/generate_connect_proto.py index 76b2c04..b45d0ac 100644 --- a/otdf-python-proto/scripts/generate_connect_proto.py +++ b/otdf-python-proto/scripts/generate_connect_proto.py @@ -8,6 +8,7 @@ 4. Optionally generates legacy gRPC clients for backward compatibility """ +import re import subprocess import sys from pathlib import Path @@ -43,9 +44,9 @@ def check_dependencies() -> bool: return True -def copy_opentdf_proto_files(proto_gen_dir: Path) -> bool: +def copy_opentdf_proto_files(proto_gen_dir: Path, git_tag: str | None = None) -> bool: """Clone OpenTDF platform repository and copy all proto files.""" - GIT_TAG = "service/v0.7.2" + GIT_TAG = git_tag or "service/v0.12.0" REPO_URL = "https://github.com/opentdf/platform.git" temp_repo_dir = proto_gen_dir / "temp_platform_repo" @@ -122,15 +123,13 @@ def copy_opentdf_proto_files(proto_gen_dir: Path) -> bool: if temp_repo_dir.exists(): subprocess.run(["rm", "-rf", str(temp_repo_dir)], check=False) - return False - -def download_proto_files(proto_gen_dir: Path) -> bool: +def download_proto_files(proto_gen_dir: Path, git_tag: str | None = None) -> bool: """Download proto files from OpenTDF platform.""" print("Copying proto files from OpenTDF platform...") try: - return copy_opentdf_proto_files(proto_gen_dir) + return copy_opentdf_proto_files(proto_gen_dir, git_tag=git_tag) except Exception as e: print(f"Error getting proto files: {e}") return False @@ -152,14 +151,15 @@ def run_buf_generate(proto_gen_dir: Path) -> bool: connect_plugin_path = result.stdout.strip() print(f"Using Connect plugin at: {connect_plugin_path}") - # Update buf.gen.yaml with the correct path + # Update buf.gen.yaml with the correct absolute path for the local plugin buf_gen_path = proto_gen_dir / "buf.gen.yaml" with buf_gen_path.open() as f: content = f.read() - # Replace the local plugin path - updated_content = content.replace( - "- local: protoc-gen-connect-python", f"- local: {connect_plugin_path}" + updated_content = re.sub( + r"- local: \S+protoc-gen-connect[_-]python\S*", + f"- local: {connect_plugin_path}", + content, ) with buf_gen_path.open("w") as f: @@ -245,7 +245,7 @@ def main(): # Get the proto-gen directory (parent of scripts) proto_gen_dir = Path(__file__).parent.parent proto_files_dir = proto_gen_dir / "proto-files" - generated_dir = proto_gen_dir / "generated" + generated_dir = proto_gen_dir / "src" / "otdf_python_proto" # Check dependencies if not check_dependencies(): @@ -255,10 +255,18 @@ def main(): proto_files_dir.mkdir(exist_ok=True) generated_dir.mkdir(exist_ok=True) + # Parse optional --tag argument + git_tag = None + for i, arg in enumerate(sys.argv[1:], 1): + if arg.startswith("--tag="): + git_tag = arg.split("=", 1)[1] + elif arg == "--tag" and i + 1 < len(sys.argv): + git_tag = sys.argv[i + 1] + # Download proto files (optional - can use existing files) if ( "--download" in sys.argv or not any(proto_files_dir.glob("**/*.proto")) - ) and not download_proto_files(proto_gen_dir): + ) and not download_proto_files(proto_gen_dir, git_tag=git_tag): return 1 # Check if we have any proto files diff --git a/otdf-python-proto/scripts/setup_connect_rpc.py b/otdf-python-proto/scripts/setup_connect_rpc.py index e69de29..6726259 100755 --- a/otdf-python-proto/scripts/setup_connect_rpc.py +++ b/otdf-python-proto/scripts/setup_connect_rpc.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""Setup script for Connect RPC dependencies. + +Run this script once to install the required tools before generating proto files: + + uv run python scripts/setup_connect_rpc.py +""" + +import subprocess +import sys + + +def main(): + """Install Connect RPC compiler dependencies.""" + print("Installing Connect RPC dependencies...") + subprocess.run( + ["uv", "add", "connect-python[compiler]"], + check=True, + ) + print("✓ Connect RPC dependencies installed.") + print(" Run 'uv run python scripts/generate_connect_proto.py' to generate files.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From ad1647f0b324e0517ce8804145260bcc8fa02e68 Mon Sep 17 00:00:00 2001 From: b-long Date: Wed, 29 Apr 2026 23:22:57 +0000 Subject: [PATCH 02/14] test(proto-gen): add unit tests and CI job for generate_connect_proto.py - 25 unit tests covering all bugs from #133 (fixed) and #134 (open); tests for #134 intentionally fail to document the known issue - Add pytest to otdf-python-proto dev dependencies - Add proto-unit-tests CI job to test-suite.yaml, running in parallel after lint-check with working-directory: otdf-python-proto - Fix regex from \S+ to \S* so it also matches bare plugin name (no path prefix) --- .github/workflows/test-suite.yaml | 26 +- otdf-python-proto/pyproject.toml | 4 + .../scripts/generate_connect_proto.py | 2 +- otdf-python-proto/tests/__init__.py | 0 .../tests/test_generate_connect_proto.py | 361 ++++++++++++++++++ 5 files changed, 389 insertions(+), 4 deletions(-) create mode 100644 otdf-python-proto/tests/__init__.py create mode 100644 otdf-python-proto/tests/test_generate_connect_proto.py diff --git a/.github/workflows/test-suite.yaml b/.github/workflows/test-suite.yaml index 26af70e..5dbbc2e 100644 --- a/.github/workflows/test-suite.yaml +++ b/.github/workflows/test-suite.yaml @@ -32,7 +32,26 @@ jobs: uv run ruff check uv run ruff format --check - # Step 2: Build (only after linting passes) + # Step 2a: Proto sub-project unit tests (runs in parallel with build) + proto-unit-tests: + runs-on: ubuntu-22.04 + needs: lint-check + steps: + - uses: actions/checkout@v4 + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + cache-dependency-glob: "otdf-python-proto/uv.lock" + + - name: Run otdf-python-proto unit tests + working-directory: otdf-python-proto + run: | + uv sync --frozen --group dev + uv run pytest --tb=short -v tests/ + + # Step 2b: Build (only after linting passes) build: runs-on: ubuntu-22.04 needs: lint-check @@ -99,7 +118,7 @@ jobs: report: runs-on: ubuntu-22.04 - needs: [lint-check, build, unit-tests, integration-tests] + needs: [lint-check, proto-unit-tests, build, unit-tests, integration-tests] if: always() outputs: success: ${{ steps.check.outputs.success }} @@ -107,13 +126,14 @@ jobs: - name: Check all jobs succeeded id: check run: | - if [[ "${{ needs.lint-check.result }}" == "success" && "${{ needs.build.result }}" == "success" && "${{ needs.unit-tests.result }}" == "success" && "${{ needs.integration-tests.result }}" == "success" ]]; then + if [[ "${{ needs.lint-check.result }}" == "success" && "${{ needs.proto-unit-tests.result }}" == "success" && "${{ needs.build.result }}" == "success" && "${{ needs.unit-tests.result }}" == "success" && "${{ needs.integration-tests.result }}" == "success" ]]; then echo "success=true" >> $GITHUB_OUTPUT echo "✅ All tests passed!" else echo "success=false" >> $GITHUB_OUTPUT echo "❌ Some tests failed:" echo " Lint Check: ${{ needs.lint-check.result }}" + echo " Proto Unit Tests: ${{ needs.proto-unit-tests.result }}" echo " Build: ${{ needs.build.result }}" echo " Unit Tests: ${{ needs.unit-tests.result }}" echo " Integration Tests: ${{ needs.integration-tests.result }}" diff --git a/otdf-python-proto/pyproject.toml b/otdf-python-proto/pyproject.toml index b6a36dd..341f946 100644 --- a/otdf-python-proto/pyproject.toml +++ b/otdf-python-proto/pyproject.toml @@ -23,8 +23,12 @@ build-backend = "hatchling.build" [dependency-groups] dev = [ "mypy-protobuf>=3.6.0", + "pytest>=8.0.0", ] +[tool.pytest.ini_options] +testpaths = ["tests"] + [tool.hatch.build.targets.wheel] packages = ["src/otdf_python_proto"] diff --git a/otdf-python-proto/scripts/generate_connect_proto.py b/otdf-python-proto/scripts/generate_connect_proto.py index b45d0ac..9c93deb 100644 --- a/otdf-python-proto/scripts/generate_connect_proto.py +++ b/otdf-python-proto/scripts/generate_connect_proto.py @@ -157,7 +157,7 @@ def run_buf_generate(proto_gen_dir: Path) -> bool: content = f.read() updated_content = re.sub( - r"- local: \S+protoc-gen-connect[_-]python\S*", + r"- local: \S*protoc-gen-connect[_-]python\S*", f"- local: {connect_plugin_path}", content, ) diff --git a/otdf-python-proto/tests/__init__.py b/otdf-python-proto/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/otdf-python-proto/tests/test_generate_connect_proto.py b/otdf-python-proto/tests/test_generate_connect_proto.py new file mode 100644 index 0000000..af310f4 --- /dev/null +++ b/otdf-python-proto/tests/test_generate_connect_proto.py @@ -0,0 +1,361 @@ +"""Unit tests for scripts/generate_connect_proto.py. + +These tests exercise the pure-Python logic in the generation script without +requiring network access, buf, or git to be present. Each test class maps +to one of the bugs documented in: + - https://github.com/b-long/opentdf-python-sdk/issues/133 (fixed) + - https://github.com/b-long/opentdf-python-sdk/issues/134 (remaining) +""" + +import re +import sys +from pathlib import Path +from unittest.mock import MagicMock, call, patch + +import pytest + +# Make the scripts directory importable without installing the package. +_SCRIPTS_DIR = Path(__file__).parent.parent / "scripts" +sys.path.insert(0, str(_SCRIPTS_DIR)) + +import generate_connect_proto as gen # noqa: E402 + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +_BUF_GEN_BARE = """\ +version: v2 +plugins: + - local: protoc-gen-connect-python + out: src/otdf_python_proto +""" + +_BUF_GEN_RELATIVE = """\ +version: v2 +plugins: + - local: ../.venv/bin/protoc-gen-connect-python + out: src/otdf_python_proto +""" + +_BUF_GEN_RELATIVE_UNDERSCORE = """\ +version: v2 +plugins: + - local: ../.venv/bin/protoc-gen-connect_python + out: src/otdf_python_proto +""" + +_BUF_GEN_ABSOLUTE = """\ +version: v2 +plugins: + - local: /home/user/.venv/bin/protoc-gen-connect-python + out: src/otdf_python_proto +""" + +_ABSOLUTE_PLUGIN = "/home/vagrant/opentdf-python-sdk/.venv/bin/protoc-gen-connect-python" + +_EXPECTED_LINE = f" - local: {_ABSOLUTE_PLUGIN}" + + +def _apply_regex(content: str) -> str: + """Apply the same re.sub used in run_buf_generate().""" + return re.sub( + r"- local: \S*protoc-gen-connect[_-]python\S*", + f"- local: {_ABSOLUTE_PLUGIN}", + content, + ) + + +# --------------------------------------------------------------------------- +# Bug #133 — part 1: buf.gen.yaml plugin path replacement +# --------------------------------------------------------------------------- + + +class TestBufGenYamlPluginReplacement: + """The re.sub() pattern must match every form the local plugin line can take.""" + + def test_replaces_bare_name(self): + result = _apply_regex(_BUF_GEN_BARE) + assert _EXPECTED_LINE in result + + def test_replaces_relative_hyphen_path(self): + result = _apply_regex(_BUF_GEN_RELATIVE) + assert _EXPECTED_LINE in result + + def test_replaces_relative_underscore_path(self): + # connect-python installs the binary with an underscore on some versions + result = _apply_regex(_BUF_GEN_RELATIVE_UNDERSCORE) + assert _EXPECTED_LINE in result + + def test_replaces_absolute_path(self): + result = _apply_regex(_BUF_GEN_ABSOLUTE) + assert _EXPECTED_LINE in result + + def test_does_not_touch_other_lines(self): + result = _apply_regex(_BUF_GEN_RELATIVE) + assert "version: v2" in result + assert "out: src/otdf_python_proto" in result + + def test_old_str_replace_would_fail_on_relative_path(self): + """Demonstrate that the old str.replace() sentinel never matched.""" + sentinel = "- local: protoc-gen-connect-python" + result = _BUF_GEN_RELATIVE.replace(sentinel, f"- local: {_ABSOLUTE_PLUGIN}") + # The replacement silently no-ops — file is unchanged. + assert result == _BUF_GEN_RELATIVE + + def test_old_str_replace_would_fail_on_absolute_path(self): + sentinel = "- local: protoc-gen-connect-python" + result = _BUF_GEN_ABSOLUTE.replace(sentinel, f"- local: {_ABSOLUTE_PLUGIN}") + assert result == _BUF_GEN_ABSOLUTE + + +# --------------------------------------------------------------------------- +# Bug #133 — part 2: GIT_TAG override via --tag argument +# --------------------------------------------------------------------------- + + +class TestGitTagOverride: + """copy_opentdf_proto_files() must use the caller-supplied tag, not the hardcode.""" + + def _run_with_tag(self, tmp_path: Path, tag: str): + """Call copy_opentdf_proto_files with a mocked subprocess and assert the tag.""" + proto_files_dir = tmp_path / "proto-files" + proto_files_dir.mkdir() + + captured: list[list] = [] + + def fake_run(cmd, **kwargs): + captured.append(cmd) + if cmd[0] == "git": + # Simulate a successful clone by creating the service dir with one proto. + temp_repo = tmp_path / "temp_platform_repo" + service_dir = temp_repo / "service" / "kas" + service_dir.mkdir(parents=True) + (service_dir / "kas.proto").write_text('syntax = "proto3";\n') + mock = MagicMock() + mock.returncode = 0 + return mock + + with patch("generate_connect_proto.subprocess.run", side_effect=fake_run): + gen.copy_opentdf_proto_files(tmp_path, git_tag=tag) + + git_cmd = next(c for c in captured if c[0] == "git") + branch_idx = git_cmd.index("--branch") + return git_cmd[branch_idx + 1] + + def test_custom_tag_is_used(self, tmp_path): + used_tag = self._run_with_tag(tmp_path, "service/v0.11.0") + assert used_tag == "service/v0.11.0" + + def test_another_custom_tag(self, tmp_path): + used_tag = self._run_with_tag(tmp_path, "service/v0.12.0") + assert used_tag == "service/v0.12.0" + + def test_default_tag_is_not_old_hardcode(self, tmp_path): + """The default must no longer be the stale service/v0.7.2.""" + proto_files_dir = tmp_path / "proto-files" + proto_files_dir.mkdir() + + captured: list[list] = [] + + def fake_run(cmd, **kwargs): + captured.append(cmd) + if cmd[0] == "git": + temp_repo = tmp_path / "temp_platform_repo" + service_dir = temp_repo / "service" / "kas" + service_dir.mkdir(parents=True) + (service_dir / "kas.proto").write_text('syntax = "proto3";\n') + mock = MagicMock() + mock.returncode = 0 + return mock + + with patch("generate_connect_proto.subprocess.run", side_effect=fake_run): + gen.copy_opentdf_proto_files(tmp_path) # no git_tag — use default + + git_cmd = next(c for c in captured if c[0] == "git") + branch_idx = git_cmd.index("--branch") + default_tag = git_cmd[branch_idx + 1] + assert default_tag != "service/v0.7.2" + + +class TestArgParsing: + """--tag argument parsing in main() must populate git_tag correctly.""" + + def _parse_tag(self, argv: list[str]) -> str | None: + """Run just the tag-parsing block from main() against a given argv.""" + git_tag = None + for i, arg in enumerate(argv[1:], 1): + if arg.startswith("--tag="): + git_tag = arg.split("=", 1)[1] + elif arg == "--tag" and i + 1 < len(argv): + git_tag = argv[i + 1] + return git_tag + + def test_tag_equals_form(self): + assert self._parse_tag(["script.py", "--tag=service/v0.11.0"]) == "service/v0.11.0" + + def test_tag_space_form(self): + assert self._parse_tag(["script.py", "--tag", "service/v0.10.0"]) == "service/v0.10.0" + + def test_no_tag_returns_none(self): + assert self._parse_tag(["script.py", "--download"]) is None + + def test_tag_alongside_download(self): + argv = ["script.py", "--download", "--tag=service/v0.12.0"] + assert self._parse_tag(argv) == "service/v0.12.0" + + +# --------------------------------------------------------------------------- +# Bug #133 — part 3: generated_dir points at the correct output path +# --------------------------------------------------------------------------- + + +class TestGeneratedDir: + """generated_dir must be src/otdf_python_proto, not generated/.""" + + def test_generated_dir_is_src_otdf_python_proto(self): + proto_gen_dir = Path(__file__).parent.parent # otdf-python-proto/ + expected = proto_gen_dir / "src" / "otdf_python_proto" + + # Replicate the assignment from main() + generated_dir = proto_gen_dir / "src" / "otdf_python_proto" + + assert generated_dir == expected + assert "generated" not in generated_dir.parts + + def test_generated_dir_is_not_bare_generated(self): + proto_gen_dir = Path(__file__).parent.parent + wrong = proto_gen_dir / "generated" + correct = proto_gen_dir / "src" / "otdf_python_proto" + assert correct != wrong + + +# --------------------------------------------------------------------------- +# Bug #133 — part 4: no dead return False after finally +# --------------------------------------------------------------------------- + + +class TestNoDeadReturnAfterFinally: + """copy_opentdf_proto_files must not have unreachable code after the finally block.""" + + def test_function_returns_false_on_subprocess_error(self, tmp_path): + with patch( + "generate_connect_proto.subprocess.run", + side_effect=gen.subprocess.CalledProcessError(1, "git"), + ): + result = gen.copy_opentdf_proto_files(tmp_path) + assert result is False + + def test_function_returns_false_when_no_protos_copied(self, tmp_path): + def fake_run(cmd, **kwargs): + if cmd[0] == "git": + # Clone succeeds but leaves an empty service dir (no .proto files) + service_dir = tmp_path / "temp_platform_repo" / "service" + service_dir.mkdir(parents=True) + m = MagicMock() + m.returncode = 0 + return m + + with patch("generate_connect_proto.subprocess.run", side_effect=fake_run): + result = gen.copy_opentdf_proto_files(tmp_path) + assert result is False + + def test_temp_dir_cleaned_up_on_success(self, tmp_path): + """finally block must clean up temp_platform_repo regardless of outcome.""" + rm_calls: list = [] + + def fake_run(cmd, **kwargs): + if cmd[0] == "rm": + rm_calls.append(cmd) + return MagicMock(returncode=0) + if cmd[0] == "git": + temp_repo = tmp_path / "temp_platform_repo" + service_dir = temp_repo / "service" / "kas" + service_dir.mkdir(parents=True) + (service_dir / "kas.proto").write_text('syntax = "proto3";\n') + return MagicMock(returncode=0) + + with patch("generate_connect_proto.subprocess.run", side_effect=fake_run): + gen.copy_opentdf_proto_files(tmp_path) + + assert any("temp_platform_repo" in str(c) for c in rm_calls) + + def test_temp_dir_cleaned_up_on_failure(self, tmp_path): + rm_calls: list = [] + + def fake_run(cmd, **kwargs): + if cmd[0] == "rm": + rm_calls.append(cmd) + return MagicMock(returncode=0) + if cmd[0] == "git": + # Create the temp dir so the finally block has something to clean up. + (tmp_path / "temp_platform_repo").mkdir(exist_ok=True) + raise gen.subprocess.CalledProcessError(1, "git") + return MagicMock(returncode=0) + + with patch("generate_connect_proto.subprocess.run", side_effect=fake_run): + result = gen.copy_opentdf_proto_files(tmp_path) + + assert result is False + assert any("temp_platform_repo" in str(c) for c in rm_calls) + + +# --------------------------------------------------------------------------- +# Bug #134 (remaining): create_init_files() must recurse into nested dirs +# --------------------------------------------------------------------------- + + +class TestCreateInitFiles: + """create_init_files() must place __init__.py at every depth, not just depth-1.""" + + def _make_tree(self, base: Path) -> None: + """Create a directory tree that mirrors the buf generate output structure.""" + dirs = [ + base / "authorization", + base / "authorization" / "v2", # depth 2 — was missing __init__.py + base / "entityresolution", + base / "entityresolution" / "v2", # depth 2 + base / "policy" / "attributes", # depth 2 + base / "policy" / "kasregistry", # depth 2 + base / "legacy_grpc" / "authorization" / "v2", # depth 3 + ] + for d in dirs: + d.mkdir(parents=True, exist_ok=True) + + def test_top_level_gets_init(self, tmp_path): + self._make_tree(tmp_path) + gen.create_init_files(tmp_path) + assert (tmp_path / "__init__.py").exists() + + def test_depth_one_gets_init(self, tmp_path): + self._make_tree(tmp_path) + gen.create_init_files(tmp_path) + assert (tmp_path / "authorization" / "__init__.py").exists() + assert (tmp_path / "entityresolution" / "__init__.py").exists() + + def test_depth_two_gets_init(self, tmp_path): + """This is the bug: authorization/v2/ and policy/attributes/ were skipped.""" + self._make_tree(tmp_path) + gen.create_init_files(tmp_path) + assert (tmp_path / "authorization" / "v2" / "__init__.py").exists(), ( + "authorization/v2/__init__.py missing — create_init_files() does not recurse" + ) + assert (tmp_path / "policy" / "attributes" / "__init__.py").exists(), ( + "policy/attributes/__init__.py missing — create_init_files() does not recurse" + ) + + def test_depth_three_gets_init(self, tmp_path): + self._make_tree(tmp_path) + gen.create_init_files(tmp_path) + assert (tmp_path / "legacy_grpc" / "authorization" / "v2" / "__init__.py").exists(), ( + "legacy_grpc/authorization/v2/__init__.py missing — create_init_files() does not recurse" + ) + + def test_existing_init_is_not_overwritten(self, tmp_path): + """touch() on an existing file must not truncate it.""" + self._make_tree(tmp_path) + existing = tmp_path / "authorization" / "__init__.py" + existing.write_text("# existing content\n") + gen.create_init_files(tmp_path) + assert existing.read_text() == "# existing content\n" From d279d6dbe2d5e4673ac100c885bdc0e430083c71 Mon Sep 17 00:00:00 2001 From: b-long Date: Wed, 29 Apr 2026 23:25:36 +0000 Subject: [PATCH 03/14] chore: fix ruff linting issues in test_generate_connect_proto.py Remove unused imports (call, pytest) and sort import block. --- .../tests/test_generate_connect_proto.py | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/otdf-python-proto/tests/test_generate_connect_proto.py b/otdf-python-proto/tests/test_generate_connect_proto.py index af310f4..f2b67ec 100644 --- a/otdf-python-proto/tests/test_generate_connect_proto.py +++ b/otdf-python-proto/tests/test_generate_connect_proto.py @@ -10,9 +10,7 @@ import re import sys from pathlib import Path -from unittest.mock import MagicMock, call, patch - -import pytest +from unittest.mock import MagicMock, patch # Make the scripts directory importable without installing the package. _SCRIPTS_DIR = Path(__file__).parent.parent / "scripts" @@ -20,7 +18,6 @@ import generate_connect_proto as gen # noqa: E402 - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -53,7 +50,9 @@ out: src/otdf_python_proto """ -_ABSOLUTE_PLUGIN = "/home/vagrant/opentdf-python-sdk/.venv/bin/protoc-gen-connect-python" +_ABSOLUTE_PLUGIN = ( + "/home/vagrant/opentdf-python-sdk/.venv/bin/protoc-gen-connect-python" +) _EXPECTED_LINE = f" - local: {_ABSOLUTE_PLUGIN}" @@ -193,10 +192,15 @@ def _parse_tag(self, argv: list[str]) -> str | None: return git_tag def test_tag_equals_form(self): - assert self._parse_tag(["script.py", "--tag=service/v0.11.0"]) == "service/v0.11.0" + assert ( + self._parse_tag(["script.py", "--tag=service/v0.11.0"]) == "service/v0.11.0" + ) def test_tag_space_form(self): - assert self._parse_tag(["script.py", "--tag", "service/v0.10.0"]) == "service/v0.10.0" + assert ( + self._parse_tag(["script.py", "--tag", "service/v0.10.0"]) + == "service/v0.10.0" + ) def test_no_tag_returns_none(self): assert self._parse_tag(["script.py", "--download"]) is None @@ -313,11 +317,11 @@ def _make_tree(self, base: Path) -> None: """Create a directory tree that mirrors the buf generate output structure.""" dirs = [ base / "authorization", - base / "authorization" / "v2", # depth 2 — was missing __init__.py + base / "authorization" / "v2", # depth 2 — was missing __init__.py base / "entityresolution", - base / "entityresolution" / "v2", # depth 2 - base / "policy" / "attributes", # depth 2 - base / "policy" / "kasregistry", # depth 2 + base / "entityresolution" / "v2", # depth 2 + base / "policy" / "attributes", # depth 2 + base / "policy" / "kasregistry", # depth 2 base / "legacy_grpc" / "authorization" / "v2", # depth 3 ] for d in dirs: @@ -348,7 +352,9 @@ def test_depth_two_gets_init(self, tmp_path): def test_depth_three_gets_init(self, tmp_path): self._make_tree(tmp_path) gen.create_init_files(tmp_path) - assert (tmp_path / "legacy_grpc" / "authorization" / "v2" / "__init__.py").exists(), ( + assert ( + tmp_path / "legacy_grpc" / "authorization" / "v2" / "__init__.py" + ).exists(), ( "legacy_grpc/authorization/v2/__init__.py missing — create_init_files() does not recurse" ) From 49f00abd7d27659c0acc1466513a2b3fa21c87a1 Mon Sep 17 00:00:00 2001 From: b-long Date: Wed, 29 Apr 2026 23:26:48 +0000 Subject: [PATCH 04/14] fix(proto-gen): recurse into nested subdirs when creating __init__.py files create_init_files() was only walking one level deep, leaving nested packages like authorization/v2/ and policy/attributes/ without __init__.py files, causing ImportError on a clean generation run. Fixes #134 --- otdf-python-proto/scripts/generate_connect_proto.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/otdf-python-proto/scripts/generate_connect_proto.py b/otdf-python-proto/scripts/generate_connect_proto.py index 9c93deb..3ba5e74 100644 --- a/otdf-python-proto/scripts/generate_connect_proto.py +++ b/otdf-python-proto/scripts/generate_connect_proto.py @@ -189,13 +189,9 @@ def run_buf_generate(proto_gen_dir: Path) -> bool: def create_init_files(generated_dir: Path) -> None: """Create __init__.py files in generated directories.""" - # Create __init__.py in main generated directory - (generated_dir / "__init__.py").touch() - - # Create __init__.py files in any subdirectories - for subdir in generated_dir.iterdir(): - if subdir.is_dir(): - (subdir / "__init__.py").touch() + for dirpath in [generated_dir, *generated_dir.rglob("*")]: + if dirpath.is_dir(): + (dirpath / "__init__.py").touch() def _fix_ignore_if_default_value(proto_files_dir): From d473b8d914b4e670443d1c8fb20b6129cc9ad214 Mon Sep 17 00:00:00 2001 From: b-long Date: Wed, 29 Apr 2026 23:29:41 +0000 Subject: [PATCH 05/14] chore(proto-gen): update uv.lock with pytest dev dependency --- otdf-python-proto/uv.lock | 135 +++++++++++++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 1 deletion(-) diff --git a/otdf-python-proto/uv.lock b/otdf-python-proto/uv.lock index f4df382..5e8b04d 100644 --- a/otdf-python-proto/uv.lock +++ b/otdf-python-proto/uv.lock @@ -162,6 +162,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + [[package]] name = "connect-python" version = "0.4.2" @@ -182,6 +191,18 @@ compiler = [ { name = "protogen" }, ] +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -448,6 +469,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "multidict" version = "6.7.0" @@ -614,6 +644,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "mypy-protobuf" }, + { name = "pytest" }, ] [package.metadata] @@ -626,7 +657,28 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "mypy-protobuf", specifier = ">=3.6.0" }] +dev = [ + { name = "mypy-protobuf", specifier = ">=3.6.0" }, + { name = "pytest", specifier = ">=8.0.0" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] [[package]] name = "propcache" @@ -769,6 +821,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/a0/c3f3a2e2fa866547d82190ec5c0cd55580bc29c7894221bd793003a578a1/protogen-0.3.1-py3-none-any.whl", hash = "sha256:65b60b284d20ee4899d515b1959882d8c7504b271552de36f4ebfe77f6b07331", size = 21425, upload-time = "2023-11-20T15:34:45.958Z" }, ] +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + [[package]] name = "setuptools" version = "80.9.0" @@ -778,6 +857,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "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", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + [[package]] name = "types-protobuf" version = "6.32.1.20251210" From 9765f33dcc78b5c8781bbe9a72c55aecab30b1b8 Mon Sep 17 00:00:00 2001 From: b-long Date: Wed, 29 Apr 2026 23:33:02 +0000 Subject: [PATCH 06/14] fix(proto-gen): restore default GIT_TAG to service/v0.7.2 --- otdf-python-proto/scripts/generate_connect_proto.py | 2 +- otdf-python-proto/tests/test_generate_connect_proto.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/otdf-python-proto/scripts/generate_connect_proto.py b/otdf-python-proto/scripts/generate_connect_proto.py index 3ba5e74..463bb0a 100644 --- a/otdf-python-proto/scripts/generate_connect_proto.py +++ b/otdf-python-proto/scripts/generate_connect_proto.py @@ -46,7 +46,7 @@ def check_dependencies() -> bool: def copy_opentdf_proto_files(proto_gen_dir: Path, git_tag: str | None = None) -> bool: """Clone OpenTDF platform repository and copy all proto files.""" - GIT_TAG = git_tag or "service/v0.12.0" + GIT_TAG = git_tag or "service/v0.7.2" REPO_URL = "https://github.com/opentdf/platform.git" temp_repo_dir = proto_gen_dir / "temp_platform_repo" diff --git a/otdf-python-proto/tests/test_generate_connect_proto.py b/otdf-python-proto/tests/test_generate_connect_proto.py index f2b67ec..22d4504 100644 --- a/otdf-python-proto/tests/test_generate_connect_proto.py +++ b/otdf-python-proto/tests/test_generate_connect_proto.py @@ -175,7 +175,7 @@ def fake_run(cmd, **kwargs): git_cmd = next(c for c in captured if c[0] == "git") branch_idx = git_cmd.index("--branch") default_tag = git_cmd[branch_idx + 1] - assert default_tag != "service/v0.7.2" + assert default_tag == "service/v0.7.2" class TestArgParsing: From a72b84fef94d16f6a92231010eff0acd790609b3 Mon Sep 17 00:00:00 2001 From: b-long Date: Thu, 30 Apr 2026 00:59:19 +0000 Subject: [PATCH 07/14] tidy comments --- otdf-python-proto/tests/test_generate_connect_proto.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/otdf-python-proto/tests/test_generate_connect_proto.py b/otdf-python-proto/tests/test_generate_connect_proto.py index 22d4504..12ed681 100644 --- a/otdf-python-proto/tests/test_generate_connect_proto.py +++ b/otdf-python-proto/tests/test_generate_connect_proto.py @@ -1,10 +1,7 @@ """Unit tests for scripts/generate_connect_proto.py. These tests exercise the pure-Python logic in the generation script without -requiring network access, buf, or git to be present. Each test class maps -to one of the bugs documented in: - - https://github.com/b-long/opentdf-python-sdk/issues/133 (fixed) - - https://github.com/b-long/opentdf-python-sdk/issues/134 (remaining) +requiring network access, buf, or git to be present. """ import re From 9fe234298df853c346d05782b1c8bf9e8665daca Mon Sep 17 00:00:00 2001 From: b-long Date: Thu, 30 Apr 2026 01:04:21 +0000 Subject: [PATCH 08/14] fix(proto-gen): address Gemini review comments - Add argparse import and replace fragile manual sys.argv parsing with argparse in generate_connect_proto.py - Use lambda in re.sub replacement to avoid backslash interpretation in paths on Windows - Relax regex to use \s+ and \S* so plugin path prefix is optional - Replace `uv add` with `uv sync` in setup_connect_rpc.py to avoid modifying pyproject.toml/uv.lock for all users --- .../scripts/generate_connect_proto.py | 20 +++++++++---------- .../scripts/setup_connect_rpc.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/otdf-python-proto/scripts/generate_connect_proto.py b/otdf-python-proto/scripts/generate_connect_proto.py index 463bb0a..2f90b87 100644 --- a/otdf-python-proto/scripts/generate_connect_proto.py +++ b/otdf-python-proto/scripts/generate_connect_proto.py @@ -8,6 +8,7 @@ 4. Optionally generates legacy gRPC clients for backward compatibility """ +import argparse import re import subprocess import sys @@ -157,8 +158,8 @@ def run_buf_generate(proto_gen_dir: Path) -> bool: content = f.read() updated_content = re.sub( - r"- local: \S*protoc-gen-connect[_-]python\S*", - f"- local: {connect_plugin_path}", + r"- local:\s+\S*protoc-gen-connect[_-]python\S*", + lambda _: f"- local: {connect_plugin_path}", content, ) @@ -251,17 +252,16 @@ def main(): proto_files_dir.mkdir(exist_ok=True) generated_dir.mkdir(exist_ok=True) - # Parse optional --tag argument - git_tag = None - for i, arg in enumerate(sys.argv[1:], 1): - if arg.startswith("--tag="): - git_tag = arg.split("=", 1)[1] - elif arg == "--tag" and i + 1 < len(sys.argv): - git_tag = sys.argv[i + 1] + # Parse arguments + parser = argparse.ArgumentParser(description="OpenTDF Connect RPC Client Generator") + parser.add_argument("--tag", help="Git tag to use for OpenTDF platform") + parser.add_argument("--download", action="store_true", help="Force download of proto files") + args = parser.parse_args() + git_tag = args.tag # Download proto files (optional - can use existing files) if ( - "--download" in sys.argv or not any(proto_files_dir.glob("**/*.proto")) + args.download or not any(proto_files_dir.glob("**/*.proto")) ) and not download_proto_files(proto_gen_dir, git_tag=git_tag): return 1 diff --git a/otdf-python-proto/scripts/setup_connect_rpc.py b/otdf-python-proto/scripts/setup_connect_rpc.py index 6726259..a1b99f0 100755 --- a/otdf-python-proto/scripts/setup_connect_rpc.py +++ b/otdf-python-proto/scripts/setup_connect_rpc.py @@ -14,7 +14,7 @@ def main(): """Install Connect RPC compiler dependencies.""" print("Installing Connect RPC dependencies...") subprocess.run( - ["uv", "add", "connect-python[compiler]"], + ["uv", "sync"], check=True, ) print("✓ Connect RPC dependencies installed.") From 1ec583d4b5a06eef7b8573fb8d58cc2d9d0f154e Mon Sep 17 00:00:00 2001 From: b-long Date: Thu, 30 Apr 2026 01:07:01 +0000 Subject: [PATCH 09/14] chore: apply ruff formatting to generate_connect_proto.py --- otdf-python-proto/scripts/generate_connect_proto.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/otdf-python-proto/scripts/generate_connect_proto.py b/otdf-python-proto/scripts/generate_connect_proto.py index 2f90b87..522bac8 100644 --- a/otdf-python-proto/scripts/generate_connect_proto.py +++ b/otdf-python-proto/scripts/generate_connect_proto.py @@ -255,7 +255,9 @@ def main(): # Parse arguments parser = argparse.ArgumentParser(description="OpenTDF Connect RPC Client Generator") parser.add_argument("--tag", help="Git tag to use for OpenTDF platform") - parser.add_argument("--download", action="store_true", help="Force download of proto files") + parser.add_argument( + "--download", action="store_true", help="Force download of proto files" + ) args = parser.parse_args() git_tag = args.tag From 2e1ccdc17c28baf836c9dfe363545e39fab90bc1 Mon Sep 17 00:00:00 2001 From: b-long Date: Thu, 30 Apr 2026 09:46:42 +0000 Subject: [PATCH 10/14] refactor(proto-gen): replace subprocess rm -rf with shutil.rmtree Removes shell-out to rm -rf for temp directory cleanup, using the stdlib shutil.rmtree instead. Updates tests to assert the directory no longer exists rather than inspecting subprocess calls. --- .../scripts/generate_connect_proto.py | 5 +++-- .../tests/test_generate_connect_proto.py | 18 +++++------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/otdf-python-proto/scripts/generate_connect_proto.py b/otdf-python-proto/scripts/generate_connect_proto.py index 522bac8..e711bce 100644 --- a/otdf-python-proto/scripts/generate_connect_proto.py +++ b/otdf-python-proto/scripts/generate_connect_proto.py @@ -10,6 +10,7 @@ import argparse import re +import shutil import subprocess import sys from pathlib import Path @@ -59,7 +60,7 @@ def copy_opentdf_proto_files(proto_gen_dir: Path, git_tag: str | None = None) -> try: # Remove existing temp directory if it exists if temp_repo_dir.exists(): - subprocess.run(["rm", "-rf", str(temp_repo_dir)], check=True) + shutil.rmtree(temp_repo_dir) print(f"Cloning OpenTDF platform repository (tag: {GIT_TAG})...") @@ -122,7 +123,7 @@ def copy_opentdf_proto_files(proto_gen_dir: Path, git_tag: str | None = None) -> finally: # Clean up temp directory if temp_repo_dir.exists(): - subprocess.run(["rm", "-rf", str(temp_repo_dir)], check=False) + shutil.rmtree(temp_repo_dir) def download_proto_files(proto_gen_dir: Path, git_tag: str | None = None) -> bool: diff --git a/otdf-python-proto/tests/test_generate_connect_proto.py b/otdf-python-proto/tests/test_generate_connect_proto.py index 12ed681..baeacb7 100644 --- a/otdf-python-proto/tests/test_generate_connect_proto.py +++ b/otdf-python-proto/tests/test_generate_connect_proto.py @@ -264,14 +264,10 @@ def fake_run(cmd, **kwargs): def test_temp_dir_cleaned_up_on_success(self, tmp_path): """finally block must clean up temp_platform_repo regardless of outcome.""" - rm_calls: list = [] + temp_repo = tmp_path / "temp_platform_repo" def fake_run(cmd, **kwargs): - if cmd[0] == "rm": - rm_calls.append(cmd) - return MagicMock(returncode=0) if cmd[0] == "git": - temp_repo = tmp_path / "temp_platform_repo" service_dir = temp_repo / "service" / "kas" service_dir.mkdir(parents=True) (service_dir / "kas.proto").write_text('syntax = "proto3";\n') @@ -280,18 +276,14 @@ def fake_run(cmd, **kwargs): with patch("generate_connect_proto.subprocess.run", side_effect=fake_run): gen.copy_opentdf_proto_files(tmp_path) - assert any("temp_platform_repo" in str(c) for c in rm_calls) + assert not temp_repo.exists() def test_temp_dir_cleaned_up_on_failure(self, tmp_path): - rm_calls: list = [] + temp_repo = tmp_path / "temp_platform_repo" def fake_run(cmd, **kwargs): - if cmd[0] == "rm": - rm_calls.append(cmd) - return MagicMock(returncode=0) if cmd[0] == "git": - # Create the temp dir so the finally block has something to clean up. - (tmp_path / "temp_platform_repo").mkdir(exist_ok=True) + temp_repo.mkdir(exist_ok=True) raise gen.subprocess.CalledProcessError(1, "git") return MagicMock(returncode=0) @@ -299,7 +291,7 @@ def fake_run(cmd, **kwargs): result = gen.copy_opentdf_proto_files(tmp_path) assert result is False - assert any("temp_platform_repo" in str(c) for c in rm_calls) + assert not temp_repo.exists() # --------------------------------------------------------------------------- From 6307901c4990f4f08963b11656028761a624b0f0 Mon Sep 17 00:00:00 2001 From: b-long Date: Thu, 30 Apr 2026 10:08:20 +0000 Subject: [PATCH 11/14] test(proto-gen): test real argparse parser in TestArgParsing Replace the private _parse_tag helper (which tested a hand-rolled parser that no longer exists) with _get_parser(), which replicates the ArgumentParser setup from main() and calls parse_args() directly. --- .../tests/test_generate_connect_proto.py | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/otdf-python-proto/tests/test_generate_connect_proto.py b/otdf-python-proto/tests/test_generate_connect_proto.py index baeacb7..b371a65 100644 --- a/otdf-python-proto/tests/test_generate_connect_proto.py +++ b/otdf-python-proto/tests/test_generate_connect_proto.py @@ -178,33 +178,32 @@ def fake_run(cmd, **kwargs): class TestArgParsing: """--tag argument parsing in main() must populate git_tag correctly.""" - def _parse_tag(self, argv: list[str]) -> str | None: - """Run just the tag-parsing block from main() against a given argv.""" - git_tag = None - for i, arg in enumerate(argv[1:], 1): - if arg.startswith("--tag="): - git_tag = arg.split("=", 1)[1] - elif arg == "--tag" and i + 1 < len(argv): - git_tag = argv[i + 1] - return git_tag + def _get_parser(self): + """Replicates the ArgumentParser setup from main().""" + parser = gen.argparse.ArgumentParser( + description="OpenTDF Connect RPC Client Generator" + ) + parser.add_argument("--tag", help="Git tag to use for OpenTDF platform") + parser.add_argument( + "--download", action="store_true", help="Force download of proto files" + ) + return parser def test_tag_equals_form(self): - assert ( - self._parse_tag(["script.py", "--tag=service/v0.11.0"]) == "service/v0.11.0" - ) + args = self._get_parser().parse_args(["--tag=service/v0.11.0"]) + assert args.tag == "service/v0.11.0" def test_tag_space_form(self): - assert ( - self._parse_tag(["script.py", "--tag", "service/v0.10.0"]) - == "service/v0.10.0" - ) + args = self._get_parser().parse_args(["--tag", "service/v0.10.0"]) + assert args.tag == "service/v0.10.0" def test_no_tag_returns_none(self): - assert self._parse_tag(["script.py", "--download"]) is None + args = self._get_parser().parse_args(["--download"]) + assert args.tag is None def test_tag_alongside_download(self): - argv = ["script.py", "--download", "--tag=service/v0.12.0"] - assert self._parse_tag(argv) == "service/v0.12.0" + args = self._get_parser().parse_args(["--download", "--tag=service/v0.12.0"]) + assert args.tag == "service/v0.12.0" # --------------------------------------------------------------------------- From 13e2b7d1cda419f58a4f30d2c3d2e964da1efdcd Mon Sep 17 00:00:00 2001 From: b-long Date: Thu, 30 Apr 2026 10:21:25 +0000 Subject: [PATCH 12/14] fix(build-script): update build_connect_proto.sh for new output directory - Replace all references to generated/ with src/otdf_python_proto/, which is where buf generate now writes files - Drop the mkdir -p generated/ call (no longer needed) - Switch uv sync --dev (deprecated flag) to plain uv sync - Replace silent uv add fallback with a hard error pointing to setup_connect_rpc.py, so missing dependencies are caught early --- .../scripts/build_connect_proto.sh | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/otdf-python-proto/scripts/build_connect_proto.sh b/otdf-python-proto/scripts/build_connect_proto.sh index 3a90859..8503aae 100755 --- a/otdf-python-proto/scripts/build_connect_proto.sh +++ b/otdf-python-proto/scripts/build_connect_proto.sh @@ -44,28 +44,27 @@ fi echo "✓ uv is available" -# Install dependencies if needed +# Install dependencies echo "Installing/updating dependencies..." cd "$PROTO_GEN_DIR" -uv sync --dev +uv sync # Check if connect-python is available if ! uv run python -c "import connectrpc" 2>/dev/null; then - echo "Installing connect-python[compiler]..." - uv add "connect-python[compiler]>=0.4.2" + echo "Error: connect-python is not available after uv sync." + echo "Run: uv run python scripts/setup_connect_rpc.py" + exit 1 fi echo "✓ connect-python is available" # Clean up previous generated files +OUTPUT_DIR="$PROTO_GEN_DIR/src/otdf_python_proto" echo "Cleaning up previous generated files..." -if [[ -d "generated" ]]; then - rm -rf generated/* +if [[ -d "$OUTPUT_DIR" ]]; then + rm -rf "${OUTPUT_DIR:?}"/* fi -# Create generated directory -mkdir -p generated - # Run the generation echo "Generating Connect RPC protobuf files..." uv run python scripts/generate_connect_proto.py "$@" @@ -75,16 +74,12 @@ if [[ $? -eq 0 ]]; then echo "✓ Connect RPC generation complete!" echo "" echo "Generated files:" - echo " - generated/*_pb2.py (Protobuf message classes)" - echo " - generated/*_pb2.pyi (Type stubs)" - echo " - generated/*_connect.py (Connect RPC clients)" + echo " - src/otdf_python_proto/**/*_pb2.py (Protobuf message classes)" + echo " - src/otdf_python_proto/**/*_pb2.pyi (Type stubs)" + echo " - src/otdf_python_proto/**/*_connect.py (Connect RPC clients)" echo "" echo "Legacy gRPC files (if generated):" - echo " - generated/legacy_grpc/*_pb2_grpc.py (gRPC stubs)" - echo "" - echo "Usage examples:" - echo " cd .." - echo " python examples/connect_rpc_client_example.py" + echo " - src/otdf_python_proto/legacy_grpc/**/*_pb2_grpc.py (gRPC stubs)" echo "" echo "For more information, see:" echo " - docs/CONNECT_RPC.md" From 389c7f21a832f629d165cbf09936b94b622cdb81 Mon Sep 17 00:00:00 2001 From: b-long Date: Thu, 30 Apr 2026 10:28:26 +0000 Subject: [PATCH 13/14] docs: fix broken link and stale setup instruction - DEVELOPING.md: replace reference to non-existent PROTOBUF_SETUP.md with the correct CONNECT_RPC.md - otdf-python-proto/README.md: replace `uv add connect-python[compiler]` troubleshooting step with `uv run python scripts/setup_connect_rpc.py`, which is the correct setup path now that setup_connect_rpc.py exists --- docs/DEVELOPING.md | 2 +- otdf-python-proto/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/DEVELOPING.md b/docs/DEVELOPING.md index dfa5aac..4d9ec80 100644 --- a/docs/DEVELOPING.md +++ b/docs/DEVELOPING.md @@ -92,4 +92,4 @@ cd otdf-python-proto uv run python scripts/generate_connect_proto.py ``` -See [`otdf-python-proto/README.md`](../otdf-python-proto/README.md) and [`PROTOBUF_SETUP.md`](./PROTOBUF_SETUP.md) for details. +See [`otdf-python-proto/README.md`](../otdf-python-proto/README.md) and [`CONNECT_RPC.md`](./CONNECT_RPC.md) for details. diff --git a/otdf-python-proto/README.md b/otdf-python-proto/README.md index 4e04f07..ef47640 100644 --- a/otdf-python-proto/README.md +++ b/otdf-python-proto/README.md @@ -170,7 +170,7 @@ If you're migrating from traditional gRPC clients to Connect RPC: Install buf: `brew install bufbuild/buf/buf` ### "protoc-gen-connect_python not found" -Install with compiler support: `uv add connect-python[compiler]` +Run the setup script: `uv run python scripts/setup_connect_rpc.py` ### Import errors after generation Ensure `__init__.py` files exist in otdf_python_proto directories From 6849c02c7ee97888b11f180d2cf618d4fe5c8e27 Mon Sep 17 00:00:00 2001 From: b-long Date: Thu, 30 Apr 2026 11:34:42 +0000 Subject: [PATCH 14/14] fix: clarify error message and test name from PR review feedback Improve the connect-python missing error message to accurately state the dependency is absent (not just unsynced), and rename the test to reflect what it actually asserts. --- otdf-python-proto/scripts/build_connect_proto.sh | 5 +++-- otdf-python-proto/tests/test_generate_connect_proto.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/otdf-python-proto/scripts/build_connect_proto.sh b/otdf-python-proto/scripts/build_connect_proto.sh index 8503aae..a57f5c5 100755 --- a/otdf-python-proto/scripts/build_connect_proto.sh +++ b/otdf-python-proto/scripts/build_connect_proto.sh @@ -51,8 +51,9 @@ uv sync # Check if connect-python is available if ! uv run python -c "import connectrpc" 2>/dev/null; then - echo "Error: connect-python is not available after uv sync." - echo "Run: uv run python scripts/setup_connect_rpc.py" + echo "Error: connect-python is not in the installed dependencies." + echo "It may need to be added first. Run: uv run python scripts/setup_connect_rpc.py" + echo "Then re-run this script." exit 1 fi diff --git a/otdf-python-proto/tests/test_generate_connect_proto.py b/otdf-python-proto/tests/test_generate_connect_proto.py index b371a65..0979eb1 100644 --- a/otdf-python-proto/tests/test_generate_connect_proto.py +++ b/otdf-python-proto/tests/test_generate_connect_proto.py @@ -148,8 +148,8 @@ def test_another_custom_tag(self, tmp_path): used_tag = self._run_with_tag(tmp_path, "service/v0.12.0") assert used_tag == "service/v0.12.0" - def test_default_tag_is_not_old_hardcode(self, tmp_path): - """The default must no longer be the stale service/v0.7.2.""" + def test_default_tag_is_used_when_no_tag_is_provided(self, tmp_path): + """The default tag must be used when no git_tag is provided.""" proto_files_dir = tmp_path / "proto-files" proto_files_dir.mkdir()