Skip to content

Commit 1ce5aa6

Browse files
committed
feat(gapic-generator): add schema support for resumable uploads
1 parent 72544a4 commit 1ce5aa6

10 files changed

Lines changed: 128 additions & 1 deletion

File tree

‎packages/gapic-generator/gapic/schema/api.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,19 @@ def has_operations_mixin(self) -> bool:
10651065
> 0
10661066
)
10671067

1068+
@cached_property
1069+
def has_resumable_upload_methods(self) -> bool:
1070+
"""Return whether any service in the API has resumable upload methods.
1071+
1072+
Used in package-level templates (such as setup.py.j2) where the full API
1073+
is in scope rather than an individual service, to determine whether the
1074+
generated package requires resumable upload dependencies.
1075+
"""
1076+
return any(
1077+
service.has_resumable_upload_methods
1078+
for service in self.services.values()
1079+
)
1080+
10681081
@cached_property
10691082
def _has_iam_overrides(self) -> bool:
10701083
if not self.has_iam_mixin:
@@ -1651,6 +1664,7 @@ def _get_methods(
16511664
output=self.api_messages[meth_pb.output_type.lstrip(".")],
16521665
retry=retry,
16531666
timeout=timeout,
1667+
resumable_upload_prefix=self.opts.resumable_upload_prefix,
16541668
)
16551669

16561670
# Done; return the answer.

‎packages/gapic-generator/gapic/schema/imp.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ def __str__(self) -> str:
2929
# Determine if we need to suppress type checking for this import.
3030
# We do this for protobuf generated files (_pb2) and api_core
3131
# internals where type information might be missing or incomplete.
32-
needs_type_ignore = self.module.endswith("_pb2") or "api_core" in self.package
32+
needs_type_ignore = self.module.endswith("_pb2") or (
33+
"api_core" in self.package and self.module != "resumable_transfer"
34+
)
3335
if needs_type_ignore:
3436
# Use 'import absolute.path as module' syntax to prevent Ruff/isort
3537
# from combining this with other imports. This ensures the

‎packages/gapic-generator/gapic/schema/wrappers.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1499,6 +1499,7 @@ class Method:
14991499
meta: metadata.Metadata = dataclasses.field(
15001500
default_factory=metadata.Metadata,
15011501
)
1502+
resumable_upload_prefix: str = "resumable/upload"
15021503

15031504
def __getattr__(self, name):
15041505
return getattr(self.method_pb, name)
@@ -1728,6 +1729,13 @@ def http_opt(self) -> Optional[Dict[str, str]]:
17281729
# TODO(yon-mg): enums for http verbs?
17291730
return answer
17301731

1732+
@property
1733+
def is_resumable_upload(self) -> bool:
1734+
"""Return True if this method is a resumable upload method."""
1735+
# Resumable upload method names are temporarily hardcoded here until
1736+
# the resumable upload proto annotation exists and is published.
1737+
return self.name in ("UploadMedia", "CreateYouTubeVideoUpload")
1738+
17311739
@property
17321740
def path_params(self) -> Sequence[str]:
17331741
"""Return the path parameters found in the http annotation path template"""
@@ -2208,6 +2216,16 @@ def has_pagers(self) -> bool:
22082216
"""Return whether the service has paged methods."""
22092217
return any(m.paged_result_field for m in self.methods.values())
22102218

