Skip to content

Commit 68e69f1

Browse files
committed
added back scripts
1 parent b718d1f commit 68e69f1

File tree

2 files changed

+424
-0
lines changed

2 files changed

+424
-0
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
#! /usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
# Copyright 2025 Google LLC
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
import argparse
18+
import os
19+
import libcst as cst
20+
import pathlib
21+
import sys
22+
from typing import (Any, Callable, Dict, List, Sequence, Tuple)
23+
24+
25+
def partition(
26+
predicate: Callable[[Any], bool],
27+
iterator: Sequence[Any]
28+
) -> Tuple[List[Any], List[Any]]:
29+
"""A stable, out-of-place partition."""
30+
results = ([], [])
31+
32+
for i in iterator:
33+
results[int(predicate(i))].append(i)
34+
35+
# Returns trueList, falseList
36+
return results[1], results[0]
37+
38+
39+
class bigtable_adminCallTransformer(cst.CSTTransformer):
40+
CTRL_PARAMS: Tuple[str] = ('retry', 'timeout', 'metadata')
41+
METHOD_TO_PARAMS: Dict[str, Tuple[str]] = {
42+
'check_consistency': ('name', 'consistency_token', 'standard_read_remote_writes', 'data_boost_read_local_writes', ),
43+
'copy_backup': ('parent', 'backup_id', 'source_backup', 'expire_time', ),
44+
'create_app_profile': ('parent', 'app_profile_id', 'app_profile', 'ignore_warnings', ),
45+
'create_authorized_view': ('parent', 'authorized_view_id', 'authorized_view', ),
46+
'create_backup': ('parent', 'backup_id', 'backup', ),
47+
'create_cluster': ('parent', 'cluster_id', 'cluster', ),
48+
'create_instance': ('parent', 'instance_id', 'instance', 'clusters', ),
49+
'create_logical_view': ('parent', 'logical_view_id', 'logical_view', ),
50+
'create_materialized_view': ('parent', 'materialized_view_id', 'materialized_view', ),
51+
'create_schema_bundle': ('parent', 'schema_bundle_id', 'schema_bundle', ),
52+
'create_table': ('parent', 'table_id', 'table', 'initial_splits', ),
53+
'create_table_from_snapshot': ('parent', 'table_id', 'source_snapshot', ),
54+
'delete_app_profile': ('name', 'ignore_warnings', ),
55+
'delete_authorized_view': ('name', 'etag', ),
56+
'delete_backup': ('name', ),
57+
'delete_cluster': ('name', ),
58+
'delete_instance': ('name', ),
59+
'delete_logical_view': ('name', 'etag', ),
60+
'delete_materialized_view': ('name', 'etag', ),
61+
'delete_schema_bundle': ('name', 'etag', ),
62+
'delete_snapshot': ('name', ),
63+
'delete_table': ('name', ),
64+
'drop_row_range': ('name', 'row_key_prefix', 'delete_all_data_from_table', ),
65+
'generate_consistency_token': ('name', ),
66+
'get_app_profile': ('name', ),
67+
'get_authorized_view': ('name', 'view', ),
68+
'get_backup': ('name', ),
69+
'get_cluster': ('name', ),
70+
'get_iam_policy': ('resource', 'options', ),
71+
'get_instance': ('name', ),
72+
'get_logical_view': ('name', ),
73+
'get_materialized_view': ('name', ),
74+
'get_schema_bundle': ('name', ),
75+
'get_snapshot': ('name', ),
76+
'get_table': ('name', 'view', ),
77+
'list_app_profiles': ('parent', 'page_size', 'page_token', ),
78+
'list_authorized_views': ('parent', 'page_size', 'page_token', 'view', ),
79+
'list_backups': ('parent', 'filter', 'order_by', 'page_size', 'page_token', ),
80+
'list_clusters': ('parent', 'page_token', ),
81+
'list_hot_tablets': ('parent', 'start_time', 'end_time', 'page_size', 'page_token', ),
82+
'list_instances': ('parent', 'page_token', ),
83+
'list_logical_views': ('parent', 'page_size', 'page_token', ),
84+
'list_materialized_views': ('parent', 'page_size', 'page_token', ),
85+
'list_schema_bundles': ('parent', 'page_size', 'page_token', ),
86+
'list_snapshots': ('parent', 'page_size', 'page_token', ),
87+
'list_tables': ('parent', 'view', 'page_size', 'page_token', ),
88+
'modify_column_families': ('name', 'modifications', 'ignore_warnings', ),
89+
'partial_update_cluster': ('cluster', 'update_mask', ),
90+
'partial_update_instance': ('instance', 'update_mask', ),
91+
'restore_table': ('parent', 'table_id', 'backup', ),
92+
'set_iam_policy': ('resource', 'policy', 'update_mask', ),
93+
'snapshot_table': ('name', 'cluster', 'snapshot_id', 'ttl', 'description', ),
94+
'test_iam_permissions': ('resource', 'permissions', ),
95+
'undelete_table': ('name', ),
96+
'update_app_profile': ('app_profile', 'update_mask', 'ignore_warnings', ),
97+
'update_authorized_view': ('authorized_view', 'update_mask', 'ignore_warnings', ),
98+
'update_backup': ('backup', 'update_mask', ),
99+
'update_cluster': ('name', 'location', 'state', 'serve_nodes', 'node_scaling_factor', 'cluster_config', 'default_storage_type', 'encryption_config', ),
100+
'update_instance': ('display_name', 'name', 'state', 'type_', 'labels', 'create_time', 'satisfies_pzs', 'satisfies_pzi', 'tags', ),
101+
'update_logical_view': ('logical_view', 'update_mask', ),
102+
'update_materialized_view': ('materialized_view', 'update_mask', ),
103+
'update_schema_bundle': ('schema_bundle', 'update_mask', 'ignore_warnings', ),
104+
'update_table': ('table', 'update_mask', 'ignore_warnings', ),
105+
}
106+
107+
def leave_Call(self, original: cst.Call, updated: cst.Call) -> cst.CSTNode:
108+
try:
109+
key = original.func.attr.value
110+
kword_params = self.METHOD_TO_PARAMS[key]
111+
except (AttributeError, KeyError):
112+
# Either not a method from the API or too convoluted to be sure.
113+
return updated
114+
115+
# If the existing code is valid, keyword args come after positional args.
116+
# Therefore, all positional args must map to the first parameters.
117+
args, kwargs = partition(lambda a: not bool(a.keyword), updated.args)
118+
if any(k.keyword.value == "request" for k in kwargs):
119+
# We've already fixed this file, don't fix it again.
120+
return updated
121+
122+
kwargs, ctrl_kwargs = partition(
123+
lambda a: a.keyword.value not in self.CTRL_PARAMS,
124+
kwargs
125+
)
126+
127+
args, ctrl_args = args[:len(kword_params)], args[len(kword_params):]
128+
ctrl_kwargs.extend(cst.Arg(value=a.value, keyword=cst.Name(value=ctrl))
129+
for a, ctrl in zip(ctrl_args, self.CTRL_PARAMS))
130+
131+
request_arg = cst.Arg(
132+
value=cst.Dict([
133+
cst.DictElement(
134+
cst.SimpleString("'{}'".format(name)),
135+
cst.Element(value=arg.value)
136+
)
137+
# Note: the args + kwargs looks silly, but keep in mind that
138+
# the control parameters had to be stripped out, and that
139+
# those could have been passed positionally or by keyword.
140+
for name, arg in zip(kword_params, args + kwargs)]),
141+
keyword=cst.Name("request")
142+
)
143+
144+
return updated.with_changes(
145+
args=[request_arg] + ctrl_kwargs
146+
)
147+
148+
149+
def fix_files(
150+
in_dir: pathlib.Path,
151+
out_dir: pathlib.Path,
152+
*,
153+
transformer=bigtable_adminCallTransformer(),
154+
):
155+
"""Duplicate the input dir to the output dir, fixing file method calls.
156+
157+
Preconditions:
158+
* in_dir is a real directory
159+
* out_dir is a real, empty directory
160+
"""
161+
pyfile_gen = (
162+
pathlib.Path(os.path.join(root, f))
163+
for root, _, files in os.walk(in_dir)
164+
for f in files if os.path.splitext(f)[1] == ".py"
165+
)
166+
167+
for fpath in pyfile_gen:
168+
with open(fpath, 'r') as f:
169+
src = f.read()
170+
171+
# Parse the code and insert method call fixes.
172+
tree = cst.parse_module(src)
173+
updated = tree.visit(transformer)
174+
175+
# Create the path and directory structure for the new file.
176+
updated_path = out_dir.joinpath(fpath.relative_to(in_dir))
177+
updated_path.parent.mkdir(parents=True, exist_ok=True)
178+
179+
# Generate the updated source file at the corresponding path.
180+
with open(updated_path, 'w') as f:
181+
f.write(updated.code)
182+
183+
184+
if __name__ == '__main__':
185+
parser = argparse.ArgumentParser(
186+
description="""Fix up source that uses the bigtable_admin client library.
187+
188+
The existing sources are NOT overwritten but are copied to output_dir with changes made.
189+
190+
Note: This tool operates at a best-effort level at converting positional
191+
parameters in client method calls to keyword based parameters.
192+
Cases where it WILL FAIL include
193+
A) * or ** expansion in a method call.
194+
B) Calls via function or method alias (includes free function calls)
195+
C) Indirect or dispatched calls (e.g. the method is looked up dynamically)
196+
197+
These all constitute false negatives. The tool will also detect false
198+
positives when an API method shares a name with another method.
199+
""")
200+
parser.add_argument(
201+
'-d',
202+
'--input-directory',
203+
required=True,
204+
dest='input_dir',
205+
help='the input directory to walk for python files to fix up',
206+
)
207+
parser.add_argument(
208+
'-o',
209+
'--output-directory',
210+
required=True,
211+
dest='output_dir',
212+
help='the directory to output files fixed via un-flattening',
213+
)
214+
args = parser.parse_args()
215+
input_dir = pathlib.Path(args.input_dir)
216+
output_dir = pathlib.Path(args.output_dir)
217+
if not input_dir.is_dir():
218+
print(
219+
f"input directory '{input_dir}' does not exist or is not a directory",
220+
file=sys.stderr,
221+
)
222+
sys.exit(-1)
223+
224+
if not output_dir.is_dir():
225+
print(
226+
f"output directory '{output_dir}' does not exist or is not a directory",
227+
file=sys.stderr,
228+
)
229+
sys.exit(-1)
230+
231+
if os.listdir(output_dir):
232+
print(
233+
f"output directory '{output_dir}' is not empty",
234+
file=sys.stderr,
235+
)
236+
sys.exit(-1)
237+
238+
fix_files(input_dir, output_dir)

0 commit comments

Comments
 (0)