Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
701476f
feat: change sampels to align with new design
busunkim96 Mar 15, 2021
c998993
feat: fulfill requirements for P0 snippetgen
busunkim96 Mar 16, 2021
6149983
fix: fix type issues
busunkim96 Mar 17, 2021
63a0509
fix: disable autogen snippets by default
busunkim96 Apr 6, 2021
793d108
fix: resolve mypy issues
busunkim96 Apr 6, 2021
be303df
fix: use trim_blocks and lstrip_blocks for jinja templates
busunkim96 Apr 8, 2021
c4f95cf
fix: fix more templates
busunkim96 Apr 13, 2021
1666d0a
fix: fix sample unit tests
busunkim96 Apr 13, 2021
23df2f3
test: add separate snippetgen session and basic golden snippet
busunkim96 Apr 19, 2021
a302db9
test: get existing unit tests to pass
busunkim96 Apr 20, 2021
093f931
chore: comment out manifest gen
busunkim96 Apr 20, 2021
2de6ac8
chore: tweak dummy ident in tests
busunkim96 Apr 20, 2021
ee1897c
feat: fill out basic snippets for other types
busunkim96 Apr 21, 2021
6b46aab
Merge branch 'master' into snippet-gen
busunkim96 Apr 22, 2021
0944178
fix: properly resolve merge conflict
busunkim96 Apr 22, 2021
a487f7d
chore: comment cleanup
busunkim96 Apr 23, 2021
351f0fe
Merge branch 'master' into snippet-gen
busunkim96 Apr 23, 2021
0ba949c
test: fix protoc installation, add snippetgen tests
busunkim96 Apr 23, 2021
0425783
Merge branch 'snippet-gen' of github.com:googleapis/gapic-generator-p…
busunkim96 Apr 23, 2021
769e06f
fix: fix style
busunkim96 Apr 23, 2021
e6224a6
chore: tell snippetgen to ignore this repo
busunkim96 Apr 23, 2021
0fce06c
fix: address review comments
busunkim96 May 3, 2021
3c3cb5a
fix: re-add templates on jinja files
busunkim96 May 3, 2021
b218789
chore: ignore test_output dir
busunkim96 May 3, 2021
cca0f54
test: add default host to gen sample spec test
busunkim96 May 3, 2021
ce309df
Merge branch 'master' into snippet-gen
software-dov May 3, 2021
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
107 changes: 74 additions & 33 deletions gapic/generator/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@

import jinja2
import yaml
import itertools
import re
import os
from typing import Any, DefaultDict, Dict, Mapping
from typing import Any, DefaultDict, Dict, Generator, Mapping
from hashlib import sha256
from collections import OrderedDict, defaultdict
from gapic.samplegen_utils.utils import coerce_response_name, is_valid_sample_cfg
Expand Down Expand Up @@ -106,25 +107,59 @@ def get_response(
)

sample_output = self._generate_samples_and_manifest(
api_schema,
self._env.get_template(sample_templates[0]),
) if sample_templates else {}

api_schema, self._env.get_template(sample_templates[0]),
opts=opts,
)
output_files.update(sample_output)


# Return the CodeGeneratorResponse output.
res = CodeGeneratorResponse(
file=[i for i in output_files.values()]) # type: ignore
res.supported_features |= CodeGeneratorResponse.Feature.FEATURE_PROTO3_OPTIONAL # type: ignore
return res


def _generate_sample_specs(self, api_schema: api.API, *, opts):
"""Given an API, generate rudimentary sample specs."""

sample_specs = []

gapic_metadata = api_schema.gapic_metadata(opts)

# Loop through the method list
for service_name, service in gapic_metadata.services.items():
raise Exception(f"duplicate entries for {service.clients}")
for client_name, client in service.clients.items():

for rpc_name, method_list in client.rpcs.items():
# generated_${api}_${apiVersion}_${serviceName}_${rpcName}_{sync|async}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to change now so that snippets can be matched to products by the snippet tracker. @busunkim96 I've cc'ed you on a comment on the desgin doc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed! One thing I noticed is that the endpoint is declared at the service level. I guess in practice the endpoint is the same across all services in the same API? https://github.com/googleapis/googleapis/blob/ae5fb2884a296832c39867e8e8c81bbc72a32ce8/google/cloud/pubsublite/v1/subscriber.proto#L35


