@@ -300,6 +300,13 @@ def _run_post_processor(output: str, library_id: str):
300300 python_mono_repo .owlbot_main (path_to_library )
301301 else :
302302 raise SYNTHTOOL_IMPORT_ERROR # pragma: NO COVER
303+
304+ # If there is no noxfile, run `isort`` and `black` on the output.
305+ # This is required for proto-only libraries which are not GAPIC.
306+ if not Path (f"{ output } /{ path_to_library } /noxfile.py" ).exists ():
307+ subprocess .run (["isort" , output ])
308+ subprocess .run (["black" , output ])
309+
303310 logger .info ("Python post-processor ran successfully." )
304311
305312
@@ -541,16 +548,19 @@ def _read_bazel_build_py_rule(api_path: str, source: str) -> Dict:
541548 source (str): Path to the directory containing API protos.
542549
543550 Returns:
544- Dict: A dictionary containing the parsed attributes of the `_py_gapic` rule.
551+ Dict: A dictionary containing the parsed attributes of the `_py_gapic` rule, if found .
545552 """
546553 build_file_path = f"{ source } /{ api_path } /BUILD.bazel"
547554 content = _read_text_file (build_file_path )
548555
549556 result = parse_googleapis_content .parse_content (content )
550557 py_gapic_entries = [key for key in result .keys () if key .endswith ("_py_gapic" )]
551558
552- # Assuming only one _py_gapic rule per BUILD file for a given language
553- return result [py_gapic_entries [0 ]]
559+ # Assuming at most one _py_gapic rule per BUILD file for a given language
560+ if len (py_gapic_entries ) > 0 :
561+ return result [py_gapic_entries [0 ]]
562+ else :
563+ return {}
554564
555565
556566def _get_api_generator_options (
@@ -598,6 +608,26 @@ def _get_api_generator_options(
598608 return generator_options
599609
600610
611+ def _construct_protoc_command (api_path : str , tmp_dir : str ) -> str :
612+ """
613+ Constructs the full protoc command string.
614+
615+ Args:
616+ api_path (str): The relative path to the API directory.
617+ tmp_dir (str): The temporary directory for protoc output.
618+
619+ Returns:
620+ str: The complete protoc command string suitable for shell execution.
621+ """
622+ command_parts = [
623+ f"protoc { api_path } /*.proto" ,
624+ f"--python_out={ tmp_dir } " ,
625+ f"--pyi_out={ tmp_dir } " ,
626+ ]
627+
628+ return " " .join (command_parts )
629+
630+
601631def _determine_generator_command (
602632 api_path : str , tmp_dir : str , generator_options : List [str ]
603633) -> str :
@@ -626,7 +656,7 @@ def _determine_generator_command(
626656 return " " .join (command_parts )
627657
628658
629- def _run_generator_command (generator_command : str , source : str ):
659+ def _run_protoc_command (generator_command : str , source : str ):
630660 """
631661 Executes the protoc generation command using subprocess.
632662
@@ -645,6 +675,74 @@ def _run_generator_command(generator_command: str, source: str):
645675 )
646676
647677
678+ def _get_staging_child_directory (api_path : str ) -> str :
679+ """
680+ Determines the correct sub-path within 'owl-bot-staging' for the generated code.
681+
682+ For proto-only libraries, the structure is usually just the proto directory,
683+ e.g., 'thing-py/google/thing'.
684+ For GAPIC libraries, it's typically the version segment, e.g., 'v1'.
685+
686+ Args:
687+ api_path (str): The relative path to the API directory (e.g., 'google/cloud/language/v1').
688+
689+ Returns:
690+ str: The sub-directory name to use for staging.
691+ """
692+
693+ version_candidate = api_path .split ("/" )[- 1 ]
694+ if version_candidate .startswith ("v" ):
695+ return version_candidate
696+ else :
697+ # Fallback for non-'v' version segment
698+ return f"{ os .path .basename (api_path )} -py/{ api_path } "
699+
700+
701+ def _stage_proto_only_library (
702+ api_path : str , source_dir : str , tmp_dir : str , staging_dir : str
703+ ) -> None :
704+ """
705+ Handles staging for proto-only libraries (e.g., common protos).
706+
707+ This involves copying the generated python files and the original proto files.
708+
709+ Args:
710+ api_path (str): The relative path to the API directory.
711+ source_dir (str): Path to the directory containing API protos.
712+ tmp_dir (str): The temporary directory where protoc output was placed.
713+ staging_dir (str): The final destination for the staged code.
714+ """
715+ # 1. Copy the generated Python files (e.g., *_pb2.py) from the protoc output
716+ # The generated Python files are placed under a directory corresponding to `api_path`
717+ # inside the temporary directory, since the protoc command ran with `api_path`
718+ # specified.
719+ shutil .copytree (f"{ tmp_dir } /{ api_path } " , staging_dir , dirs_exist_ok = True )
720+
721+ # 2. Copy the original proto files to the staging directory
722+ # This is typically done for proto-only libraries so that the protos are included
723+ # in the distributed package.
724+ proto_glob_path = f"{ source_dir } /{ api_path } /*.proto"
725+ for proto_file in glob .glob (proto_glob_path ):
726+ # The glob is expected to find the file inside the source_dir.
727+ # We copy only the filename to the target staging directory.
728+ shutil .copyfile (proto_file , f"{ staging_dir } /{ os .path .basename (proto_file )} " )
729+
730+
731+ def _stage_gapic_library (tmp_dir : str , staging_dir : str ) -> None :
732+ """
733+ Handles staging for GAPIC client libraries.
734+
735+ This involves copying all contents from the temporary output directory.
736+
737+ Args:
738+ tmp_dir (str): The temporary directory where protoc/GAPIC generator output was placed.
739+ staging_dir (str): The final destination for the staged code.
740+ """
741+ # For GAPIC, the generator output is flat in `tmp_dir` and includes all
742+ # necessary files like setup.py, client library, etc.
743+ shutil .copytree (tmp_dir , staging_dir )
744+
745+
648746def _generate_api (
649747 api_path : str , library_id : str , source : str , output : str , gapic_version : str
650748):
@@ -660,18 +758,32 @@ def _generate_api(
660758 in a format which follows PEP-440.
661759 """
662760 py_gapic_config = _read_bazel_build_py_rule (api_path , source )
663- generator_options = _get_api_generator_options (
664- api_path , py_gapic_config , gapic_version = gapic_version
665- )
761+ is_proto_only_library = len (py_gapic_config ) == 0
666762
667763 with tempfile .TemporaryDirectory () as tmp_dir :
668- generator_command = _determine_generator_command (
669- api_path , tmp_dir , generator_options
764+ # 1. Determine the command for code generation
765+ if is_proto_only_library :
766+ command = _construct_protoc_command (api_path , tmp_dir )
767+ else :
768+ generator_options = _get_api_generator_options (
769+ api_path , py_gapic_config , gapic_version = gapic_version
770+ )
771+ command = _determine_generator_command (api_path , tmp_dir , generator_options )
772+
773+ # 2. Execute the code generation command
774+ _run_protoc_command (command , source )
775+
776+ # 3. Determine staging location
777+ staging_child_directory = _get_staging_child_directory (api_path )
778+ staging_dir = os .path .join (
779+ output , "owl-bot-staging" , library_id , staging_child_directory
670780 )
671- _run_generator_command (generator_command , source )
672- api_version = api_path .split ("/" )[- 1 ]
673- staging_dir = os .path .join (output , "owl-bot-staging" , library_id , api_version )
674- shutil .copytree (tmp_dir , staging_dir )
781+
782+ # 4. Stage the generated code
783+ if is_proto_only_library :
784+ _stage_proto_only_library (api_path , source , tmp_dir , staging_dir )
785+ else :
786+ _stage_gapic_library (tmp_dir , staging_dir )
675787
676788
677789def _run_nox_sessions (library_id : str , repo : str ):
0 commit comments