Skip to content
This repository was archived by the owner on Oct 19, 2023. It is now read-only.

Commit aa8cc7f

Browse files
committed
google-assistant-sdk: better client-secrets handling
- resolve project id lazily so that subcommand can validate arguments. - rename --client-secret to --client-secrets for consistency - add docstrings - add tests Bug: 67744677 Bug: 67062654 Change-Id: Id1d3e07f1fcba2009c40eb163b20dc85eeeb8b0b
1 parent 620cd20 commit aa8cc7f

File tree

2 files changed

+116
-39
lines changed

2 files changed

+116
-39
lines changed

google-assistant-sdk/googlesamples/assistant/grpc/devicetool.py

Lines changed: 70 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
import google.auth.transport.requests
2424

2525

26+
ASSISTANT_API_VERSION = 'v1alpha2'
27+
28+
2629
def failed_request_exception(message, r):
2730
"""Build ClickException from a failed request."""
2831
try:
@@ -37,8 +40,46 @@ def failed_request_exception(message, r):
3740
r.text))
3841

3942

40-
# Prints out a device model in the terminal by parsing dict
43+
def resolve_project_id(client_secrets, credentials):
44+
"""Resolve project ID from client secrets."""
45+
if client_secrets is None:
46+
client_secrets = 'client_secret_%s.json' % credentials.client_id
47+
try:
48+
with open(client_secrets, 'r') as f:
49+
secret = json.load(f)
50+
return secret['installed']['project_id']
51+
except Exception as e:
52+
raise click.ClickException('Error loading client secret: %s.\n'
53+
'Run the device tool '
54+
'with --client-secrets '
55+
'or --project option.\n'
56+
'Or copy the %s file '
57+
'in the current directory.'
58+
% (e, client_secrets))
59+
60+
61+
def build_api_url(api_endpoint, api_version, project_id):
62+
return 'https://%s/%s/projects/%s' % (api_endpoint,
63+
api_version,
64+
project_id)
65+
66+
67+
def build_client_from_context(ctx):
68+
project_id = (ctx.obj['PROJECT']
69+
or resolve_project_id(ctx.obj['CLIENT_SECRETS'],
70+
ctx.obj['CREDENTIALS']))
71+
api_url = build_api_url(ctx.obj['API_ENDPOINT'],
72+
ctx.obj['API_VERSION'],
73+
project_id)
74+
session = (ctx.obj['SESSION'] or
75+
google.auth.transport.requests.AuthorizedSession(
76+
ctx.obj['CREDENTIALS']
77+
))
78+
return session, api_url, project_id
79+
80+
4181
def pretty_print_model(devicemodel):
82+
"""Prints out a device model in the terminal by parsing dict."""
4283
PRETTY_PRINT_MODEL = """Device Model Id: %(deviceModelId)s
4384
Project Id: %(projectId)s
4485
Device Type: %(deviceType)s"""
@@ -51,8 +92,8 @@ def pretty_print_model(devicemodel):
5192
logging.info('') # Newline
5293

5394

