Skip to content

Commit 5a047ac

Browse files
authored
feat: create repo-metadata during generation (#14538)
This PR creates a basic `.repo-metadata.json` file for a package that is to be generated using librarian. Note that if a `.repo-metadata.json` file for a package exists in `generator-input`, then the generated metadata file will be overriden. There will be follow up PRs to address the TODO comments. Towards googleapis/librarian#2262 🦕
1 parent 4513c85 commit 5a047ac

File tree

2 files changed

+167
-7
lines changed

2 files changed

+167
-7
lines changed

.generator/cli.py

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def _write_json_file(path: str, updated_content: Dict):
115115
json.dump(updated_content, f, indent=2)
116116
f.write("\n")
117117

118+
118119
def _add_new_library_source_roots(library_config: Dict, library_id: str) -> None:
119120
"""Adds the default source_roots to the library configuration if not present.
120121
@@ -320,13 +321,22 @@ def _copy_files_needed_for_post_processing(output: str, input: str, library_id:
320321
"""
321322

322323
path_to_library = f"packages/{library_id}"
324+
repo_metadata_path = f"{input}/{path_to_library}/.repo-metadata.json"
323325

324326
# We need to create these directories so that we can copy files necessary for post-processing.
325-
os.makedirs(f"{output}/{path_to_library}/scripts/client-post-processing", exist_ok=True)
326-
shutil.copy(
327-
f"{input}/{path_to_library}/.repo-metadata.json",
328-
f"{output}/{path_to_library}/.repo-metadata.json",
327+
os.makedirs(
328+
f"{output}/{path_to_library}/scripts/client-post-processing", exist_ok=True
329329
)
330+
# TODO(https://github.com/googleapis/librarian/issues/2334):
331+
# if `.repo-metadata.json` for a library exists in
332+
# `.librarian/generator-input`, then we override the generated `.repo-metadata.json`
333+
# with what we have in `generator-input`. Remove this logic once the
334+
# generated `.repo-metadata.json` file is completely backfilled.
335+
if os.path.exists(repo_metadata_path):
336+
shutil.copy(
337+
repo_metadata_path,
338+
f"{output}/{path_to_library}/.repo-metadata.json",
339+
)
330340

331341
# copy post-procesing files
332342
for post_processing_file in glob.glob(
@@ -373,6 +383,78 @@ def _clean_up_files_after_post_processing(output: str, library_id: str):
373383
os.remove(gapic_version_file)
374384

375385

386+
def _create_repo_metadata_from_service_config(
387+
service_config_name: str, api_path: str, source: str, library_id: str
388+
) -> Dict:
389+
"""Creates the .repo-metadata.json content from the service config.
390+
391+
Args:
392+
service_config_name (str): The name of the service config file.
393+
api_path (str): The path to the API.
394+
source (str): The path to the source directory.
395+
library_id (str): The ID of the library.
396+
397+
Returns:
398+
Dict: The content of the .repo-metadata.json file.
399+
"""
400+
full_service_config_path = f"{source}/{api_path}/{service_config_name}"
401+
402+
# TODO(https://github.com/googleapis/librarian/issues/2332): Read the api
403+
# service config to backfill `.repo-metadata.json`.
404+
return {
405+
"api_shortname": "",
406+
"name_pretty": "",
407+
"product_documentation": "",
408+
"api_description": "",
409+
"client_documentation": "",
410+
"issue_tracker": "",
411+
"release_level": "",
412+
"language": "python",
413+
"library_type": "GAPIC_AUTO",
414+
"repo": "googleapis/google-cloud-python",
415+
"distribution_name": "",
416+
"api_id": "",
417+
}
418+
419+
420+
def _generate_repo_metadata_file(
421+
output: str, library_id: str, source: str, apis: List[Dict]
422+
):
423+
"""Generates the .repo-metadata.json file from the primary API service config.
424+
425+
Args:
426+
output (str): The path to the output directory.
427+
library_id (str): The ID of the library.
428+
source (str): The path to the source directory.
429+
apis (List[Dict]): A list of APIs to generate.
430+
"""
431+
path_to_library = f"packages/{library_id}"
432+
output_repo_metadata = f"{output}/{path_to_library}/.repo-metadata.json"
433+
434+
# TODO(https://github.com/googleapis/librarian/issues/2334)): If `.repo-metadata.json`
435+
# already exists in the `output` dir, then this means that it has been successfully copied
436+
# over from the `input` dir and we can skip the logic here. Remove the following logic
437+
# once we clean up all the `.repo-metadata.json` files from `.librarian/generator-input`.
438+
if os.path.exists(output_repo_metadata):
439+
return
440+
441+
os.makedirs(f"{output}/{path_to_library}", exist_ok=True)
442+
443+
# TODO(https://github.com/googleapis/librarian/issues/2333): Programatically
444+
# determine the primary api to be used to
445+
# to determine the information for metadata. For now, let's use the first
446+
# api in the list.
447+
primary_api = apis[0]
448+
449+
metadata_content = _create_repo_metadata_from_service_config(
450+
primary_api.get("service_config"),
451+
primary_api.get("path"),
452+
source,
453+
library_id,
454+
)
455+
_write_json_file(output_repo_metadata, metadata_content)
456+
457+
376458
def handle_generate(
377459
librarian: str = LIBRARIAN_DIR,
378460
source: str = SOURCE_DIR,
@@ -411,6 +493,7 @@ def handle_generate(
411493
if api_path:
412494
_generate_api(api_path, library_id, source, output)
413495
_copy_files_needed_for_post_processing(output, input, library_id)
496+
_generate_repo_metadata_file(output, library_id, source, apis_to_generate)
414497
_run_post_processor(output, library_id)
415498
_clean_up_files_after_post_processing(output, library_id)
416499
except Exception as e:
@@ -961,7 +1044,9 @@ def _process_changelog(
9611044
entry_parts.append(f"\n\n### {change_type_map[adjusted_change_type]}\n")
9621045
for change in library_changes:
9631046
commit_link = f"([{change[source_commit_hash_key]}]({_REPO_URL}/commit/{change[source_commit_hash_key]}))"
964-
entry_parts.append(f"* {change[subject_key]} {change[body_key]} {commit_link}")
1047+
entry_parts.append(
1048+
f"* {change[subject_key]} {change[body_key]} {commit_link}"
1049+
)
9651050

9661051
new_entry_text = "\n".join(entry_parts)
9671052
anchor_pattern = re.compile(

.generator/test_cli.py

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@
3737
_clean_up_files_after_post_processing,
3838
_copy_files_needed_for_post_processing,
3939
_create_main_version_header,
40+
_create_repo_metadata_from_service_config,
4041
_determine_generator_command,
4142
_determine_library_namespace,
4243
_generate_api,
44+
_generate_repo_metadata_file,
4345
_get_api_generator_options,
4446
_get_library_dist_name,
4547
_get_library_id,
@@ -742,13 +744,28 @@ def test_invalid_json(mocker):
742744
_read_json_file("fake/path.json")
743745

744746

745-
def test_copy_files_needed_for_post_processing_success(mocker):
747+
def test_copy_files_needed_for_post_processing_copies_metadata_if_exists(mocker):
748+
"""Tests that .repo-metadata.json is copied if it exists."""
746749
mock_makedirs = mocker.patch("os.makedirs")
747750
mock_shutil_copy = mocker.patch("shutil.copy")
751+
mocker.patch("os.path.exists", return_value=True)
752+
748753
_copy_files_needed_for_post_processing("output", "input", "library_id")
749754

750-
mock_makedirs.assert_called()
751755
mock_shutil_copy.assert_called_once()
756+
mock_makedirs.assert_called()
757+
758+
759+
def test_copy_files_needed_for_post_processing_skips_metadata_if_not_exists(mocker):
760+
"""Tests that .repo-metadata.json is not copied if it does not exist."""
761+
mock_makedirs = mocker.patch("os.makedirs")
762+
mock_shutil_copy = mocker.patch("shutil.copy")
763+
mocker.patch("os.path.exists", return_value=False)
764+
765+
_copy_files_needed_for_post_processing("output", "input", "library_id")
766+
767+
mock_shutil_copy.assert_not_called()
768+
mock_makedirs.assert_called()
752769

753770

754771
def test_clean_up_files_after_post_processing_success(mocker):
@@ -1091,6 +1108,64 @@ def test_determine_library_namespace_success(
10911108
assert namespace == expected_namespace
10921109

10931110

1111+
def test_create_repo_metadata_from_service_config():
1112+
"""Tests the creation of .repo-metadata.json content."""
1113+
service_config_name = "service_config.yaml"
1114+
api_path = "google/cloud/language/v1"
1115+
source = "/source"
1116+
library_id = "google-cloud-language"
1117+
1118+
metadata = _create_repo_metadata_from_service_config(
1119+
service_config_name, api_path, source, library_id
1120+
)
1121+
1122+
assert metadata["language"] == "python"
1123+
assert metadata["library_type"] == "GAPIC_AUTO"
1124+
assert metadata["repo"] == "googleapis/google-cloud-python"
1125+
1126+
1127+
def test_generate_repo_metadata_file(mocker):
1128+
"""Tests the generation of the .repo-metadata.json file."""
1129+
mock_write_json = mocker.patch("cli._write_json_file")
1130+
mock_create_metadata = mocker.patch(
1131+
"cli._create_repo_metadata_from_service_config",
1132+
return_value={"repo": "googleapis/google-cloud-python"},
1133+
)
1134+
mocker.patch("os.makedirs")
1135+
1136+
output = "/output"
1137+
library_id = "google-cloud-language"
1138+
source = "/source"
1139+
apis = [
1140+
{
1141+
"service_config": "service_config.yaml",
1142+
"path": "google/cloud/language/v1",
1143+
}
1144+
]
1145+
1146+
_generate_repo_metadata_file(output, library_id, source, apis)
1147+
1148+
mock_create_metadata.assert_called_once_with(
1149+
"service_config.yaml", "google/cloud/language/v1", source, library_id
1150+
)
1151+
mock_write_json.assert_called_once_with(
1152+
f"{output}/packages/{library_id}/.repo-metadata.json",
1153+
{"repo": "googleapis/google-cloud-python"},
1154+
)
1155+
1156+
1157+
def test_generate_repo_metadata_file_skips_if_exists(mocker):
1158+
"""Tests that the generation of the .repo-metadata.json file is skipped if it already exists."""
1159+
mock_write_json = mocker.patch("cli._write_json_file")
1160+
mock_create_metadata = mocker.patch("cli._create_repo_metadata_from_service_config")
1161+
mocker.patch("os.path.exists", return_value=True)
1162+
1163+
_generate_repo_metadata_file("output", "library_id", "source", [])
1164+
1165+
mock_create_metadata.assert_not_called()
1166+
mock_write_json.assert_not_called()
1167+
1168+
10941169
def test_determine_library_namespace_fails_not_subpath():
10951170
"""Tests that a ValueError is raised if the gapic path is not inside the package root."""
10961171
pkg_root_path = Path("repo/packages/my-lib")

0 commit comments

Comments
 (0)