@@ -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,75 @@ def _run_generator_command(generator_command: str, source: str):
645675 )
646676
647677
678+ def _get_staging_child_directory (api_path : str , is_proto_only : bool ) -> 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., 'google/protobuf/struct'.
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+ is_proto_only (bool): True if the library is proto-only (no GAPIC rule).
689+
690+ Returns:
691+ str: The sub-directory name to use for staging.
692+ """
693+
694+ version_candidate = api_path .split ("/" )[- 1 ]
695+ if version_candidate .startswith ("v" ):
696+ return version_candidate
697+ else :
698+ # Fallback for non-'v' version segment
699+ return f"{ os .path .basename (api_path )} -py/{ api_path } "
700+
701+
702+ def _stage_proto_only_library (
703+ api_path : str , source_dir : str , tmp_dir : str , staging_dir : str
704+ ) -> None :
705+ """
706+ Handles staging for proto-only libraries (e.g., common protos).
707+
708+ This involves copying the generated python files and the original proto files.
709+
710+ Args:
711+ api_path (str): The relative path to the API directory.
712+ source_dir (str): Path to the directory containing API protos.
713+ tmp_dir (str): The temporary directory where protoc output was placed.
714+ staging_dir (str): The final destination for the staged code.
715+ """
716+ # 1. Copy the generated Python files (e.g., *_pb2.py) from the protoc output
717+ # The generated Python files are placed under a directory corresponding to `api_path`
718+ # inside the temporary directory, since the protoc command ran with `api_path`
719+ # specified.
720+ shutil .copytree (f"{ tmp_dir } /{ api_path } " , staging_dir , dirs_exist_ok = True )
721+
722+ # 2. Copy the original proto files to the staging directory
723+ # This is typically done for proto-only libraries so that the protos are included
724+ # in the distributed package.
725+ proto_glob_path = f"{ source_dir } /{ api_path } /*.proto"
726+ for proto_file in glob .glob (proto_glob_path ):
727+ # The glob is expected to find the file inside the source_dir.
728+ # We copy only the filename to the target staging directory.
729+ shutil .copyfile (proto_file , f"{ staging_dir } /{ os .path .basename (proto_file )} " )
730+
731+
732+ def _stage_gapic_library (tmp_dir : str , staging_dir : str ) -> None :
733+ """
734+ Handles staging for GAPIC client libraries.
735+
736+ This involves copying all contents from the temporary output directory.
737+
738+ Args:
739+ tmp_dir (str): The temporary directory where protoc/GAPIC generator output was placed.
740+ staging_dir (str): The final destination for the staged code.
741+ """
742+ # For GAPIC, the generator output is flat in `tmp_dir` and includes all
743+ # necessary files like setup.py, client library, etc.
744+ shutil .copytree (tmp_dir , staging_dir )
745+
746+
648747def _generate_api (
649748 api_path : str , library_id : str , source : str , output : str , gapic_version : str
650749):
@@ -660,18 +759,34 @@ def _generate_api(
660759 in a format which follows PEP-440.
661760 """
662761 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- )
762+ is_proto_only_library = len (py_gapic_config ) == 0
666763
667764 with tempfile .TemporaryDirectory () as tmp_dir :
668- generator_command = _determine_generator_command (
669- api_path , tmp_dir , generator_options
765+ # 1. Determine the command for code generation
766+ if is_proto_only_library :
767+ command = _construct_protoc_command (api_path , tmp_dir )
768+ else :
769+ generator_options = _get_api_generator_options (
770+ api_path , py_gapic_config , gapic_version = gapic_version
771+ )
772+ command = _determine_generator_command (api_path , tmp_dir , generator_options )
773+
774+ # 2. Execute the code generation command
775+ _run_protoc_command (command , source )
776+
777+ # 3. Determine staging location
778+ staging_child_directory = _get_staging_child_directory (
779+ api_path , is_proto_only_library
780+ )
781+ staging_dir = os .path .join (
782+ output , "owl-bot-staging" , library_id , staging_child_directory
670783 )
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 )
784+
785+ # 4. Stage the generated code
786+ if is_proto_only_library :
787+ _stage_proto_only_library (api_path , source , tmp_dir , staging_dir )
788+ else :
789+ _stage_gapic_library (tmp_dir , staging_dir )
675790
676791
677792def _run_nox_sessions (library_id : str , repo : str ):
0 commit comments