2219+
@property
2220+
def has_resumable_upload_methods(self) -> bool:
2221+
"""Return whether this specific service has resumable upload methods.
2222+
2223+
Used during per-service file generation and in service-level templates
2224+
to conditionally emit resumable upload imports, transports, and client
2225+
methods only for services that define resumable upload RPCs.
2226+
"""
2227+
return any(m.is_resumable_upload for m in self.methods.values())
2228+
22112229
@property
22122230
def host(self) -> str:
22132231
"""Return the hostname for this service, if specified.

‎packages/gapic-generator/gapic/utils/options.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class Options:
5252
proto_plus_deps: Tuple[str, ...] = dataclasses.field(default=("",))
5353
gapic_version: str = "0.0.0"
5454
resource_name_aliases: Dict[str, str] = dataclasses.field(default_factory=dict)
55+
resumable_upload_prefix: str = "resumable/upload"
5556

5657
# Class constants
5758
PYTHON_GAPIC_PREFIX: str = "python-gapic-"
@@ -78,6 +79,8 @@ class Options:
7879
# resource path to a custom TitleCase alias.
7980
# Format: resource.path/Name:AliasName
8081
"resource-name-alias",
82+
# Prefix for resumable upload requests
83+
"resumable-upload-prefix",
8184
)
8285
)
8386

@@ -222,6 +225,8 @@ def tweak_path(p):
222225
"Expected format is 'resource.path/Name:AliasName'."
223226
)
224227

228+
resumable_upload_prefix = opts.pop("resumable-upload-prefix", ["resumable/upload"])[0]
229+
225230
answer = Options(
226231
name=opts.pop("name", [""]).pop(),
227232
namespace=tuple(opts.pop("namespace", [])),
@@ -245,6 +250,7 @@ def tweak_path(p):
245250
proto_plus_deps=proto_plus_deps,
246251
gapic_version=opts.pop("gapic-version", ["0.0.0"]).pop(),
247252
resource_name_aliases=resource_name_aliases,
253+
resumable_upload_prefix=resumable_upload_prefix,
248254
)
249255

250256
# Note: if we ever need to recursively check directories for sample

‎packages/gapic-generator/tests/unit/common_types.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class DummyMethod:
4242
client_output: bool = False
4343
client_output_async: bool = False
4444
is_internal: bool = False
45+
is_resumable_upload: bool = False
4546

4647
@property
4748
def client_method_name(self):

‎packages/gapic-generator/tests/unit/generator/test_options.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,14 @@ def test_options_resource_name_aliases():
280280
# 3. MissingAlias:
281281
# (The empty ' ' string safely 'continues' without warning, as intended)
282282
assert warn.call_count == 3
283+
284+
285+
def test_options_resumable_upload_prefix():
286+
# Verify that resumable_upload_prefix defaults to "resumable/upload" when not specified.
287+
opts_default = Options.build("")
288+
assert opts_default.resumable_upload_prefix == "resumable/upload"
289+
290+
# Verify that a custom resumable-upload-prefix option is parsed and stored.
291+
opts_custom = Options.build("resumable-upload-prefix=custom/upload/prefix")
292+
assert opts_custom.resumable_upload_prefix == "custom/upload/prefix"
293+

‎packages/gapic-generator/tests/unit/schema/test_api.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4745,3 +4745,31 @@ def test_api_build_selective_multiple_protos_kept():
47454745
assert len(api_schema.protos) == 2
47464746
assert "proto1.proto" in api_schema.protos
47474747
assert "proto2.proto" in api_schema.protos
4748+
assert not api_schema.has_resumable_upload_methods
4749+
4750+
4751+
def test_api_has_resumable_upload_methods():
4752+
# Verify that an API containing a recognized resumable upload method
4753+
# reports has_resumable_upload_methods as True.
4754+
# Note: In MethodDescriptorProto, output_type is the protobuf RPC response
4755+
# message defined in the .proto file (e.g. UploadResponse).
4756+
fd = make_file_pb2(
4757+
name="upload.proto",
4758+
package="google.example.v1",
4759+
services=(
4760+
descriptor_pb2.ServiceDescriptorProto(
4761+
name="UploadService",
4762+
method=(
4763+
descriptor_pb2.MethodDescriptorProto(
4764+
name="UploadMedia",
4765+
input_type="google.example.v1.UploadRequest",
4766+
output_type="google.example.v1.UploadResponse",
4767+
),
4768+
),
4769+
),
4770+
),
4771+
messages=(make_message_pb2("UploadRequest"), make_message_pb2("UploadResponse")),
4772+
)
4773+
api_schema = api.API.build([fd], package="google.example.v1")
4774+
assert api_schema.has_resumable_upload_methods
4775+

‎packages/gapic-generator/tests/unit/schema/test_imp.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ def test_str_untyped_api_core():
4040
assert str(i) == "import foo.api_core.baz as bacon # type: ignore"
4141

4242

43+
def test_str_typed_resumable_transfer():
44+
# Verify that google.api_core.resumable_transfer is imported without "# type: ignore".
45+
i = imp.Import(package=("google", "api_core"), module="resumable_transfer")
46+
assert str(i) == "from google.api_core import resumable_transfer"
47+
48+
4349
def test_str_eq():
4450
i1 = imp.Import(package=("foo", "bar"), module="baz")
4551
i2 = imp.Import(package=("foo", "bar"), module="baz")

‎packages/gapic-generator/tests/unit/schema/wrappers/test_method.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,3 +1118,21 @@ def test__validate_paged_field_size_type(field_type, pb_type, expected):
11181118

11191119
actual = method._validate_paged_field_size_type(page_field_size=page_size)
11201120
assert actual == expected
1121+
1122+
1123+
def test_method_is_resumable_upload():
1124+
# UploadMedia (and CreateYouTubeVideoUpload) are temporarily hardcoded as
1125+
# resumable upload method names until resumable upload proto annotations
1126+
# are adopted in the future.
1127+
# Verify that UploadMedia is identified as a resumable upload method.
1128+
method_upload = make_method("UploadMedia")
1129+
assert method_upload.is_resumable_upload
1130+
1131+
# Verify that CreateYouTubeVideoUpload is also recognized as a resumable upload method.
1132+
method_youtube_upload = make_method("CreateYouTubeVideoUpload")
1133+
assert method_youtube_upload.is_resumable_upload
1134+
1135+
# Verify that a non-resumable method name is not marked as a resumable upload.
1136+
method_other = make_method("OtherMethod")
1137+
assert not method_other.is_resumable_upload
1138+

‎packages/gapic-generator/tests/unit/schema/wrappers/test_service.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,3 +757,26 @@ def test_resource_messages_raises_on_malformed_typeless_resource():
757757
# 2. Trigger the property and expect it to fail fast with the AIP-123 URL
758758
with pytest.raises(ValueError, match="https://google.aip.dev/123"):
759759
_ = service.resource_messages
760+
761+
762+
def test_service_has_resumable_upload_methods():
763+
# Verify that a service containing a resumable upload method reports
764+
# has_resumable_upload_methods as True.
765+
m_upload = make_method("UploadMedia")
766+
m_status = make_method("GetStatus")
767+
768+
service_with_resumable = make_service(
769+
name="ResumableService",
770+
methods=(m_upload, m_status),
771+
)
772+
assert service_with_resumable.has_resumable_upload_methods
773+
774+
# Verify that a service with only standard methods reports
775+
# has_resumable_upload_methods as False.
776+
m_other = make_method("DoThing")
777+
service_without_resumable = make_service(
778+
name="StandardService",
779+
methods=(m_other,),
780+
)
781+
assert not service_without_resumable.has_resumable_upload_methods
782+

0 commit comments

Comments
 (0)