xref: /aosp_15_r20/external/google-cloud-java/generation/new_client/new-client.py (revision 55e87721aa1bc457b326496a7ca40f3ea1a63287)
1*55e87721SMatt Gilbride# Copyright 2019 Google LLC
2*55e87721SMatt Gilbride#
3*55e87721SMatt Gilbride# Licensed under the Apache License, Version 2.0 (the "License");
4*55e87721SMatt Gilbride# you may not use this file except in compliance with the License.
5*55e87721SMatt Gilbride# You may obtain a copy of the License at
6*55e87721SMatt Gilbride#
7*55e87721SMatt Gilbride#     https://www.apache.org/licenses/LICENSE-2.0
8*55e87721SMatt Gilbride#
9*55e87721SMatt Gilbride# Unless required by applicable law or agreed to in writing, software
10*55e87721SMatt Gilbride# distributed under the License is distributed on an "AS IS" BASIS,
11*55e87721SMatt Gilbride# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*55e87721SMatt Gilbride# See the License for the specific language governing permissions and
13*55e87721SMatt Gilbride# limitations under the License.
14*55e87721SMatt Gilbride
15*55e87721SMatt Gilbrideimport json
16*55e87721SMatt Gilbrideimport os
17*55e87721SMatt Gilbridefrom pathlib import Path
18*55e87721SMatt Gilbrideimport re
19*55e87721SMatt Gilbrideimport subprocess
20*55e87721SMatt Gilbrideimport sys
21*55e87721SMatt Gilbride
22*55e87721SMatt Gilbrideimport click
23*55e87721SMatt Gilbrideimport templates
24*55e87721SMatt Gilbride
25*55e87721SMatt Gilbride
26*55e87721SMatt Gilbride@click.group(invoke_without_command=False)
27*55e87721SMatt Gilbride@click.pass_context
28*55e87721SMatt Gilbride@click.version_option(message="%(version)s")
29*55e87721SMatt Gilbridedef main(ctx):
30*55e87721SMatt Gilbride    pass
31*55e87721SMatt Gilbride
32*55e87721SMatt Gilbride@main.command()
33*55e87721SMatt Gilbride@click.option(
34*55e87721SMatt Gilbride    "--api_shortname",
35*55e87721SMatt Gilbride    required=True,
36*55e87721SMatt Gilbride    type=str,
37*55e87721SMatt Gilbride    prompt="Service name? (e.g. automl)",
38*55e87721SMatt Gilbride    help="Name for the new directory name and (default) artifact name"
39*55e87721SMatt Gilbride)
40*55e87721SMatt Gilbride@click.option(
41*55e87721SMatt Gilbride    "--name-pretty",
42*55e87721SMatt Gilbride    required=True,
43*55e87721SMatt Gilbride    type=str,
44*55e87721SMatt Gilbride    prompt="Pretty name? (e.g. 'Cloud AutoML')",
45*55e87721SMatt Gilbride    help="The human-friendly name that appears in README.md"
46*55e87721SMatt Gilbride)
47*55e87721SMatt Gilbride@click.option(
48*55e87721SMatt Gilbride    "--product-docs",
49*55e87721SMatt Gilbride    required=True,
50*55e87721SMatt Gilbride    type=str,
51*55e87721SMatt Gilbride    prompt="Product Documentation URL",
52*55e87721SMatt Gilbride    help="Documentation URL that appears in README.md"
53*55e87721SMatt Gilbride)
54*55e87721SMatt Gilbride@click.option(
55*55e87721SMatt Gilbride    "--api-description",
56*55e87721SMatt Gilbride    required=True,
57*55e87721SMatt Gilbride    type=str,
58*55e87721SMatt Gilbride    prompt="Description for README. The first sentence is prefixed by the "
59*55e87721SMatt Gilbride           "pretty name",
60*55e87721SMatt Gilbride    help="Description that appears in README.md"
61*55e87721SMatt Gilbride)
62*55e87721SMatt Gilbride@click.option(
63*55e87721SMatt Gilbride    "--release-level",
64*55e87721SMatt Gilbride    type=click.Choice(["stable", "preview"]),
65*55e87721SMatt Gilbride    default="preview",
66*55e87721SMatt Gilbride    show_default=True,
67*55e87721SMatt Gilbride    help="A label that appears in repo-metadata.json. The first library "
68*55e87721SMatt Gilbride         "generation is always 'preview'."
69*55e87721SMatt Gilbride)
70*55e87721SMatt Gilbride@click.option(
71*55e87721SMatt Gilbride    "--transport",
72*55e87721SMatt Gilbride    type=click.Choice(["grpc", "http", "both"]),
73*55e87721SMatt Gilbride    default="grpc",
74*55e87721SMatt Gilbride    show_default=True,
75*55e87721SMatt Gilbride    help="A label that appears in repo-metadata.json"
76*55e87721SMatt Gilbride)
77*55e87721SMatt Gilbride@click.option("--language", type=str, default="java", show_default=True)
78*55e87721SMatt Gilbride@click.option(
79*55e87721SMatt Gilbride    "--distribution-name",
80*55e87721SMatt Gilbride    type=str,
81*55e87721SMatt Gilbride    help="Maven coordinates of the generated library. By default it's "
82*55e87721SMatt Gilbride         "com.google.cloud:google-cloud-<api_shortname>"
83*55e87721SMatt Gilbride)
84*55e87721SMatt Gilbride@click.option(
85*55e87721SMatt Gilbride    "--api-id",
86*55e87721SMatt Gilbride    type=str,
87*55e87721SMatt Gilbride    help="The value of the apiid parameter used in README.md It has link to "
88*55e87721SMatt Gilbride         "https://console.cloud.google.com/flows/enableapi?apiid=<api_id>"
89*55e87721SMatt Gilbride)
90*55e87721SMatt Gilbride@click.option(
91*55e87721SMatt Gilbride    "--requires-billing",
92*55e87721SMatt Gilbride    type=bool,
93*55e87721SMatt Gilbride    default=True,
94*55e87721SMatt Gilbride    show_default=True,
95*55e87721SMatt Gilbride    help="Based on this value, README.md explains whether billing setup is "
96*55e87721SMatt Gilbride         "needed or not."
97*55e87721SMatt Gilbride)
98*55e87721SMatt Gilbride@click.option(
99*55e87721SMatt Gilbride    "--destination-name",
100*55e87721SMatt Gilbride    type=str,
101*55e87721SMatt Gilbride    default=None,
102*55e87721SMatt Gilbride    help="The directory name of the new library. By default it's "
103*55e87721SMatt Gilbride         "java-<api_shortname>"
104*55e87721SMatt Gilbride)
105*55e87721SMatt Gilbride@click.option(
106*55e87721SMatt Gilbride    "--proto-path",
107*55e87721SMatt Gilbride    required=True,
108*55e87721SMatt Gilbride    type=str,
109*55e87721SMatt Gilbride    default=None,
110*55e87721SMatt Gilbride    help="Path to proto file from the root of the googleapis repository to the"
111*55e87721SMatt Gilbride         "directory that contains the proto files (without the version)."
112*55e87721SMatt Gilbride         "For example, to generate the library for 'google/maps/routing/v2', "
113*55e87721SMatt Gilbride         "then you specify this value as 'google/maps/routing'"
114*55e87721SMatt Gilbride)
115*55e87721SMatt Gilbride@click.option(
116*55e87721SMatt Gilbride    "--cloud-api",
117*55e87721SMatt Gilbride    type=bool,
118*55e87721SMatt Gilbride    default=True,
119*55e87721SMatt Gilbride    show_default=True,
120*55e87721SMatt Gilbride    help="If true, the artifact ID of the library is 'google-cloud-'; "
121*55e87721SMatt Gilbride         "otherwise 'google-'"
122*55e87721SMatt Gilbride)
123*55e87721SMatt Gilbride@click.option(
124*55e87721SMatt Gilbride    "--group-id",
125*55e87721SMatt Gilbride    type=str,
126*55e87721SMatt Gilbride    default="com.google.cloud",
127*55e87721SMatt Gilbride    show_default=True,
128*55e87721SMatt Gilbride    help="The group ID of the artifact when distribution name is not set"
129*55e87721SMatt Gilbride)
130*55e87721SMatt Gilbride@click.option(
131*55e87721SMatt Gilbride    "--owlbot-image",
132*55e87721SMatt Gilbride    type=str,
133*55e87721SMatt Gilbride    default="gcr.io/cloud-devrel-public-resources/owlbot-java",
134*55e87721SMatt Gilbride    show_default=True,
135*55e87721SMatt Gilbride    help="The owlbot container image used in OwlBot.yaml"
136*55e87721SMatt Gilbride)
137*55e87721SMatt Gilbride@click.option(
138*55e87721SMatt Gilbride    "--library-type",
139*55e87721SMatt Gilbride    type=str,
140*55e87721SMatt Gilbride    default="GAPIC_AUTO",
141*55e87721SMatt Gilbride    show_default=True,
142*55e87721SMatt Gilbride    help="A label that appear in repo-metadata.json to tell how the library is "
143*55e87721SMatt Gilbride         "maintained or generated"
144*55e87721SMatt Gilbride)
145*55e87721SMatt Gilbride@click.option(
146*55e87721SMatt Gilbride    "--googleapis-gen-url",
147*55e87721SMatt Gilbride    type=str,
148*55e87721SMatt Gilbride    default="https://github.com/googleapis/googleapis-gen.git",
149*55e87721SMatt Gilbride    show_default=True,
150*55e87721SMatt Gilbride    help="The URL of the repository that has generated Java code from proto "
151*55e87721SMatt Gilbride         "service definition"
152*55e87721SMatt Gilbride)
153*55e87721SMatt Gilbridedef generate(
154*55e87721SMatt Gilbride    api_shortname,
155*55e87721SMatt Gilbride    name_pretty,
156*55e87721SMatt Gilbride    product_docs,
157*55e87721SMatt Gilbride    api_description,
158*55e87721SMatt Gilbride    release_level,
159*55e87721SMatt Gilbride    distribution_name,
160*55e87721SMatt Gilbride    api_id,
161*55e87721SMatt Gilbride    requires_billing,
162*55e87721SMatt Gilbride    transport,
163*55e87721SMatt Gilbride    language,
164*55e87721SMatt Gilbride    destination_name,
165*55e87721SMatt Gilbride    proto_path,
166*55e87721SMatt Gilbride    cloud_api,
167*55e87721SMatt Gilbride    group_id,
168*55e87721SMatt Gilbride    owlbot_image,
169*55e87721SMatt Gilbride    library_type,
170*55e87721SMatt Gilbride    googleapis_gen_url,
171*55e87721SMatt Gilbride):
172*55e87721SMatt Gilbride    cloud_prefix = "cloud-" if cloud_api else ""
173*55e87721SMatt Gilbride
174*55e87721SMatt Gilbride    output_name = destination_name if destination_name else api_shortname
175*55e87721SMatt Gilbride    if distribution_name is None:
176*55e87721SMatt Gilbride        distribution_name = f"{group_id}:google-{cloud_prefix}{output_name}"
177*55e87721SMatt Gilbride
178*55e87721SMatt Gilbride    distribution_name_short = re.split(r"[:\/]", distribution_name)[-1]
179*55e87721SMatt Gilbride
180*55e87721SMatt Gilbride    if api_id is None:
181*55e87721SMatt Gilbride        api_id = f"{api_shortname}.googleapis.com"
182*55e87721SMatt Gilbride
183*55e87721SMatt Gilbride    if not product_docs.startswith("https"):
184*55e87721SMatt Gilbride        sys.exit("product_docs must starts with 'https://'")
185*55e87721SMatt Gilbride
186*55e87721SMatt Gilbride    client_documentation = (
187*55e87721SMatt Gilbride        f"https://cloud.google.com/{language}/docs/reference/{distribution_name_short}/latest/overview"
188*55e87721SMatt Gilbride    )
189*55e87721SMatt Gilbride
190*55e87721SMatt Gilbride    if proto_path is None:
191*55e87721SMatt Gilbride        proto_path = f"/google/cloud/{api_shortname}"
192*55e87721SMatt Gilbride
193*55e87721SMatt Gilbride    if api_shortname == "":
194*55e87721SMatt Gilbride        sys.exit("api_shortname is empty")
195*55e87721SMatt Gilbride
196*55e87721SMatt Gilbride    repo_metadata = {
197*55e87721SMatt Gilbride        "api_shortname": api_shortname,
198*55e87721SMatt Gilbride        "name_pretty": name_pretty,
199*55e87721SMatt Gilbride        "product_documentation": product_docs,
200*55e87721SMatt Gilbride        "api_description": api_description,
201*55e87721SMatt Gilbride        "client_documentation": client_documentation,
202*55e87721SMatt Gilbride        "release_level": release_level,
203*55e87721SMatt Gilbride        "transport": transport,
204*55e87721SMatt Gilbride        "language": language,
205*55e87721SMatt Gilbride        "repo": f"googleapis/{language}-{output_name}",
206*55e87721SMatt Gilbride        "repo_short": f"{language}-{output_name}",
207*55e87721SMatt Gilbride        "distribution_name": distribution_name,
208*55e87721SMatt Gilbride        "api_id": api_id,
209*55e87721SMatt Gilbride        "library_type": library_type,
210*55e87721SMatt Gilbride    }
211*55e87721SMatt Gilbride    if requires_billing:
212*55e87721SMatt Gilbride        repo_metadata["requires_billing"] = True
213*55e87721SMatt Gilbride
214*55e87721SMatt Gilbride    # Initialize workdir
215*55e87721SMatt Gilbride    workdir = Path(f"{sys.path[0]}/../../java-{output_name}").resolve()
216*55e87721SMatt Gilbride    if os.path.isdir(workdir):
217*55e87721SMatt Gilbride      sys.exit(
218*55e87721SMatt Gilbride          "Couldn't create the module because "
219*55e87721SMatt Gilbride          f"the module {workdir} already exists. In Java client library "
220*55e87721SMatt Gilbride          "generation, a new API version of an existing module does not "
221*55e87721SMatt Gilbride          "require new-client.py invocation. "
222*55e87721SMatt Gilbride          "See go/yoshi-java-new-client#adding-a-new-service-version-by-owlbot."
223*55e87721SMatt Gilbride      )
224*55e87721SMatt Gilbride    print(f"Creating a new module {workdir}")
225*55e87721SMatt Gilbride    os.makedirs(workdir, exist_ok=False)
226*55e87721SMatt Gilbride    # write .repo-metadata.json file
227*55e87721SMatt Gilbride    with open(workdir / ".repo-metadata.json", "w") as fp:
228*55e87721SMatt Gilbride        json.dump(repo_metadata, fp, indent=2)
229*55e87721SMatt Gilbride
230*55e87721SMatt Gilbride    # create owlbot.py
231*55e87721SMatt Gilbride    templates.render(
232*55e87721SMatt Gilbride        template_name="owlbot.py.j2",
233*55e87721SMatt Gilbride        output_name=str(workdir / "owlbot.py"),
234*55e87721SMatt Gilbride        should_include_templates=True,
235*55e87721SMatt Gilbride        template_excludes=[],
236*55e87721SMatt Gilbride    )
237*55e87721SMatt Gilbride
238*55e87721SMatt Gilbride    # In monorepo, .OwlBot.yaml needs to be in the directory of the module.
239*55e87721SMatt Gilbride    owlbot_yaml_location_from_module = ".OwlBot.yaml"
240*55e87721SMatt Gilbride    # create owlbot config
241*55e87721SMatt Gilbride    templates.render(
242*55e87721SMatt Gilbride        template_name="owlbot.yaml.monorepo.j2",
243*55e87721SMatt Gilbride        output_name=str(workdir / owlbot_yaml_location_from_module),
244*55e87721SMatt Gilbride        artifact_name=distribution_name_short,
245*55e87721SMatt Gilbride        proto_path=proto_path,
246*55e87721SMatt Gilbride        module_name=f"java-{output_name}",
247*55e87721SMatt Gilbride        api_shortname=api_shortname
248*55e87721SMatt Gilbride    )
249*55e87721SMatt Gilbride
250*55e87721SMatt Gilbride    # get the sha256 digets for the owlbot image
251*55e87721SMatt Gilbride    subprocess.check_call(["docker", "pull", "-q", owlbot_image])
252*55e87721SMatt Gilbride    owlbot_image_digest = (
253*55e87721SMatt Gilbride        subprocess.check_output(
254*55e87721SMatt Gilbride            ["docker", "inspect", "--format='{{index .RepoDigests 0}}", owlbot_image,],
255*55e87721SMatt Gilbride            encoding="utf-8",
256*55e87721SMatt Gilbride        )
257*55e87721SMatt Gilbride            .strip()
258*55e87721SMatt Gilbride            .split("@")[-1]
259*55e87721SMatt Gilbride    )
260*55e87721SMatt Gilbride
261*55e87721SMatt Gilbride    user = subprocess.check_output(["id", "-u"], encoding="utf8").strip()
262*55e87721SMatt Gilbride    group = subprocess.check_output(["id", "-g"], encoding="utf8").strip()
263*55e87721SMatt Gilbride
264*55e87721SMatt Gilbride    # run owlbot copy
265*55e87721SMatt Gilbride    print("Cloning googleapis-gen...")
266*55e87721SMatt Gilbride    subprocess.check_call(["git", "clone", "-q", googleapis_gen_url, "./gen/googleapis-gen"], cwd=workdir)
267*55e87721SMatt Gilbride    subprocess.check_call(["docker", "pull", "gcr.io/cloud-devrel-public-resources/owlbot-cli:latest"])
268*55e87721SMatt Gilbride    copy_code_parameters = [
269*55e87721SMatt Gilbride        "docker",
270*55e87721SMatt Gilbride        "run",
271*55e87721SMatt Gilbride        "--rm",
272*55e87721SMatt Gilbride        "--user",
273*55e87721SMatt Gilbride        f"{user}:{group}",
274*55e87721SMatt Gilbride        "-v",
275*55e87721SMatt Gilbride        f"{workdir}:/repo",
276*55e87721SMatt Gilbride        "-v",
277*55e87721SMatt Gilbride        ""f"{workdir}""/gen/googleapis-gen:/googleapis-gen",
278*55e87721SMatt Gilbride        "-w",
279*55e87721SMatt Gilbride        "/repo",
280*55e87721SMatt Gilbride        "--env", "HOME=/tmp",
281*55e87721SMatt Gilbride        "gcr.io/cloud-devrel-public-resources/owlbot-cli:latest",
282*55e87721SMatt Gilbride        "copy-code",
283*55e87721SMatt Gilbride        "--source-repo=/googleapis-gen",
284*55e87721SMatt Gilbride        f"--config-file={owlbot_yaml_location_from_module}"
285*55e87721SMatt Gilbride    ]
286*55e87721SMatt Gilbride    print("Running copy-code: " + str(copy_code_parameters))
287*55e87721SMatt Gilbride    print("  in directory: " + str(workdir))
288*55e87721SMatt Gilbride    subprocess.check_call(
289*55e87721SMatt Gilbride        copy_code_parameters,
290*55e87721SMatt Gilbride        cwd=workdir,
291*55e87721SMatt Gilbride    )
292*55e87721SMatt Gilbride
293*55e87721SMatt Gilbride    print("Removing googleapis-gen...")
294*55e87721SMatt Gilbride    subprocess.check_call(["rm", "-fr", "gen"], cwd=workdir)
295*55e87721SMatt Gilbride
296*55e87721SMatt Gilbride    # Bringing owl-bot-staging from the new module's directory to the root
297*55e87721SMatt Gilbride    # directory so that owlbot-java can process them.
298*55e87721SMatt Gilbride    subprocess.check_call(
299*55e87721SMatt Gilbride        [
300*55e87721SMatt Gilbride            "mv",
301*55e87721SMatt Gilbride            "owl-bot-staging",
302*55e87721SMatt Gilbride            "../"
303*55e87721SMatt Gilbride        ],
304*55e87721SMatt Gilbride        cwd=workdir,
305*55e87721SMatt Gilbride    )
306*55e87721SMatt Gilbride    monorepo_root=(workdir / '..').resolve()
307*55e87721SMatt Gilbride    print("monorepo_root=",monorepo_root)
308*55e87721SMatt Gilbride    print("Running the post-processor...")
309*55e87721SMatt Gilbride    subprocess.check_call(
310*55e87721SMatt Gilbride        [
311*55e87721SMatt Gilbride            "docker",
312*55e87721SMatt Gilbride            "run",
313*55e87721SMatt Gilbride            "--rm",
314*55e87721SMatt Gilbride            "-v",
315*55e87721SMatt Gilbride            f"{monorepo_root}:/workspace",
316*55e87721SMatt Gilbride            "--user",
317*55e87721SMatt Gilbride            f"{user}:{group}",
318*55e87721SMatt Gilbride            owlbot_image,
319*55e87721SMatt Gilbride        ],
320*55e87721SMatt Gilbride        cwd=monorepo_root,
321*55e87721SMatt Gilbride    )
322*55e87721SMatt Gilbride
323*55e87721SMatt Gilbride    # Remove irrelevant files from templates
324*55e87721SMatt Gilbride    subprocess.check_call(
325*55e87721SMatt Gilbride        ["bash", "generation/update_owlbot_postprocessor_config.sh"],
326*55e87721SMatt Gilbride        cwd=monorepo_root
327*55e87721SMatt Gilbride    )
328*55e87721SMatt Gilbride    subprocess.check_call(
329*55e87721SMatt Gilbride        ["bash", "generation/delete_non_generated_samples.sh"],
330*55e87721SMatt Gilbride        cwd=monorepo_root
331*55e87721SMatt Gilbride    )
332*55e87721SMatt Gilbride
333*55e87721SMatt Gilbride    print("Regenerating the BOM")
334*55e87721SMatt Gilbride    subprocess.check_call(
335*55e87721SMatt Gilbride        [
336*55e87721SMatt Gilbride            "bash", "generation/generate_gapic_bom.sh",
337*55e87721SMatt Gilbride        ],
338*55e87721SMatt Gilbride        cwd=monorepo_root,
339*55e87721SMatt Gilbride    )
340*55e87721SMatt Gilbride
341*55e87721SMatt Gilbride    print("Regenerating root pom.xml")
342*55e87721SMatt Gilbride
343*55e87721SMatt Gilbride    # This script takes care of updating the root pom.xml
344*55e87721SMatt Gilbride    os.system(f"cd {monorepo_root} && generation/generate_root_pom.sh")
345*55e87721SMatt Gilbride
346*55e87721SMatt Gilbride    print("Consolidating configurations")
347*55e87721SMatt Gilbride    subprocess.check_call(
348*55e87721SMatt Gilbride        [
349*55e87721SMatt Gilbride            "bash", "generation/consolidate_config.sh"
350*55e87721SMatt Gilbride        ],
351*55e87721SMatt Gilbride        cwd=monorepo_root,
352*55e87721SMatt Gilbride    )
353*55e87721SMatt Gilbride    print("Setting parent poms")
354*55e87721SMatt Gilbride    subprocess.check_call(
355*55e87721SMatt Gilbride        [
356*55e87721SMatt Gilbride            "bash", "generation/set_parent_pom.sh"
357*55e87721SMatt Gilbride        ],
358*55e87721SMatt Gilbride        cwd=monorepo_root,
359*55e87721SMatt Gilbride    )
360*55e87721SMatt Gilbride
361*55e87721SMatt Gilbride    print("Applying the versions")
362*55e87721SMatt Gilbride    subprocess.check_call(
363*55e87721SMatt Gilbride        [
364*55e87721SMatt Gilbride            "bash", "generation/apply_current_versions.sh"
365*55e87721SMatt Gilbride        ],
366*55e87721SMatt Gilbride        cwd=monorepo_root,
367*55e87721SMatt Gilbride    )
368*55e87721SMatt Gilbride
369*55e87721SMatt Gilbride    print("Adding annotations in readme")
370*55e87721SMatt Gilbride    subprocess.check_call(
371*55e87721SMatt Gilbride        [
372*55e87721SMatt Gilbride            "bash", "generation/readme_update.sh"
373*55e87721SMatt Gilbride        ],
374*55e87721SMatt Gilbride        cwd=monorepo_root,
375*55e87721SMatt Gilbride    )
376*55e87721SMatt Gilbride
377*55e87721SMatt Gilbride    print(f"Prepared new library in {workdir}")
378*55e87721SMatt Gilbride    print(f"Please create a pull request:\n"
379*55e87721SMatt Gilbride          f"  $ git checkout -b new_module_java-{output_name}\n"
380*55e87721SMatt Gilbride          f"  $ git add .\n"
381*55e87721SMatt Gilbride          f"  $ git commit -m 'feat: [{api_shortname}] new module for {api_shortname}'\n"
382*55e87721SMatt Gilbride          f"  $ gh pr create --title 'feat: [{api_shortname}] new module for {api_shortname}'")
383*55e87721SMatt Gilbride
384*55e87721SMatt Gilbrideif __name__ == "__main__":
385*55e87721SMatt Gilbride    main()
386