Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
__pycache__/
*.pyc
*.json

output/
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ The script requires a `--language` argument to specify the target language (`py`

```bash
# For Python
./extract.sh --language py --input-repo /path/to/adk-python output.json
./extract.sh --language py --input-repo /path/to/adk-python output_dir

# For TypeScript
./extract.sh --language ts --input-repo /path/to/adk-js output.json
./extract.sh --language ts --input-repo /path/to/adk-js output_dir
```

### CLI Arguments
Expand All @@ -52,18 +52,18 @@ The script requires a `--language` argument to specify the target language (`py`
| `--input-file <path>` | Path to a single file to process. |
| `--input-dir <path>` | Path to a directory containing files. |
| `--input-repo <path>` | Path to the root of an ADK repository. Recursive search in `src` (Python) or `core/src` (TS). |
| `output` | **Required.** Path to the output JSON file. |
| `output` | **Required.** Path to the output directory. |

**Examples:**

```bash
# Process a single file
./extract.sh --language python --input-file src/my_agent.py output.json
./extract.sh --language python --input-file src/my_agent.py output_dir

# Process a directory
python3 -m google.adk.scope.extractors.python.extractor \
--input-dir src/google/adk \
output.json
output_dir
```

## Development
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies = [
"ruff",
"pytest",
"pytest-cov",
"pyyaml",
]


Expand Down
71 changes: 59 additions & 12 deletions src/google/adk/scope/extractors/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
import sys
from pathlib import Path

from google.protobuf.json_format import MessageToJson
import datetime
import yaml
from google.protobuf import text_format
from google.protobuf.json_format import MessageToJson, MessageToDict
from google.adk.scope.features_pb2 import FeatureRegistry
from google.adk.scope.utils.args import parse_args
from google.adk.scope.extractors import extractor_py, extractor_ts
Expand All @@ -12,6 +15,12 @@
)
logger = logging.getLogger(__name__)

_JSON_INDENT = 2
_JSON_OUTPUT = True
_YAML_OUTPUT = True
_PROTO_OUTPUT = True


EXTRACTORS = {
"python": extractor_py,
"typescript": extractor_ts,
Expand Down Expand Up @@ -161,19 +170,57 @@ def main():
features=all_features,
)

output_dir = args.output
try:
with open(args.output, "w") as f:
f.write(
MessageToJson(
registry,
indent=2,
preserving_proto_field_name=True,
always_print_fields_with_no_presence=True,
)
)
logger.info("Successfully wrote output to %s", args.output)
output_dir.mkdir(parents=True, exist_ok=True)
except IOError as e:
logger.error("Failed to write output: %s", e)
logger.error("Failed to create output directory %s: %s", output_dir, e)
sys.exit(1)

timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
prefix = "py" if args.language in {"python", "py"} else "ts"
base_filename = f"{prefix}_{timestamp}"

if _JSON_OUTPUT:
# 1. JSON Output
json_path = output_dir / f"{base_filename}.json"
try:
json_content = MessageToJson(
registry,
indent=_JSON_INDENT,
preserving_proto_field_name=True,
always_print_fields_with_no_presence=True,
)
json_path.write_text(json_content)
logger.info("Generated JSON: %s", json_path)
except IOError as e:
logger.error("Failed to write JSON output: %s", e)

if _YAML_OUTPUT:
# 2. YAML Output
yaml_path = output_dir / f"{base_filename}.yaml"
try:
dict_content = MessageToDict(
registry,
preserving_proto_field_name=True,
always_print_fields_with_no_presence=True,
use_integers_for_enums=False,
)
with open(yaml_path, "w") as f:
yaml.dump(dict_content, f, sort_keys=False)
logger.info("Generated YAML: %s", yaml_path)
except IOError as e:
logger.error("Failed to write YAML output: %s", e)

if _PROTO_OUTPUT:
# 3. TextProto Output
txtpb_path = output_dir / f"{base_filename}.txtpb"
try:
txtpb_content = text_format.MessageToString(registry)
txtpb_path.write_text(txtpb_content)
logger.info("Generated TextProto: %s", txtpb_path)
except IOError as e:
logger.error("Failed to write TextProto output: %s", e)


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion src/google/adk/scope/utils/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument(
"output",
type=Path,
help="Path to the output file",
help="Directory path for the output files",
)

parser.add_argument(
Expand Down
19 changes: 14 additions & 5 deletions test/adk/scope/extractors/test_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class TestExtractMain(unittest.TestCase):
def setUp(self):
self.test_dir = tempfile.mkdtemp()
self.root = Path(self.test_dir)
self.output_file = self.root / "output.json"
self.output_dir = self.root / "output"

# Patches
self.mock_args_patcher = patch(
Expand Down Expand Up @@ -107,7 +107,7 @@ def configure_args(
mock_args.input_file = Path(input_file) if input_file else None
mock_args.input_dir = Path(input_dir) if input_dir else None
mock_args.input_repo = Path(input_repo) if input_repo else None
mock_args.output = output or str(self.output_file)
mock_args.output = Path(output) if output else self.output_dir
self.mock_args.return_value = mock_args

def test_unsupported_language(self):
Expand All @@ -133,7 +133,10 @@ def test_input_file_mode(self):
extract.main()

self.mock_py_extractor.extract_features.assert_called()
self.assertTrue(self.output_file.exists())
self.assertTrue(self.output_dir.exists())
self.assertEqual(len(list(self.output_dir.glob("py_*.json"))), 1)
self.assertEqual(len(list(self.output_dir.glob("py_*.yaml"))), 1)
self.assertEqual(len(list(self.output_dir.glob("py_*.txtpb"))), 1)

def test_input_file_not_found(self):
self.configure_args(lang="python", input_file="/non/existent.py")
Expand All @@ -153,7 +156,10 @@ def test_input_dir_mode(self):
extract.main()

self.mock_py_extractor.find_files.assert_called_with(d, recursive=False)
self.assertTrue(self.output_file.exists())
self.assertTrue(self.output_dir.exists())
self.assertEqual(len(list(self.output_dir.glob("py_*.json"))), 1)
self.assertEqual(len(list(self.output_dir.glob("py_*.yaml"))), 1)
self.assertEqual(len(list(self.output_dir.glob("py_*.txtpb"))), 1)

def test_input_repo_mode(self):
r = self.root
Expand All @@ -169,7 +175,10 @@ def test_input_repo_mode(self):
self.mock_py_extractor.find_files.assert_called_with(
r / "src", recursive=True
)
self.assertTrue(self.output_file.exists())
self.assertTrue(self.output_dir.exists())
self.assertEqual(len(list(self.output_dir.glob("py_*.json"))), 1)
self.assertEqual(len(list(self.output_dir.glob("py_*.yaml"))), 1)
self.assertEqual(len(list(self.output_dir.glob("py_*.txtpb"))), 1)


if __name__ == "__main__":
Expand Down
4 changes: 2 additions & 2 deletions test/adk/scope/utils/test_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def test_parse_args(self, mock_parse):
mock_args = argparse.Namespace(
language="py",
input_repo=Path("/tmp/repo"),
output=Path("/tmp/out.json"),
output=Path("/tmp/out_dir"),
input_file=None,
input_dir=None,
)
Expand All @@ -24,7 +24,7 @@ def test_parse_args(self, mock_parse):
args = parse_args()

self.assertEqual(args.input_repo, Path("/tmp/repo"))
self.assertEqual(args.output, Path("/tmp/out.json"))
self.assertEqual(args.output, Path("/tmp/out_dir"))
# Should be normalized
self.assertEqual(args.language, "python")

Expand Down