54-
# Prints out a device instance in the terminal by parsing dict
5595
def pretty_print_device(device):
96+
"""Prints out a device instance in the terminal by parsing dict."""
5697
logging.info('Device Instance Id: %s' % device['id'])
5798
if 'nickname' in device:
5899
logging.info(' Nickname: %s' % device['nickname'])
@@ -67,8 +108,8 @@ def pretty_print_device(device):
67108
'use with the registration tool. If you don\'t use this flag, '
68109
'the tool will use the project listed in the '
69110
'<client_secret_client-id.json> file you specify with the '
70-
'--client-secret flag.')
71-
@click.option('--client-secret',
111+
'--client-secrets flag.')
112+
@click.option('--client-secrets',
72113
help='Enter the path and filename for the '
73114
'<client_secret_client-id.json> file you downloaded from your '
74115
'developer project. This file is used to infer the Google '
@@ -93,7 +134,7 @@ def pretty_print_device(device):
93134
'API. You can use this flag if the credentials were generated '
94135
'in a location that is different than the default.')
95136
@click.pass_context
96-
def cli(ctx, project, client_secret, verbose, api_endpoint, credentials):
137+
def cli(ctx, project, client_secrets, verbose, api_endpoint, credentials):
97138
try:
98139
with open(credentials, 'r') as f:
99140
c = google.oauth2.credentials.Credentials(token=None,
@@ -104,25 +145,12 @@ def cli(ctx, project, client_secret, verbose, api_endpoint, credentials):
104145
raise click.ClickException('Error loading credentials: %s.\n'
105146
'Run google-oauthlib-tool to initialize '
106147
'new OAuth 2.0 credentials.' % e)
107-
if project is None:
108-
if client_secret is None:
109-
client_secret = 'client_secret_%s.json' % c.client_id
110-
try:
111-
with open(client_secret, 'r') as f:
112-
secret = json.load(f)
113-
project = secret['installed']['project_id']
114-
except Exception as e:
115-
raise click.ClickException('Error loading client secret: %s.\n'
116-
'Run the register tool '
117-
'with --client-secret '
118-
'or --project option.\n'
119-
'Or copy the %s file '
120-
'in the current directory.'
121-
% (e, client_secret))
122-
ctx.obj['SESSION'] = google.auth.transport.requests.AuthorizedSession(c)
123-
ctx.obj['API_URL'] = ('https://%s/v1alpha2/projects/%s'
124-
% (api_endpoint, project))
125-
ctx.obj['PROJECT_ID'] = project
148+
ctx.obj['API_ENDPOINT'] = api_endpoint
149+
ctx.obj['API_VERSION'] = ASSISTANT_API_VERSION
150+
ctx.obj['SESSION'] = None
151+
ctx.obj['PROJECT'] = project
152+
ctx.obj['CREDENTIALS'] = c
153+
ctx.obj['CLIENT_SECRETS'] = client_secrets
126154
logging.basicConfig(format='',
127155
level=logging.DEBUG if verbose else logging.INFO)
128156

@@ -174,6 +202,13 @@ def register(ctx, model, type, trait, manufacturer, product_name, description,
174202
following symbols: period (.), hyphen (-), underscore (_), space ( ) and
175203
plus (+). The first character of a field must be a letter or number.
176204
"""
205+
# cache SESSION and PROJECT so that we don't re-create them between request
206+
ctx.obj['SESSION'] = google.auth.transport.requests.AuthorizedSession(
207+
ctx.obj['CREDENTIALS']
208+
)
209+
ctx.obj['PROJECT'] = (ctx.obj['PROJECT']
210+
or resolve_project_id(ctx.obj['CLIENT_SECRETS'],
211+
ctx.obj['CREDENTIALS']))
177212
ctx.invoke(register_model,
178213
model=model, type=type, trait=trait,
179214
manufacturer=manufacturer,
@@ -218,13 +253,12 @@ def register_model(ctx, model, type, trait,
218253
following symbols: period (.), hyphen (-), underscore (_), space ( ) and
219254
plus (+). The first character of a field must be a letter or number.
220255
"""
221-
session = ctx.obj['SESSION']
222-
223-
model_base_url = '/'.join([ctx.obj['API_URL'], 'deviceModels'])
256+
session, api_url, project_id = build_client_from_context(ctx)
257+
model_base_url = '/'.join([api_url, 'deviceModels'])
224258
model_url = '/'.join([model_base_url, model])
225259
payload = {
226260
'device_model_id': model,
227-
'project_id': ctx.obj['PROJECT_ID'],
261+
'project_id': project_id,
228262
'device_type': 'action.devices.types.' + type,
229263
}
230264
if trait:
@@ -278,9 +312,8 @@ def register_device(ctx, device, model, nickname, client_type):
278312
following symbols: period (.), hyphen (-), underscore (_), space ( ) and
279313
plus (+). The first character of a field must be a letter or number.
280314
"""
281-
session = ctx.obj['SESSION']
282-
283-
device_base_url = '/'.join([ctx.obj['API_URL'], 'devices'])
315+
session, api_url, project_id = build_client_from_context(ctx)
316+
device_base_url = '/'.join([api_url, 'devices'])
284317
device_url = '/'.join([device_base_url, device])
285318
payload = {
286319
'id': device,
@@ -319,9 +352,8 @@ def get(ctx, resource, id):
319352
"""Gets all of the information (fields) for a given device model or
320353
instance.
321354
"""
322-
session = ctx.obj['SESSION']
323-
324-
url = '/'.join([ctx.obj['API_URL'], resource, id])
355+
session, api_url, project_id = build_client_from_context(ctx)
356+
url = '/'.join([api_url, resource, id])
325357
r = session.get(url)
326358
if r.status_code != 200:
327359
raise failed_request_exception('Failed to get resource', r)
@@ -344,8 +376,8 @@ def get(ctx, resource, id):
344376
def delete(ctx, resource, id):
345377
"""Delete given device model or instance.
346378
"""
347-
session = ctx.obj['SESSION']
348-
url = '/'.join([ctx.obj['API_URL'], resource, id])
379+
session, api_url, project_id = build_client_from_context(ctx)
380+
url = '/'.join([api_url, resource, id])
349381
r = session.delete(url)
350382
if r.status_code != 200:
351383
raise failed_request_exception('failed to delete resource', r)
@@ -361,9 +393,8 @@ def list(ctx, resource):
361393
current Google Developer project. To change the current project, use the
362394
devicetool's --project flag.
363395
"""
364-
session = ctx.obj['SESSION']
365-
366-
url = '/'.join([ctx.obj['API_URL'], resource])
396+
session, api_url, project_id = build_client_from_context(ctx)
397+
url = '/'.join([api_url, resource])
367398
r = session.get(url)
368399
if r.status_code != 200:
369400
raise failed_request_exception('Failed to list resources', r)

google-assistant-sdk/tests/test_devicetool.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16+
import json
17+
import tempfile
18+
1619
from googlesamples.assistant.grpc import devicetool
1720

1821

@@ -25,3 +28,46 @@ def test_print_model_with_no_trait(caplog):
2528
assert 'model-id' in caplog.text
2629
assert 'project-id' in caplog.text
2730
assert 'device-type' in caplog.text
31+
32+
33+
def test_resolve_project_id():
34+
with tempfile.NamedTemporaryFile(mode='w+') as f:
35+
f.write(json.dumps({
36+
'installed': {
37+
'project_id': 'foo'
38+
}
39+
}))
40+
f.flush()
41+
assert 'foo' == devicetool.resolve_project_id(f.name, '42')
42+
43+
44+
def test_build_api_url():
45+
assert ('https://myhostname/myversion/projects/myproject' ==
46+
devicetool.build_api_url('myhostname', 'myversion', 'myproject'))
47+
48+
49+
class Context(object):
50+
pass
51+
52+
53+
class Session(object):
54+
pass
55+
56+
57+
def test_build_client():
58+
my_session = Session()
59+
my_context = Context()
60+
my_context.obj = {
61+
'PROJECT': 'myproject',
62+
'API_ENDPOINT': 'myhostname',
63+
'API_VERSION': 'myversion',
64+
'SESSION': my_session
65+
}
66+
session, api_url, project = devicetool.build_client_from_context(
67+
my_context
68+
)
69+
assert session is my_session
70+
assert project == 'myproject'
71+
assert 'myhostname' in api_url
72+
assert 'myversion' in api_url
73+
assert 'myproject' in api_url

0 commit comments

Comments
 (0)