googleapis / gapic-generator-python

Generate Python API client libraries from Protocol Buffers.
Apache License 2.0
119 stars 67 forks source link

Support accessing trailing_metadata in unary functions #1856

Open daniel-sanche opened 10 months ago

daniel-sanche commented 10 months ago

Most grpc calls return a grpc.Call object containing trailing metadata and other useful information. This object is exposed to gapic users in streaming rpcs, because it is part of the iterator object that is returned when the rpc is called. Unary gapic methods don't currently expose this in any way, so there is no way to access trailing metadata without going around gapic

There are a few ways we could surface the grpc.Call object:

Synchronous

Option 1.

We can expose this in synchronous methods through a separete *_with_call method, which returns a tuple of the result data, and the Call object. This would align us with how the underlying grpc library handles the issue.

result = client.read_row()
result, call_data = client.read_row_with_call()

Option 2

Alternatively, we could expose this through a callback that is passed in, if we want to keep the result type static

def on_complete(call):
  print(call)

result = client.read_row(grpc_call_callback=on_complete)

~Either way, implementing this requires this change in the api-core repo~ (Merged)

Async

Option 1

One way to address this is to return the Call object directly, instead of awaiting it as part of the function. This would force us to change all of our function signatures from "coroutine functions" into "sync functions that return a coroutine", but functionally it should be the same. (This is already how we handle the stream methods)

# from
async def read_rows(*args, **kwargs):
  return await rpc(*args, **kwargs)

# to
def read_rows(*args, **kwargs):
  return rpc(*args, **kwargs)

Option 2

Alternatively, we could follow the same format we choose for the Synchronous surface, to keep things consistent (i.e. either returning a tuple when a flag is set, or accepting a callback)

A callback could be a bit tricky convenient for async functions though, because we need to be an an async context to access any useful information on the Call object

result, call = await client.read_row(with_call=True)

or

async def on_complete(call):
  print(call)

result = await client.read_row(grpc_call_callback=on_complete)

or

def on_complete(call, metadata, status):
  print(call)

result = await client.read_row(grpc_call_callback=on_complete)

or

future = asyncio.Future()
result = await client.read_row(grpc_call_future=future)
call_data = await future
vchudnov-g commented 9 months ago

@daniel-sanche , is this blocking your work in any way? Asking so we can prioritize this appropriately.

daniel-sanche commented 9 months ago

For now I will be making patches to the bigtable library directly, so not completely blocked. But having it available in the upstream generator soon would definitely help make things go smoother