Skip to content

Commit 88d227d

Browse files
authored
fix(proto-gen): fix buf plugin path replacement, GIT_TAG, and generated_dir (#135)
* 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 * 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) * chore: fix ruff linting issues in test_generate_connect_proto.py Remove unused imports (call, pytest) and sort import block. * 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 * chore(proto-gen): update uv.lock with pytest dev dependency * fix(proto-gen): restore default GIT_TAG to service/v0.7.2 * tidy comments * 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 * chore: apply ruff formatting to generate_connect_proto.py * 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. * 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. * 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 * 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 * 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.
1 parent 30942c7 commit 88d227d

10 files changed

Lines changed: 586 additions & 45 deletions

File tree

.github/workflows/test-suite.yaml

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,26 @@ jobs:
3232
uv run ruff check
3333
uv run ruff format --check
3434
35-
# Step 2: Build (only after linting passes)
35+
# Step 2a: Proto sub-project unit tests (runs in parallel with build)
36+
proto-unit-tests:
37+
runs-on: ubuntu-22.04
38+
needs: lint-check
39+
steps:
40+
- uses: actions/checkout@v4
41+
42+
- name: Set up uv
43+
uses: astral-sh/setup-uv@v6
44+
with:
45+
enable-cache: true
46+
cache-dependency-glob: "otdf-python-proto/uv.lock"
47+
48+
- name: Run otdf-python-proto unit tests
49+
working-directory: otdf-python-proto
50+
run: |
51+
uv sync --frozen --group dev
52+
uv run pytest --tb=short -v tests/
53+
54+
# Step 2b: Build (only after linting passes)
3655
build:
3756
runs-on: ubuntu-22.04
3857
needs: lint-check
@@ -99,21 +118,22 @@ jobs:
99118

100119
report:
101120
runs-on: ubuntu-22.04
102-
needs: [lint-check, build, unit-tests, integration-tests]
121+
needs: [lint-check, proto-unit-tests, build, unit-tests, integration-tests]
103122
if: always()
104123
outputs:
105124
success: ${{ steps.check.outputs.success }}
106125
steps:
107126
- name: Check all jobs succeeded
108127
id: check
109128
run: |
110-
if [[ "${{ needs.lint-check.result }}" == "success" && "${{ needs.build.result }}" == "success" && "${{ needs.unit-tests.result }}" == "success" && "${{ needs.integration-tests.result }}" == "success" ]]; then
129+
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
111130
echo "success=true" >> $GITHUB_OUTPUT
112131
echo "✅ All tests passed!"
113132
else
114133
echo "success=false" >> $GITHUB_OUTPUT
115134
echo "❌ Some tests failed:"
116135
echo " Lint Check: ${{ needs.lint-check.result }}"
136+
echo " Proto Unit Tests: ${{ needs.proto-unit-tests.result }}"
117137
echo " Build: ${{ needs.build.result }}"
118138
echo " Unit Tests: ${{ needs.unit-tests.result }}"
119139
echo " Integration Tests: ${{ needs.integration-tests.result }}"