region_tag = f"generated_{api_schema.naming.versioned_module_name}_{service_name}_{rpc_name}_sync"
spec = {
"sample_type": "standalone",
"rpc": rpc_name,
"request": {},
"response": {},
"service": f"{api_schema.naming.proto_package}.{service_name}",
"region_tag": region_tag,
"description": f"Snippet for {utils.to_snake_case(rpc_name)}"
}

sample_specs.append(spec)

return sample_specs



def _generate_samples_and_manifest(
self, api_schema: api.API, sample_template: jinja2.Template,
) -> Dict[str, CodeGeneratorResponse.File]:
self, api_schema: api.API, sample_template: jinja2.Template, *, opts: Options) -> Dict:
"""Generate samples and samplegen manifest for the API.

Arguments:
api_schema (api.API): The schema for the API to which the samples belong.
sample_template (jinja2.Template): The template to use to generate samples.
opts (Options): Additional generator options.

Returns:
Dict[str, CodeGeneratorResponse.File]: A dict mapping filepath to rendered file.
Expand All @@ -135,12 +170,16 @@ def _generate_samples_and_manifest(
id_to_hash_to_spec: DefaultDict[str,
Dict[str, Any]] = defaultdict(dict)

spec_generator = ()

STANDALONE_TYPE = "standalone"

# Process optional handwritten configs
for config_fpath in self._sample_configs:
with open(config_fpath) as f:
configs = yaml.safe_load_all(f.read())

spec_generator = (
spec_generator = itertools.chain(spec_generator, (
spec
for cfg in configs
if is_valid_sample_cfg(cfg)
Expand All @@ -149,41 +188,44 @@ def _generate_samples_and_manifest(
# If sample_types are specified, standalone samples must be
# explicitly enabled.
if STANDALONE_TYPE in spec.get("sample_type", [STANDALONE_TYPE])
)
))

# Add autogenerated sample configs
generated_specs = self._generate_sample_specs(api_schema, opts=opts)
spec_generator = itertools.chain(spec_generator, generated_specs)

for spec in spec_generator:
# Every sample requires an ID. This may be provided
# by a samplegen config author.
# If no ID is provided, fall back to the region tag.
#
# Ideally the sample author should pick a descriptive, unique ID,
# but this may be impractical and can be error-prone.
spec_hash = sha256(str(spec).encode("utf8")).hexdigest()[:8]
sample_id = spec.get("id") or spec["region_tag"]
spec["id"] = sample_id

hash_to_spec = id_to_hash_to_spec[sample_id]

if spec_hash in hash_to_spec:
raise DuplicateSample(
f"Duplicate samplegen spec found: {spec}")

hash_to_spec[spec_hash] = spec

for spec in spec_generator:
# Every sample requires an ID, preferably provided by the
# samplegen config author.
# If no ID is provided, fall back to the region tag.
# If there's no region tag, generate a unique ID.
#
# Ideally the sample author should pick a descriptive, unique ID,
# but this may be impractical and can be error-prone.
spec_hash = sha256(str(spec).encode("utf8")).hexdigest()[:8]
sample_id = spec.get("id") or spec.get(
"region_tag") or spec_hash
spec["id"] = sample_id

hash_to_spec = id_to_hash_to_spec[sample_id]
if spec_hash in hash_to_spec:
raise DuplicateSample(
f"Duplicate samplegen spec found: {spec}")

hash_to_spec[spec_hash] = spec

out_dir = "samples"
out_dir = "samples/generated_samples"
fpath_to_spec_and_rendered = {}
for hash_to_spec in id_to_hash_to_spec.values():
for spec_hash, spec in hash_to_spec.items():
id_is_unique = len(hash_to_spec) == 1
# The ID is used to generate the file name and by sample tester
# to link filenames to invoked samples. It must be globally unique.
# The ID is used to generate the file name. It must be globally unique.
if not id_is_unique:
spec["id"] += f"_{spec_hash}"

sample = samplegen.generate_sample(
spec, api_schema, sample_template,)

# TODO: Make the samples have snake_case names
fpath = spec["id"] + ".py"
fpath_to_spec_and_rendered[os.path.join(out_dir, fpath)] = (
spec,
Expand All @@ -197,7 +239,6 @@ def _generate_samples_and_manifest(
for fname, (_, sample) in fpath_to_spec_and_rendered.items()
}

# Only generate a manifest if we generated samples.
if output_files:
manifest_fname, manifest_doc = manifest.generate(
(
Expand Down
5 changes: 1 addition & 4 deletions gapic/samplegen/samplegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,10 +880,7 @@ def generate_sample(

return sample_template.render(
sample=sample,
imports=[
"from google import auth",
"from google.auth import credentials",
],
imports=[],
calling_form=calling_form,
calling_form_enum=types.CallingForm,
api=api_schema,
Expand Down
8 changes: 6 additions & 2 deletions gapic/templates/examples/feature_fragments.j2
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ There is a little, but not enough for it to be important because

{% macro sample_header(sample, calling_form) %}

# DO NOT EDIT! This is a generated sample ("{{ calling_form }}", "{{ sample.id }}")
# Generated code. DO NOT EDIT!
#
# Snippet for {{ sample.rpc }}
# NOTE: This snippet has been automatically generated for illustrative purposes only.
# It may require modifications to work in your environment.

# To install the latest published package dependency, execute the following:
# pip3 install {{ sample.package_name }}
# python3 -m pip install {{ sample.package_name }}
{% endmacro %}

{% macro print_string_formatting(string_list) %}
Expand Down
30 changes: 8 additions & 22 deletions gapic/templates/examples/sample.py.j2
Original file line number Diff line number Diff line change
@@ -1,18 +1,3 @@
{#
# Copyright (C) 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#}
Comment thread
busunkim96 marked this conversation as resolved.
{% extends "_base.py.j2" %}

{% block content %}
Expand All @@ -26,23 +11,24 @@
{% for import_statement in imports %}
{{ import_statement }}
{% endfor %}
from {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }} import {{ service.client_name }}
from {{ api.naming.module_namespace|join(".") }} import {{ api.naming.versioned_module_name }}

{# also need calling form #}
def sample_{{ frags.render_method_name(sample.rpc)|trim -}}({{ frags.print_input_params(sample.request)|trim -}}):
"""{{ sample.description }}"""

client = {{ service.client_name }}(
credentials=credentials.AnonymousCredentials(),
transport="grpc",
)
# Create client
client = {{ api.naming.versioned_module_name }}.{{ service.client_name }}()

{{ frags.render_request_setup(sample.request)|indent }}
{# TODO: Enable detailed request setup #}
{# {{ frags.render_request_setup(sample.request)|indent }} #}
{% with method_call = frags.render_method_call(sample, calling_form, calling_form_enum) %}
# Make the request
{{ frags.render_calling_form(method_call, calling_form, calling_form_enum, sample.response, )|indent -}}
{% endwith %}

# [END {{ sample.id }}]

{{ frags.render_main_block(sample.rpc, sample.request) }}
{# TODO: Enable main block (or decide to remove main block from python sample) #}
{# {{ frags.render_main_block(sample.rpc, sample.request) }} #}
{%- endblock %}
5 changes: 3 additions & 2 deletions gapic/utils/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,9 @@ def tweak_path(p):
retry=retry_cfg,
sample_configs=tuple(
cfg_path
for s in sample_paths
for cfg_path in samplegen_utils.generate_all_sample_fpaths(s)
for cfg_path in sample_paths
#for s in sample_paths
#for cfg_path in samplegen_utils.generate_all_sample_fpaths(s)
),
templates=tuple(path.expanduser(i) for i in templates),
lazy_import=bool(opts.pop('lazy-import', False)),
Expand Down