Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit 5dd8fcc

Browse files
authored
feat: add gapic metadata file (#781)
The GAPIC metadata file is used to track code, samples, and test coverage for every RPC and library method.
1 parent b199b14 commit 5dd8fcc

8 files changed

Lines changed: 277 additions & 10 deletions

File tree

.github/CODEOWNERS

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
# For syntax help see:
55
# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
66

7-
* @googleapis/actools-python @googleapis/yoshi-python @lukesneeringer
8-
*.yaml @googleapis/actools @googleapis/yoshi-python @googleapis/actools-python @lukesneeringer
7+
* @googleapis/actools-python @googleapis/yoshi-python
8+
*.yaml @googleapis/actools @googleapis/yoshi-python @googleapis/actools-python

.github/workflows/tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,4 +310,4 @@ jobs:
310310
python -m pip install autopep8
311311
- name: Check diff
312312
run: |
313-
find gapic tests -name "*.py" | xargs autopep8 --in-place --exit-code
313+
find gapic tests -name "*.py" | xargs autopep8 --diff --exit-code
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{# {{ api.gapic_metadata_json(opts) }} #} {# TODO(dovs): This is temporarily commented out pending the addition of a flag #}

gapic/cli/generate.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,11 @@ def generate(
4747
# This generator uses a slightly different mechanism for determining
4848
# which files to generate; it tracks at package level rather than file
4949
# level.
50-
package = os.path.commonprefix([i.package for i in filter(
51-
lambda p: p.name in req.file_to_generate,
52-
req.proto_file,
53-
)]).rstrip('.')
50+
package = os.path.commonprefix([
51+
p.package
52+
for p in req.proto_file
53+
if p.name in req.file_to_generate
54+
]).rstrip('.')
5455

5556
# Build the API model object.
5657
# This object is a frozen representation of the whole API, and is sent

gapic/schema/api.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828

2929
from google.api_core import exceptions # type: ignore
3030
from google.api import resource_pb2 # type: ignore
31+
from google.gapic.metadata import gapic_metadata_pb2 # type: ignore
3132
from google.longrunning import operations_pb2 # type: ignore
3233
from google.protobuf import descriptor_pb2
34+
from google.protobuf.json_format import MessageToJson
3335

3436
import grpc # type: ignore
3537

@@ -392,6 +394,45 @@ def subpackages(self) -> Mapping[str, 'API']:
392394
)
393395
return answer
394396

397+
def gapic_metadata(self, options: Options) -> gapic_metadata_pb2.GapicMetadata:
398+
gm = gapic_metadata_pb2.GapicMetadata(
399+
schema="1.0",
400+
comment="This file maps proto services/RPCs to the corresponding library clients/methods",
401+
language="python",
402+
proto_package=self.naming.proto_package,
403+
library_package=".".join(
404+
self.naming.module_namespace +
405+
(self.naming.versioned_module_name,)
406+
),
407+
)
408+
409+
for service in sorted(self.services.values(), key=lambda s: s.name):
410+
service_desc = gm.services.get_or_create(service.name)
411+
412+
# At least one of "grpc" or "rest" is guaranteed to be present because
413+
# of the way that Options instances are created.
414+
# This assumes the options are generated by the class method factory.
415+
transports = []
416+
if "grpc" in options.transport:
417+
transports.append(("grpc", service.client_name))
418+
transports.append(("grpcAsync", service.async_client_name))
419+
420+
if "rest" in options.transport:
421+
transports.append(("rest", service.client_name))
422+
423+
methods = sorted(service.methods.values(), key=lambda m: m.name)
424+
for tprt, client_name in transports:
425+
transport = service_desc.clients.get_or_create(tprt)
426+
transport.library_client = client_name
427+
for method in methods:
428+
method_desc = transport.rpcs.get_or_create(method.name)
429+
method_desc.methods.append(to_snake_case(method.name))
430+
431+
return gm
432+
433+
def gapic_metadata_json(self, options: Options) -> str:
434+
return MessageToJson(self.gapic_metadata(options), sort_keys=True)
435+
395436
def requires_package(self, pkg: Tuple[str, ...]) -> bool:
396437
return any(
397438
message.ident.package == pkg
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{# {{ api.gapic_metadata_json(opts) }} #} {# TODO(dovs): This is temporarily commented out pending the addition of a flag #}

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
install_requires=(
4646
"click >= 6.7",
4747
"google-api-core >= 1.17.0",
48-
"googleapis-common-protos >= 1.6.0",
48+
"googleapis-common-protos >= 1.53.0",
4949
"grpcio >= 1.24.3",
5050
"jinja2 >= 2.10",
5151
"protobuf >= 3.12.0",

tests/unit/schema/test_api.py

Lines changed: 225 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
from google.api import client_pb2
2323
from google.api import resource_pb2
2424
from google.api_core import exceptions
25+
from google.gapic.metadata import gapic_metadata_pb2
2526
from google.longrunning import operations_pb2
2627
from google.protobuf import descriptor_pb2
28+
from google.protobuf.json_format import MessageToJson
2729

2830
from gapic.schema import api
2931
from gapic.schema import imp
@@ -260,8 +262,8 @@ def test_proto_oneof():
260262
name='Bar',
261263
fields=(
262264
make_field_pb2(name='imported_message', number=1,
263-
type_name='.google.dep.ImportedMessage',
264-
oneof_index=0),
265+
type_name='.google.dep.ImportedMessage',
266+
oneof_index=0),
265267
make_field_pb2(
266268
name='primitive', number=2, type=1, oneof_index=0),
267269
),
@@ -1287,3 +1289,224 @@ def test_map_field_name_disambiguation():
12871289
# The same module used in the same place should have the same import alias.
12881290
# Because there's a "mollusc" name used, the import should be disambiguated.
12891291
assert mollusc_ident == mollusc_map_ident == "am_mollusc.Mollusc"
1292+
1293+
1294+
def test_gapic_metadata():
1295+
api_schema = api.API.build(
1296+
file_descriptors=[
1297+
descriptor_pb2.FileDescriptorProto(
1298+
name="cephalopod.proto",
1299+
package="animalia.mollusca.v1",
1300+
message_type=[
1301+
descriptor_pb2.DescriptorProto(
1302+
name="MolluscRequest",
1303+
),
1304+
descriptor_pb2.DescriptorProto(
1305+
name="Mollusc",
1306+
),
1307+
],
1308+
service=[
1309+
descriptor_pb2.ServiceDescriptorProto(
1310+
name="Squid",
1311+
method=[
1312+
descriptor_pb2.MethodDescriptorProto(
1313+
name="Ramshorn",
1314+
input_type="animalia.mollusca.v1.MolluscRequest",
1315+
output_type="animalia.mollusca.v1.Mollusc",
1316+
),
1317+
descriptor_pb2.MethodDescriptorProto(
1318+
name="Humboldt",
1319+
input_type="animalia.mollusca.v1.MolluscRequest",
1320+
output_type="animalia.mollusca.v1.Mollusc",
1321+
),
1322+
descriptor_pb2.MethodDescriptorProto(
1323+
name="Giant",
1324+
input_type="animalia.mollusca.v1.MolluscRequest",
1325+
output_type="animalia.mollusca.v1.Mollusc",
1326+
),
1327+
],
1328+
),
1329+
descriptor_pb2.ServiceDescriptorProto(
1330+
name="Octopus",
1331+
method=[
1332+
descriptor_pb2.MethodDescriptorProto(
1333+
name="GiantPacific",
1334+
input_type="animalia.mollusca.v1.MolluscRequest",
1335+
output_type="animalia.mollusca.v1.Mollusc",
1336+
),
1337+
descriptor_pb2.MethodDescriptorProto(
1338+
name="BlueSpot",
1339+
input_type="animalia.mollusca.v1.MolluscRequest",
1340+
output_type="animalia.mollusca.v1.Mollusc",
1341+
),
1342+
]
1343+
),
1344+
],
1345+
)
1346+
]
1347+
)
1348+
1349+
opts = Options.build("transport=grpc")
1350+
expected = gapic_metadata_pb2.GapicMetadata(
1351+
schema="1.0",
1352+
comment="This file maps proto services/RPCs to the corresponding library clients/methods",
1353+
language="python",
1354+
proto_package="animalia.mollusca.v1",
1355+
library_package="animalia.mollusca_v1",
1356+
services={
1357+
"Octopus": gapic_metadata_pb2.GapicMetadata.ServiceForTransport(
1358+
clients={
1359+
"grpc": gapic_metadata_pb2.GapicMetadata.ServiceAsClient(
1360+
library_client="OctopusClient",
1361+
rpcs={
1362+
"BlueSpot": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["blue_spot"]),
1363+
"GiantPacific": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["giant_pacific"]),
1364+
},
1365+
),
1366+
"grpcAsync": gapic_metadata_pb2.GapicMetadata.ServiceAsClient(
1367+
library_client="OctopusAsyncClient",
1368+
rpcs={
1369+
"BlueSpot": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["blue_spot"]),
1370+
"GiantPacific": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["giant_pacific"]),
1371+
},
1372+
),
1373+
}
1374+
),
1375+
"Squid": gapic_metadata_pb2.GapicMetadata.ServiceForTransport(
1376+
clients={
1377+
"grpc": gapic_metadata_pb2.GapicMetadata.ServiceAsClient(
1378+
library_client="SquidClient",
1379+
rpcs={
1380+
"Giant": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["giant"]),
1381+
"Humboldt": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["humboldt"]),
1382+
"Ramshorn": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["ramshorn"]),
1383+
},
1384+
),
1385+
"grpcAsync": gapic_metadata_pb2.GapicMetadata.ServiceAsClient(
1386+
library_client="SquidAsyncClient",
1387+
rpcs={
1388+
"Giant": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["giant"]),
1389+
"Humboldt": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["humboldt"]),
1390+
"Ramshorn": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["ramshorn"]),
1391+
},
1392+
),
1393+
}
1394+
),
1395+
}
1396+
)
1397+
actual = api_schema.gapic_metadata(opts)
1398+
assert expected == actual
1399+
expected = MessageToJson(expected, sort_keys=True)
1400+
actual = api_schema.gapic_metadata_json(opts)
1401+
assert expected == actual
1402+
1403+
opts = Options.build("transport=rest")
1404+
expected = gapic_metadata_pb2.GapicMetadata(
1405+
schema="1.0",
1406+
comment="This file maps proto services/RPCs to the corresponding library clients/methods",
1407+
language="python",
1408+
proto_package="animalia.mollusca.v1",
1409+
library_package="animalia.mollusca_v1",
1410+
services={
1411+
"Octopus": gapic_metadata_pb2.GapicMetadata.ServiceForTransport(
1412+
clients={
1413+
"rest": gapic_metadata_pb2.GapicMetadata.ServiceAsClient(
1414+
library_client="OctopusClient",
1415+
rpcs={
1416+
"BlueSpot": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["blue_spot"]),
1417+
"GiantPacific": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["giant_pacific"]),
1418+
},
1419+
)
1420+
}
1421+
),
1422+
"Squid": gapic_metadata_pb2.GapicMetadata.ServiceForTransport(
1423+
clients={
1424+
"rest": gapic_metadata_pb2.GapicMetadata.ServiceAsClient(
1425+
library_client="SquidClient",
1426+
rpcs={
1427+
"Giant": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["giant"]),
1428+
"Humboldt": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["humboldt"]),
1429+
"Ramshorn": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["ramshorn"]),
1430+
},
1431+
),
1432+
1433+
}
1434+
),
1435+
}
1436+
)
1437+
actual = api_schema.gapic_metadata(opts)
1438+
assert expected == actual
1439+
expected = MessageToJson(expected, sort_keys=True)
1440+
actual = api_schema.gapic_metadata_json(opts)
1441+
assert expected == actual
1442+
1443+
opts = Options.build("transport=rest+grpc")
1444+
expected = gapic_metadata_pb2.GapicMetadata(
1445+
schema="1.0",
1446+
comment="This file maps proto services/RPCs to the corresponding library clients/methods",
1447+
language="python",
1448+
proto_package="animalia.mollusca.v1",
1449+
library_package="animalia.mollusca_v1",
1450+
services={
1451+
"Octopus": gapic_metadata_pb2.GapicMetadata.ServiceForTransport(
1452+
clients={
1453+
"grpc": gapic_metadata_pb2.GapicMetadata.ServiceAsClient(
1454+
library_client="OctopusClient",
1455+
rpcs={
1456+
"BlueSpot": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["blue_spot"]),
1457+
"GiantPacific": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["giant_pacific"]),
1458+
},
1459+
),
1460+
"grpcAsync": gapic_metadata_pb2.GapicMetadata.ServiceAsClient(
1461+
library_client="OctopusAsyncClient",
1462+
rpcs={
1463+
"BlueSpot": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["blue_spot"]),
1464+
"GiantPacific": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["giant_pacific"]),
1465+
},
1466+
),
1467+
"rest": gapic_metadata_pb2.GapicMetadata.ServiceAsClient(
1468+
library_client="OctopusClient",
1469+
rpcs={
1470+
"BlueSpot": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["blue_spot"]),
1471+
"GiantPacific": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["giant_pacific"]),
1472+
},
1473+
)
1474+
}
1475+
),
1476+
"Squid": gapic_metadata_pb2.GapicMetadata.ServiceForTransport(
1477+
clients={
1478+
"grpc": gapic_metadata_pb2.GapicMetadata.ServiceAsClient(
1479+
library_client="SquidClient",
1480+
rpcs={
1481+
"Giant": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["giant"]),
1482+
"Humboldt": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["humboldt"]),
1483+
"Ramshorn": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["ramshorn"]),
1484+
},
1485+
),
1486+
"grpcAsync": gapic_metadata_pb2.GapicMetadata.ServiceAsClient(
1487+
library_client="SquidAsyncClient",
1488+
rpcs={
1489+
"Giant": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["giant"]),
1490+
"Humboldt": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["humboldt"]),
1491+
"Ramshorn": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["ramshorn"]),
1492+
},
1493+
),
1494+
"rest": gapic_metadata_pb2.GapicMetadata.ServiceAsClient(
1495+
library_client="SquidClient",
1496+
rpcs={
1497+
"Giant": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["giant"]),
1498+
"Humboldt": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["humboldt"]),
1499+
"Ramshorn": gapic_metadata_pb2.GapicMetadata.MethodList(methods=["ramshorn"]),
1500+
},
1501+
),
1502+
1503+
}
1504+
),
1505+
}
1506+
)
1507+
1508+
actual = api_schema.gapic_metadata(opts)
1509+
assert expected == actual
1510+
expected = MessageToJson(expected, sort_keys=True)
1511+
actual = api_schema.gapic_metadata_json(opts)
1512+
assert expected == actual

0 commit comments

Comments
 (0)