Skip to content

Commit 4d369a2

Browse files
committed
draft proof of concept microgenerator 2
1 parent 89d44f4 commit 4d369a2

File tree

3 files changed

+229
-0
lines changed

3 files changed

+229
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# TODO: add header if needed.
2+
3+
include_class_name_patterns:
4+
- Client
5+
6+
exclude_class_name_patterns: []
7+
8+
include_method_name_patterns:
9+
- batch_delete_
10+
- cancel_
11+
- create_
12+
- delete_
13+
- get_
14+
- insert_
15+
- list_
16+
- patch_
17+
- undelete_
18+
- update_
19+
20+
exclude_method_name_patterns:
21+
- get_mtls_endpoint_and_cert_source
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import ast
2+
import os
3+
from collections import defaultdict
4+
5+
import jinja2
6+
7+
from config_helper import (
8+
CLASSES_TO_INCLUDE,
9+
# CLASSES_TO_EXCLUDE, # Not currently being used.
10+
METHODS_TO_INCLUDE,
11+
METHODS_TO_EXCLUDE,
12+
)
13+
14+
# Constants
15+
BASE_DIR = "google/cloud/bigquery_v2/services"
16+
FILES_TO_PARSE = [
17+
os.path.join(root, file)
18+
for root, _, files in os.walk(BASE_DIR)
19+
for file in files
20+
if file.endswith(".py")
21+
]
22+
23+
24+
def create_tree(file_path):
25+
with open(file_path, "r") as source:
26+
tree = ast.parse(source.read())
27+
return tree
28+
29+
30+
def _extract_classes(tree):
31+
"""Extracts class nodes from an AST."""
32+
classes = []
33+
34+
for node in ast.walk(tree):
35+
if isinstance(node, ast.ClassDef) and node.name.endswith(
36+
*CLASSES_TO_INCLUDE
37+
): # TODO: currently this is one class. Refactor if necessary
38+
classes.append(node)
39+
return classes
40+
41+
42+
def _extract_methods(class_node):
43+
"""Extracts method nodes from a class node."""
44+
return (m for m in class_node.body if isinstance(m, ast.FunctionDef))
45+
46+
47+
def _process_method(method, class_name, parsed_data):
48+
"""Processes a single method and updates parsed_data."""
49+
method_name = method.name
50+
if any(method_name.startswith(prefix) for prefix in METHODS_TO_INCLUDE) and not any(
51+
method_name.startswith(prefix) for prefix in METHODS_TO_EXCLUDE
52+
):
53+
parameters = [arg.arg for arg in method.args.args + method.args.kwonlyargs]
54+
parsed_data[class_name][method_name] = parameters
55+
56+
57+
def parse_files(file_paths):
58+
"""
59+
Parse a list of Python files and extract information about classes,
60+
methods, and parameters.
61+
62+
Args:
63+
file_paths (list): List of file paths to parse.
64+
65+
Returns:
66+
Defaultdict with zero or more entries.
67+
"""
68+
69+
parsed_data = defaultdict(dict)
70+
71+
for file_path in file_paths:
72+
tree = create_tree(file_path)
73+
74+
for class_ in _extract_classes(tree):
75+
class_name = class_.name
76+
parsed_data[class_name]
77+
78+
for method in _extract_methods(class_):
79+
_process_method(method, class_name, parsed_data)
80+
81+
return parsed_data
82+
83+
84+
def _format_args(method_args):
85+
"""Formats method arguments for use in creating a method definition
86+
and a method call.
87+
"""
88+
args_for_def = ", ".join(method_args)
89+
args_for_call = ", ".join([f"{arg}={arg}" for arg in method_args if arg != "self"])
90+
return args_for_def, args_for_call
91+
92+
93+
def _format_class_name(method_name, suffix="Request"):
94+
"""Formats a class name from a method name.
95+
96+
Example:
97+
list_datasets -> ListDatasetsRequest
98+
"""
99+
return "".join(word.capitalize() for word in method_name.split("_")) + suffix
100+
101+
102+
def generate_client_class_source(data):
103+
"""
104+
Generates the BigQueryClient source code using a Jinja2 template.
105+
106+
Args:
107+
data: A dictionary where keys are *ServiceClient class names and
108+
values are dictionaries of methods for that client.
109+
110+
Returns:
111+
A string containing the complete, formatted Python source code
112+
for the BigQueryClient class.
113+
"""
114+
115+
# TODO: move template strings to a separate file.
116+
class_template_string = """\
117+
class BigQueryClient:
118+
def __init__(self):
119+
self._clients = {}
120+
121+
{% for method in methods %}
122+
def {{ method.name }}({{ method.args_for_def }}):
123+
\"\"\"A generated method to call the BigQuery API.\"\"\"
124+
125+
if "{{ method.class_name }}" not in self._clients:
126+
from google.cloud.bigquery_v2 import {{ method.class_name }}
127+
self._clients["{{ method.class_name }}"] = {{ method.class_name }}()
128+
129+
client = self._clients["{{ method.class_name }}"]
130+
from google.cloud.bigquery_v2 import types
131+
request = types.{{ method.request_class_name }}({{ method.args_for_call }})
132+
return client.{{ method.name }}(request=request)
133+
134+
{% endfor %}
135+
"""
136+
137+
# Prepare the context for the template.
138+
# We transform the input data into a flat list of methods
139+
methods_context = []
140+
for class_name, methods in data.items():
141+
for method_name, method_args in methods.items():
142+
args_for_def, args_for_call = _format_args(method_args)
143+
request_class_name = _format_class_name(method_name)
144+
methods_context.append(
145+
{
146+
"name": method_name,
147+
"class_name": class_name,
148+
"args_for_def": args_for_def,
149+
"args_for_call": args_for_call,
150+
"request_class_name": request_class_name,
151+
}
152+
)
153+
154+
# Create a Jinja2 Template object and render it with the context.
155+
template = jinja2.Template(class_template_string, trim_blocks=True)
156+
generated_code = template.render(methods=methods_context)
157+
158+
return generated_code
159+
160+
161+
if __name__ == "__main__":
162+
data = parse_files(FILES_TO_PARSE)
163+
164+
final_code = generate_client_class_source(data)
165+
# TODO: write final code to file.
166+
167+
print(final_code)
168+
169+
# Ensure black gets called on the generated source files as a final step.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# config_helper.py
2+
3+
import yaml
4+
import os
5+
6+
7+
def load_config_yaml(filepath):
8+
"""Loads configuration from a YAML file."""
9+
try:
10+
with open(filepath, "r") as f:
11+
config = yaml.safe_load(f)
12+
return config
13+
except FileNotFoundError:
14+
print(f"Error: Configuration file '{filepath}' not found.")
15+
return None
16+
except yaml.YAMLError as e:
17+
print(f"Error: Could not load YAML from '{filepath}': {e}")
18+
return None
19+
20+
21+
# Determine the absolute path to the config file relative to this file.
22+
# This makes the path robust to where the script is run from.
23+
_CONFIG_FILE_PATH = os.path.join(
24+
os.path.dirname(__file__), "bigqueryclient_config.yaml"
25+
)
26+
27+
config_data = load_config_yaml(_CONFIG_FILE_PATH)
28+
29+
if config_data:
30+
CLASSES_TO_INCLUDE = config_data.get("include_class_name_patterns", [])
31+
CLASSES_TO_EXCLUDE = config_data.get("exclude_class_name_patterns", [])
32+
METHODS_TO_INCLUDE = config_data.get("include_method_name_patterns", [])
33+
METHODS_TO_EXCLUDE = config_data.get("exclude_method_name_patterns", [])
34+
else:
35+
# Define default empty values if the config fails to load
36+
CLASSES_TO_INCLUDE = []
37+
CLASSES_TO_EXCLUDE = []
38+
METHODS_TO_INCLUDE = []
39+
METHODS_TO_EXCLUDE = []

0 commit comments

Comments
 (0)