Skip to content

Commit 98f98e3

Browse files
authored
feat: read content of generate request (#14113)
Closes #14122
1 parent f955689 commit 98f98e3

File tree

6 files changed

+219
-32
lines changed

6 files changed

+219
-32
lines changed

.generator/Dockerfile

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,10 @@ RUN apt-get update && \
3434
rm -rf /var/lib/apt/lists/*
3535

3636
# Set up environment variables for tool versions to make updates easier.
37-
# TODO(): Downgrade to Python `3.11` if `3.13.5` is incompatible with googleapis.
38-
ENV PYTHON_VERSION=3.13.5
37+
ENV PYTHON_VERSION=3.11.5
3938

4039
# Create a symbolic link for `python3` to point to our specific version.
41-
ENV PATH /usr/local/bin/python3.13:$PATH
40+
ENV PATH /usr/local/bin/python3.11:$PATH
4241

4342
# Install Python from source
4443
RUN wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz && \
@@ -64,8 +63,7 @@ WORKDIR /app
6463
# TODO(https://github.com/googleapis/librarian/issues/906): Clone googleapis and run bazelisk build.
6564

6665
# Copy the CLI script into the container and set ownership.
67-
# No other files or dependencies are needed for this simple script.
68-
COPY cli.py .
66+
COPY .generator/cli.py .
6967

7068
# Set the entrypoint for the container to run the script.
71-
ENTRYPOINT ["python3.13", "./cli.py"]
69+
ENTRYPOINT ["python3.11", "./cli.py"]

.generator/cli.py

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,88 @@
1313
# limitations under the License.
1414

1515
import argparse
16+
import json
17+
import logging
18+
import os
1619
import sys
1720

18-
def handle_configure(dry_run=False):
19-
# TODO(https://github.com/googleapis/librarian/issues/466): Implement configure command.
20-
print("'configure' command executed.")
21+
logger = logging.getLogger()
2122

22-
def handle_generate(dry_run=False):
23-
# TODO(https://github.com/googleapis/librarian/issues/448): Implement generate command.
24-
print("'generate' command executed.")
23+
LIBRARIAN_DIR = "librarian"
24+
GENERATE_REQUEST_FILE = "generate-request.json"
2525

26-
def handle_build(dry_run=False):
27-
# TODO(https://github.com/googleapis/librarian/issues/450): Implement build command.
28-
print("'build' command executed.")
26+
27+
def _read_json_file(path):
28+
"""Helper function that reads a json file path and returns the loaded json content.
29+
30+
Args:
31+
path (str): The file path to read.
32+
33+
Returns:
34+
dict: The parsed JSON content.
35+
36+
Raises:
37+
FileNotFoundError: If the file is not found at the specified path.
38+
json.JSONDecodeError: If the file does not contain valid JSON.
39+
IOError: If there is an issue reading the file.
40+
"""
41+
with open(path, "r") as f:
42+
return json.load(f)
43+
44+
45+
def handle_configure():
46+
# TODO(https://github.com/googleapis/librarian/issues/466): Implement configure command and update docstring.
47+
logger.info("'configure' command executed.")
48+
49+
50+
def handle_generate():
51+
"""The main coordinator for the code generation process.
52+
53+
This function orchestrates the generation of a client library by reading a
54+
`librarian/generate-request.json` file, determining the necessary Bazel rule for each API, and
55+
(in future steps) executing the build.
56+
57+
Raises:
58+
ValueError: If the `generate-request.json` file is not found or read.
59+
"""
60+
61+
# Read a generate-request.json file
62+
try:
63+
request_data = _read_json_file(f"{LIBRARIAN_DIR}/{GENERATE_REQUEST_FILE}")
64+
except Exception as e:
65+
raise ValueError(
66+
f"failed to read {LIBRARIAN_DIR}/{GENERATE_REQUEST_FILE}"
67+
) from e
68+
69+
logger.info(json.dumps(request_data, indent=2))
70+
71+
# TODO(https://github.com/googleapis/librarian/issues/448): Implement generate command and update docstring.
72+
logger.info("'generate' command executed.")
73+
74+
75+
def handle_build():
76+
# TODO(https://github.com/googleapis/librarian/issues/450): Implement build command and update docstring.
77+
logger.info("'build' command executed.")
2978

3079

3180
if __name__ == "__main__":
3281
parser = argparse.ArgumentParser(description="A simple CLI tool.")
33-
subparsers = parser.add_subparsers(dest="command", required=True, help="Available commands")
82+
subparsers = parser.add_subparsers(
83+
dest="command", required=True, help="Available commands"
84+
)
3485

3586
# Define commands
87+
handler_map = {
88+
"configure": handle_configure,
89+
"generate": handle_generate,
90+
"build": handle_build,
91+
}
92+
3693
for command_name, help_text in [
3794
("configure", "Onboard a new library or an api path to Librarian workflow."),
3895
("generate", "generate a python client for an API."),
39-
("build", "Run unit tests via nox for the generated library.")
96+
("build", "Run unit tests via nox for the generated library."),
4097
]:
41-
handler_map = {"configure": handle_configure, "generate": handle_generate, "build": handle_build}
4298
parser_cmd = subparsers.add_parser(command_name, help=help_text)
4399
parser_cmd.set_defaults(func=handler_map[command_name])
44100

@@ -47,4 +103,4 @@ def handle_build(dry_run=False):
47103
sys.exit(1)
48104

49105
args = parser.parse_args()
50-
args.func(args)
106+
args.func()

.generator/requirements-test.in

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
pytest
16+
pytest-mock

.generator/test_cli.py

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,105 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from cli import handle_generate, handle_build, handle_configure
15+
import os
16+
import pytest
17+
import json
18+
import logging
1619

20+
from unittest.mock import mock_open
1721

18-
def test_handle_configure_dry_run():
19-
# This is a simple test to ensure that the dry run command succeeds.
20-
handle_configure(dry_run=True)
22+
from cli import (
23+
_read_json_file,
24+
handle_generate,
25+
handle_build,
26+
handle_configure,
27+
LIBRARIAN_DIR,
28+
GENERATE_REQUEST_FILE,
29+
)
2130

22-
def test_handle_generate_dry_run():
23-
# This is a simple test to ensure that the dry run command succeeds.
24-
handle_generate(dry_run=True)
2531

26-
def test_handle_build_dry_run():
27-
# This is a simple test to ensure that the dry run command succeeds.
28-
handle_build(dry_run=True)
32+
@pytest.fixture
33+
def mock_generate_request_file(tmp_path, monkeypatch):
34+
"""Creates the mock request file at the correct path inside a temp dir."""
35+
# Create the path as expected by the script: .librarian/generate-request.json
36+
request_path = f"{LIBRARIAN_DIR}/{GENERATE_REQUEST_FILE}"
37+
request_dir = tmp_path / os.path.dirname(request_path)
38+
request_dir.mkdir()
39+
request_file = request_dir / os.path.basename(request_path)
40+
41+
request_content = {
42+
"id": "google-cloud-language",
43+
"apis": [{"path": "google/cloud/language/v1"}],
44+
}
45+
request_file.write_text(json.dumps(request_content))
46+
47+
# Change the current working directory to the temp path for the test.
48+
monkeypatch.chdir(tmp_path)
49+
return request_file
50+
51+
52+
def test_handle_configure_success(caplog, mock_generate_request_file):
53+
"""
54+
Tests the successful execution path of handle_configure.
55+
"""
56+
caplog.set_level(logging.INFO)
57+
58+
handle_configure()
59+
60+
assert "'configure' command executed." in caplog.text
61+
62+
63+
def test_handle_generate_success(caplog, mock_generate_request_file):
64+
"""
65+
Tests the successful execution path of handle_generate.
66+
"""
67+
caplog.set_level(logging.INFO)
68+
69+
handle_generate()
70+
71+
assert "google-cloud-language" in caplog.text
72+
assert "'generate' command executed." in caplog.text
73+
74+
75+
def test_handle_generate_fail(caplog):
76+
"""
77+
Tests the failed to read `librarian/generate-request.json` file in handle_generates.
78+
"""
79+
with pytest.raises(ValueError):
80+
handle_generate()
81+
82+
83+
def test_handle_build_success(caplog, mock_generate_request_file):
84+
"""
85+
Tests the successful execution path of handle_build.
86+
"""
87+
caplog.set_level(logging.INFO)
88+
89+
handle_build()
90+
91+
assert "'build' command executed." in caplog.text
92+
93+
94+
def test_read_valid_json(mocker):
95+
"""Tests reading a valid JSON file."""
96+
mock_content = '{"key": "value"}'
97+
mocker.patch("builtins.open", mocker.mock_open(read_data=mock_content))
98+
result = _read_json_file("fake/path.json")
99+
assert result == {"key": "value"}
100+
101+
102+
def test_file_not_found(mocker):
103+
"""Tests behavior when the file does not exist."""
104+
mocker.patch("builtins.open", side_effect=FileNotFoundError("No such file"))
105+
106+
with pytest.raises(FileNotFoundError):
107+
_read_json_file("non/existent/path.json")
108+
109+
110+
def test_invalid_json(mocker):
111+
"""Tests reading a file with malformed JSON."""
112+
mock_content = '{"key": "value",}'
113+
mocker.patch("builtins.open", mocker.mock_open(read_data=mock_content))
114+
115+
with pytest.raises(json.JSONDecodeError):
116+
_read_json_file("fake/path.json")

.github/workflows/generator.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ jobs:
2121
- name: Setup Python
2222
uses: actions/setup-python@v5
2323
with:
24-
python-version: "3.10"
25-
- name: Install pytest
24+
python-version: "3.11"
25+
- name: Install dependencies
2626
run: |
27-
python -m pip install pytest
27+
python -m pip install --upgrade pip
28+
pip install -r .generator/requirements-test.in
2829
- name: Run generator_cli tests
2930
run: |
3031
pytest .generator/test_cli.py

cloudbuild.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
steps:
16+
# This step builds the Docker image.
17+
- name: 'gcr.io/cloud-builders/docker'
18+
args:
19+
- 'build'
20+
- '-t'
21+
- 'gcr.io/$PROJECT_ID/python-librarian-generator:latest'
22+
- '-f'
23+
- '.generator/Dockerfile'
24+
- '.'
25+
26+
# This section automatically create a storage bucket for storing docker build logs.
27+
options:
28+
default_logs_bucket_behavior: REGIONAL_USER_OWNED_BUCKET

0 commit comments

Comments
 (0)