docs/DEVELOPING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,4 @@ cd otdf-python-proto
9292
uv run python scripts/generate_connect_proto.py
9393
```
9494

95-
See [`otdf-python-proto/README.md`](../otdf-python-proto/README.md) and [`PROTOBUF_SETUP.md`](./PROTOBUF_SETUP.md) for details.
95+
See [`otdf-python-proto/README.md`](../otdf-python-proto/README.md) and [`CONNECT_RPC.md`](./CONNECT_RPC.md) for details.

otdf-python-proto/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ If you're migrating from traditional gRPC clients to Connect RPC:
170170
Install buf: `brew install bufbuild/buf/buf`
171171

172172
### "protoc-gen-connect_python not found"
173-
Install with compiler support: `uv add connect-python[compiler]`
173+
Run the setup script: `uv run python scripts/setup_connect_rpc.py`
174174

175175
### Import errors after generation
176176
Ensure `__init__.py` files exist in otdf_python_proto directories

otdf-python-proto/pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ build-backend = "hatchling.build"
2323
[dependency-groups]
2424
dev = [
2525
"mypy-protobuf>=3.6.0",
26+
"pytest>=8.0.0",
2627
]
2728

29+
[tool.pytest.ini_options]
30+
testpaths = ["tests"]
31+
2832
[tool.hatch.build.targets.wheel]
2933
packages = ["src/otdf_python_proto"]
3034

otdf-python-proto/scripts/build_connect_proto.sh

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,28 +44,28 @@ fi
4444

4545
echo "✓ uv is available"
4646

47-
# Install dependencies if needed
47+
# Install dependencies
4848
echo "Installing/updating dependencies..."
4949
cd "$PROTO_GEN_DIR"
50-
uv sync --dev
50+
uv sync
5151

5252
# Check if connect-python is available
5353
if ! uv run python -c "import connectrpc" 2>/dev/null; then
54-
echo "Installing connect-python[compiler]..."
55-
uv add "connect-python[compiler]>=0.4.2"
54+
echo "Error: connect-python is not in the installed dependencies."
55+
echo "It may need to be added first. Run: uv run python scripts/setup_connect_rpc.py"
56+
echo "Then re-run this script."
57+
exit 1
5658
fi
5759

5860
echo "✓ connect-python is available"
5961

6062
# Clean up previous generated files
63+
OUTPUT_DIR="$PROTO_GEN_DIR/src/otdf_python_proto"
6164
echo "Cleaning up previous generated files..."
62-
if [[ -d "generated" ]]; then
63-
rm -rf generated/*
65+
if [[ -d "$OUTPUT_DIR" ]]; then
66+
rm -rf "${OUTPUT_DIR:?}"/*
6467
fi
6568

66-
# Create generated directory
67-
mkdir -p generated
68-
6969
# Run the generation
7070
echo "Generating Connect RPC protobuf files..."
7171
uv run python scripts/generate_connect_proto.py "$@"
@@ -75,16 +75,12 @@ if [[ $? -eq 0 ]]; then
7575
echo "✓ Connect RPC generation complete!"
7676
echo ""
7777
echo "Generated files:"
78-
echo " - generated/*_pb2.py (Protobuf message classes)"
79-
echo " - generated/*_pb2.pyi (Type stubs)"
80-
echo " - generated/*_connect.py (Connect RPC clients)"
78+
echo " - src/otdf_python_proto/**/*_pb2.py (Protobuf message classes)"
79+
echo " - src/otdf_python_proto/**/*_pb2.pyi (Type stubs)"
80+
echo " - src/otdf_python_proto/**/*_connect.py (Connect RPC clients)"
8181
echo ""
8282
echo "Legacy gRPC files (if generated):"
83-
echo " - generated/legacy_grpc/*_pb2_grpc.py (gRPC stubs)"
84-
echo ""
85-
echo "Usage examples:"
86-
echo " cd .."
87-
echo " python examples/connect_rpc_client_example.py"
83+
echo " - src/otdf_python_proto/legacy_grpc/**/*_pb2_grpc.py (gRPC stubs)"
8884
echo ""
8985
echo "For more information, see:"
9086
echo " - docs/CONNECT_RPC.md"

otdf-python-proto/scripts/generate_connect_proto.py

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
4. Optionally generates legacy gRPC clients for backward compatibility
99
"""
1010

11+
import argparse
12+
import re
13+
import shutil
1114
import subprocess
1215
import sys
1316
from pathlib import Path
@@ -43,9 +46,9 @@ def check_dependencies() -> bool:
4346
return True
4447

4548

46-
def copy_opentdf_proto_files(proto_gen_dir: Path) -> bool:
49+
def copy_opentdf_proto_files(proto_gen_dir: Path, git_tag: str | None = None) -> bool:
4750
"""Clone OpenTDF platform repository and copy all proto files."""
48-
GIT_TAG = "service/v0.7.2"
51+
GIT_TAG = git_tag or "service/v0.7.2"
4952
REPO_URL = "https://github.com/opentdf/platform.git"
5053

5154
temp_repo_dir = proto_gen_dir / "temp_platform_repo"
@@ -57,7 +60,7 @@ def copy_opentdf_proto_files(proto_gen_dir: Path) -> bool:
5760
try:
5861
# Remove existing temp directory if it exists
5962
if temp_repo_dir.exists():
60-
subprocess.run(["rm", "-rf", str(temp_repo_dir)], check=True)
63+
shutil.rmtree(temp_repo_dir)
6164

6265
print(f"Cloning OpenTDF platform repository (tag: {GIT_TAG})...")
6366

@@ -120,17 +123,15 @@ def copy_opentdf_proto_files(proto_gen_dir: Path) -> bool:
120123
finally:
121124
# Clean up temp directory
122125
if temp_repo_dir.exists():
123-
subprocess.run(["rm", "-rf", str(temp_repo_dir)], check=False)
126+
shutil.rmtree(temp_repo_dir)
124127

125-
return False
126128

127-
128-
def download_proto_files(proto_gen_dir: Path) -> bool:
129+
def download_proto_files(proto_gen_dir: Path, git_tag: str | None = None) -> bool:
129130
"""Download proto files from OpenTDF platform."""
130131
print("Copying proto files from OpenTDF platform...")
131132

132133
try:
133-
return copy_opentdf_proto_files(proto_gen_dir)
134+
return copy_opentdf_proto_files(proto_gen_dir, git_tag=git_tag)
134135
except Exception as e:
135136
print(f"Error getting proto files: {e}")
136137
return False
@@ -152,14 +153,15 @@ def run_buf_generate(proto_gen_dir: Path) -> bool:
152153
connect_plugin_path = result.stdout.strip()
153154
print(f"Using Connect plugin at: {connect_plugin_path}")
154155

155-
# Update buf.gen.yaml with the correct path
156+
# Update buf.gen.yaml with the correct absolute path for the local plugin
156157
buf_gen_path = proto_gen_dir / "buf.gen.yaml"
157158
with buf_gen_path.open() as f:
158159
content = f.read()
159160

160-
# Replace the local plugin path
161-
updated_content = content.replace(
162-
"- local: protoc-gen-connect-python", f"- local: {connect_plugin_path}"
161+
updated_content = re.sub(
162+
r"- local:\s+\S*protoc-gen-connect[_-]python\S*",
163+
lambda _: f"- local: {connect_plugin_path}",
164+
content,
163165
)
164166

165167
with buf_gen_path.open("w") as f:
@@ -189,13 +191,9 @@ def run_buf_generate(proto_gen_dir: Path) -> bool:
189191

190192
def create_init_files(generated_dir: Path) -> None:
191193
"""Create __init__.py files in generated directories."""
192-
# Create __init__.py in main generated directory
193-
(generated_dir / "__init__.py").touch()
194-
195-
# Create __init__.py files in any subdirectories
196-
for subdir in generated_dir.iterdir():
197-
if subdir.is_dir():
198-
(subdir / "__init__.py").touch()
194+
for dirpath in [generated_dir, *generated_dir.rglob("*")]:
195+
if dirpath.is_dir():
196+
(dirpath / "__init__.py").touch()
199197

200198

201199
def _fix_ignore_if_default_value(proto_files_dir):
@@ -245,7 +243,7 @@ def main():
245243
# Get the proto-gen directory (parent of scripts)
246244
proto_gen_dir = Path(__file__).parent.parent
247245
proto_files_dir = proto_gen_dir / "proto-files"
248-
generated_dir = proto_gen_dir / "generated"
246+
generated_dir = proto_gen_dir / "src" / "otdf_python_proto"
249247

250248
# Check dependencies
251249
if not check_dependencies():
@@ -255,10 +253,19 @@ def main():
255253
proto_files_dir.mkdir(exist_ok=True)
256254
generated_dir.mkdir(exist_ok=True)
257255

256+
# Parse arguments
257+
parser = argparse.ArgumentParser(description="OpenTDF Connect RPC Client Generator")
258+
parser.add_argument("--tag", help="Git tag to use for OpenTDF platform")
259+
parser.add_argument(
260+
"--download", action="store_true", help="Force download of proto files"
261+
)
262+
args = parser.parse_args()
263+
git_tag = args.tag
264+
258265
# Download proto files (optional - can use existing files)
259266
if (
260-
"--download" in sys.argv or not any(proto_files_dir.glob("**/*.proto"))
261-
) and not download_proto_files(proto_gen_dir):
267+
args.download or not any(proto_files_dir.glob("**/*.proto"))
268+
) and not download_proto_files(proto_gen_dir, git_tag=git_tag):
262269
return 1
263270

264271
# Check if we have any proto files
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env python3
2+
"""Setup script for Connect RPC dependencies.
3+
4+
Run this script once to install the required tools before generating proto files:
5+
6+
uv run python scripts/setup_connect_rpc.py
7+
"""
8+
9+
import subprocess
10+
import sys
11+
12+
13+
def main():
14+
"""Install Connect RPC compiler dependencies."""
15+
print("Installing Connect RPC dependencies...")
16+
subprocess.run(
17+
["uv", "sync"],
18+
check=True,
19+
)
20+
print("✓ Connect RPC dependencies installed.")
21+
print(" Run 'uv run python scripts/generate_connect_proto.py' to generate files.")
22+
return 0
23+
24+
25+
if __name__ == "__main__":
26+
sys.exit(main())

otdf-python-proto/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)