Exploring Google (Cloud) APIs
with Python & JavaScript
Wesley Chun
Developer Advocate
Google Cloud
G Suite Dev Show
goo.gl/JpBQ40
About the speaker
● Developer Advocate, Google Cloud
● Mission: enable current & future developers to be successful using
Google Cloud and other Google developer tools, APIs, and platforms
● Videos: host of the G Suite Dev Show on YouTube
● Blogs: developers.googleblog.com & gsuite-developers.googleblog.com
● Twitters: @wescpy, @GCPcloud, @GoogleDevs, @GSuiteDevs
● Background
● Software engineer & architect for 20+ years
● One of the original Yahoo!Mail engineers
● Author of bestselling "Core Python" books (corepython.com)
● Teacher and technical instructor since 1983 (all ages)
● Fellow of the Python Software Foundation
● AB Mathematics & CMP Music, UC Berkeley; MSCS UC Santa Barbara
Agenda
● Introduction
● Getting started with Google API projects
● Google Cloud (HTTP-based) REST APIs
● Serverless (compute) platforms
● Miscellaneous & wrap-up
Part I: Introduction
BUT
... wait, there’s more...
Part II: Getting Started with Google API projects
The first word on Security
Authentication ("authn") vs authorization ("authz")
● authn: you are who you say you are
○ login & passwd
○ handprint authentication
○ retina scan
● authz: okay, you are who you say you are, but can you haz data?
○ OAuth2 - mostly authz, but some authn
○ Mostly about 3rd-party access to data
○ Users must give YOUR app access to THEIR data
○ Most of the time when you see "auth", it refers to authz
Developers Console (devconsole)
OAuth2 scenarios
● Varies on application type (where apps located)
○ Web (server) applications
○ Installed (mobile & desktop computers) apps
○ Service accounts (cloud/server-to-server)
○ Client-side (JavaScript) apps
○ Limited-input devices (game consoles, printers, cameras, etc.)
○ developers.google.com/identity/protocols/OAuth2
○ TODAY: command-line script == "Installed"
Google APIs client
libraries for many
languages; demos in
developers.google.com/
api-client-library
Accessing API client libraries
● Python
○ pip install -U google-api-python-client
○ developers.google.com/api-client-library/
python/start/get_started
● JavaScript ("browser"/client-side)
○ Use gapi.client.request
○ developers.google.com/api-client-library/
javascript/start/start-js
● JavaScript (Node.js/server-side)
○ npm install googleapis
○ googleapis.github.io/google-api-nodejs-client
SIMPLE
AUTHORIZED
Which do you choose?
Simple API access
Google OAuth2 process
● Goal: valid access token == 3rd-party API data access
● How OAuth2 works (in general)
○ Step 1: Create OAuth2 credentials in DevConsole
○ Step 2: Send credentials to get access & refresh tokens
○ Step 3: Use access token to make authorized API calls
○ Step 4: Access tokens expire; use refresh to get new one
● developers.google.com/identity/protocols/OAuth2
● Other APIs will be similar with some differences
Authorized API access
OAuth2
HTTP-based REST APIs 1
HTTP
2
Google APIs request-response workflow
1. Application makes request
2. Request received by service
3. Process data, return response
4. Results sent to application
a.k.a. client-server model
Part III: Google Cloud REST APIs
Compute
Big Data
BigQuery
Cloud
Pub/Sub
Cloud
Dataproc
Cloud
Datalab
Cloud
Pub/Sub
Genomics
Cloud AI
Cloud Machine
Learning Engine
Cloud
Vision API
Cloud
Speech-to-Text
Cloud Natural
Language API
Cloud
Translation
API
Cloud
Jobs API
Cloud
Dataprep
Cloud Video
Intelligence
API
Advanced
Solutions Lab
Compute
Engine
App
Engine
Kubernetes
Engine
GPU
Cloud
Functions
Container-
Optimized OS
Identity & Security
Cloud IAM
Cloud Resource
Manager
Cloud Security
Scanner
Key
Management
Service
BeyondCorp
Data Loss
Prevention API
Identity-Aware
Proxy
Security Key
Enforcement
Cloud
AutoML
Cloud
Text-to-Speech
Cloud TPU
Dialogflow
Enterprise
Edition
Data Transfer
Transfer
Appliance
Cloud
Composer
Cloud Security
Command
Center
Networking
Virtual
Private Cloud
Cloud Load
Balancing
Cloud
CDN
Dedicated
Interconnect
Cloud DNS
Cloud
Network
Cloud
External IP
Addresses
Cloud
Firewall Rules
Cloud
Routes
Cloud VPN
Management Tools
Stackdriver Monitoring Logging
Error
Reporting
Trace
Debugger
Cloud
Deployment
Manager
Cloud
Console
Cloud
Shell
Cloud Mobile
App
Cloud
Billing API
Cloud
APIs
Cloud
Router
Partner
Interconnect
Cloud Armor
Standard
Network Tier
Premium
Network Tier
Profiler
GCP products and
services without a
unique icon have a
generic hexagon.
Developer Tools
Cloud SDK
Cloud Source
Repositories
Maven App
Engine Plugin
Cloud Tools
for IntelliJ
Cloud
Tools for
PowerShell
Cloud
Tools for
Visual Studio
Container
Registry
Cloud Tools
for Eclipse
Cloud Build
API Platform & Ecosystems
API
Analytics
API
Monetization
Apigee API
Platform
Apigee
Sense
Cloud
Endpoints
Developer
Portal
Gradle App
Engine Plugin
IDE plugins
Internet of Things
Cloud IoT
Core
Storage & Databases
Cloud
Storage
Cloud
Bigtable
Cloud
Datastore
Cloud SQL
Cloud
Spanner
Persistent
Disk
Cloud
Memorystore
Cloud
Filestore
Cloud
Test Lab
Cloud IoT
Edge
G Suite APIs
Proprietary + Confidential
● Launched API v3 (Dec 2015)
○ v2 not deprecated (yet)
● WhatsApp and Google BigQuery
integrations (May 2016)
● Team Drives (Jun 2017)
Drive
Back up your file archives
Write your own or see github.com/gsuitedevs/drive-zipextractor (JS)
Automate photo albums
OR
List (first 100) files/folders in Google Drive
from __future__ import print_function
from apiclient import discovery
from httplib2 import Http
from oauth2client import file, client, tools
SCOPES = 'https://www.googleapis.com/auth/drive.readonly.metadata'
store = file.Storage('storage.json')
creds = store.get()
if not creds or creds.invalid:
flow = client.flow_from_clientsecrets('client_secret.json', SCOPES)
creds = tools.run_flow(flow, store)
DRIVE = discovery.build('drive', 'v3', http=creds.authorize(Http()))
files = DRIVE.files().list().execute().get('files', [])
for f in files:
print(f['name'], f['mimeType'])
Listing your files
goo.gl/ZIgf8k
Proprietary + Confidential
● Launched API v4 (I/O 2016)
● Support text rotation (Mar 2017)
● Developer metadata (Sep 2017)
● Macro recorder (Apr 2018)
Sheets
Powerful API: expect UI features available programmatically
● Create new sheets within
spreadsheets
● Add data to sheets
● Create new spreadsheets
● Set frozen rows
● Text+number formatting
● Enforce cell validation
● Adjust column sizes
● Apply formulas
● Build pivot tables
● Create charts
● … and more!
BEFORE
(<= v3)
AFTER
(>= v4)
Import/Export: Customized reports, “database,” or both!
Try our Node.js customized reporting tool codelab:
g.co/codelabs/sheets
Migrate SQL data to a Sheet
# read SQL data then create new spreadsheet & add rows into it
FIELDS = ('ID', 'Customer Name', 'Product Code',
'Units Ordered', 'Unit Price', 'Status')
cxn = sqlite3.connect('db.sqlite')
cur = cxn.cursor()
rows = cur.execute('SELECT * FROM orders').fetchall()
cxn.close()
rows.insert(0, FIELDS)
DATA = {'properties': {'title': 'Customer orders'}}
SHEET_ID = SHEETS.spreadsheets().create(body=DATA,
fields='spreadsheetId').execute().get('spreadsheetId')
SHEETS.spreadsheets().values().update(spreadsheetId=SHEET_ID, range='A1',
body={'values': rows}, valueInputOption='RAW').execute()
Migrate SQL
data to Sheets
goo.gl/N1RPwC
Format cells (frozen & bold row)
requests = [
# freeze row 1
{'updateSheetProperties': {
'properties': {'gridProperties': {'frozenRowCount': 1}},
'fields': 'gridProperties.frozenRowCount',
}},
# bold row 1
{'repeatCell': {
'range': {'startRowIndex': 0, 'endRowIndex': 1},
'cell': {'userEnteredFormat': {'textFormat': {'bold': True}}},
'fields': 'userEnteredFormat.textFormat.bold',
}},
]
SHEETS.spreadsheets().batchUpdate(body={'requests': requests},
spreadsheetId=SHEET_ID, fields='').execute()
Formatting cells
goo.gl/U6Oljn
● API preview (I/O 2016)
● Launched API v1 (Nov 2016)
● Enhancements “v1.1” (Feb 2017)
● Apps Script support (Sep 2017)
Slides
Try our Node.js Markdown-to-Google-Slides generator:
github.com/googlesamples/md2googleslides
Replace text & images from template deck
requests = [
# (global) search-and-replace text
{'replaceAllText': {
'findText': '{{TITLE}}',
'replaceText': 'Hello World!',
}},
# replace text-based image placeholders (global)
{'replaceAllShapesWithImage': {
'imageUrl': IMG_URL, # link to product logo
'replaceMethod': 'CENTER_INSIDE',
'containsText': {'text': '{{LOGO}}'},
}},
]
SLIDES.presentations().batchUpdate(body={'requests': requests},
presentationId=DECK_ID, fields='').execute()
Replacing text
and images
goo.gl/o6EFwk
Proprietary + Confidential
● Settings endpoints launch (Jul 2016)
● New metadata scope (Nov 2016)
● Gmail Add-ons (Oct 2017)
● Admin SDK Email Settings
deprecation (Oct 2018)
Gmail
Display Gmail threads (& # of messages)
# get all "busy" mail threads & display (non-blank) Subject lines
threads = GMAIL.users().threads().list(userId='me',
fields='threads').execute().get('threads', [])
for thread in threads:
tdata = GMAIL.users().threads().get(userId='me', id=thread['id'],
format='metadata', metadataHeaders=['subject'],
fields='messages/payload/headers(name,value)').execute()
if 'messages' not in tdata:
continue
if len(tdata['messages']) > 10:
msg = tdata['messages'][0]['payload']
subject = ''
for header in msg['headers']:
if header['name'] == 'Subject':
subject = header['value']
break
if subject:
print('%2d message(s) in thread: %s' % (
len(tdata['messages']), subject))
Gmail threads
goo.gl/pFYUQ2
Proprietary + Confidential
● Events on Google Maps (Sep 2016)
● Find a time” on iOS (Sep 2016)
● New Calendar iOS app (Nov 2016)
● New Calendar UI (Oct 2017)
● Hangouts Meet integration in
Calendar API (Dec 2017)
Calendar
Create events in Calendar
# define event data, then create event
TIMEZONE = 'America/Los_Angeles'
EVENT = {
'summary': 'Dinner with friends',
'start': {'dateTime': '2017-06-14T19:00:00', 'timeZone': TIMEZONE},
'end': {'dateTime': '2017-06-14T22:00:00', 'timeZone': TIMEZONE},
'attendees': [
{'email': 'friend1@example.com'},
{'email': 'friend2@example.com'},
],
}
GCAL.events().insert(calendarId='primary', body=EVENT,
sendNotifications=True, fields='').execute()
Modifying and
recurring events
goo.gl/J2XkXc
Creating events
goo.gl/KuYMiq
Proprietary + Confidential
● Launched API v1 EAP (NEXT 2018)
○ developers.google.com/docs
Docs
Global search & replace from template doc
requests = [
# (global) search-and-replace text
{'replaceAllText': {
'containsText': {'text': '{{TITLE}}'},
'replaceText': 'Hello World!',
}},
]
DOCS.documents().batchUpdate(body={'requests': requests},
name='documents/'+DOC_ID, fields='').execute()
Google Cloud Platform
● Added support for Standard SQL
2011 (Sep 2016)
● Launched BigQuery Data Transfer
Service (Mar 2017)
BigQuery
Top 10 most common Shakespeare words
TITLE = "The top 10 most common words in all of Shakespeare's works"
QUERY = '''
SELECT LOWER(word) AS word, sum(word_count) AS count
FROM [bigquery-public-data:samples.shakespeare]
GROUP BY word ORDER BY count DESC LIMIT 10
'''
rsp = BQ.query(body={'query': QUERY}, projectId=PROJ_ID).execute()
print('n*** Results for %r:n' % TITLE)
for col in rsp['schema']['fields']: # HEADERS
print(col['name'].upper(), end='t')
print()
for row in rsp['rows']: # DATA
for col in row['f']:
print(col['v'], end='t')
print()
Google Cloud Platform
Top 10 most common Shakespeare words
$ python3 bq_shake.py
*** Results for "The most common words in all of Shakespeare's works":
WORD COUNT
the 29801
and 27529
i 21029
to 20957
of 18514
a 15370
you 14010
my 12936
in 11722
that 11519
Google Cloud Platform
Google Cloud Platform
● API v1 launch (Nov 2016)
● Extended sentiment entity analysis
& language support (May 2017)
● Automatic content classification
(Sep 2017)
● AutoML Natural Language Beta
release (Cloud NEXT 2018)
Cloud
Natural
Language
What is it, and how does it work?
● Cloud Natural Language API
○ ML algorithms: analyze unstructured text
○ Sentiment & Entity analysis: derive structure & meaning of text
○ Extract information about people, places, events, and more
○ cloud.google.com/natural-language
● Natural Language API requests & responses (JSON)
○ Request payload: content, type, language, encoding
○ Sentiment analysis score: -1.0 (negative) to 1.0 (positive) + magnitude
○ Entity analysis: entity types & metadata, salience, and mentions
○ Entity sentiment analysis: combination of above pair
○ Syntactic analysis: extract sentences and tokens from content
○ Content classification: identified content categories
Google Cloud Platform
[simple API/API key sample]
Simple sentiment & classification analysis
text = '''
Google, headquartered in Mountain View, unveiled the new Android
phone at the Consumer Electronics Show. Sundar Pichai said in
his keynote that users love their new Android phones.'''
data = {'type': 'PLAIN_TEXT', 'content': text}
NL = discovery.build('language', 'v1', developerKey=API_KEY)
sentiment = NL.documents().analyzeSentiment(
body={'document': data}).execute().get('documentSentiment')
print('TEXT:', text)
print('nSENTIMENT: score (%s), magnitude (%s)' % (
sentiment['score'], sentiment['magnitude']))
print('nCATEGORIES:')
categories = NL.documents().classifyText(
body={'document': data}).execute().get('categories')
for c in categories:
print ('* %s (%s)' % (c['name'][1:], c['confidence']))
PY
Sentiment & classification analysis output
$ python3 nl_sent_class.py
TEXT: Google, headquartered in Mountain View, unveiled the new Android
phone at the Consumer Electronics Show. Sundar Pichai said in
his keynote that users love their new Android phones.
SENTIMENT: score (0.3), magnitude (0.6)
CATEGORIES:
* Internet & Telecom (0.76)
* Computers & Electronics (0.64)
* News (0.56)
Google Proprietary + Confidential
● YouTube Data API updates & bug
fixes (Nov 2016)
YouTube
[simple API/API key sample]
Search YouTube for videos
from __future__ import print_function
from apiclient import discovery
from settings import API_KEY
QUERY = 'python -snake'
trim = lambda x, ct: ('%s%s' % (x[:ct],
'...' if len(x)>ct else '')).ljust(ct+3)
print('n** Searching for %r videos...' % QUERY)
YOUTUBE = discovery.build('youtube', 'v3', developerKey=API_KEY)
res = YOUTUBE.search().list(q=QUERY, type='video',
part='id,snippet').execute().get('items', [])
for item in res:
print('http://youtu.be/%st%s' % (
trim(item['id']['videoId'], 24),
trim(item['snippet']['title'], 48)))
Can use multiple APIs in same app
Simple, authorized, or a mix of both API types; re-use HTTP client
from __future__ import print_function
from apiclient import discovery
from httplib2 import Http
from oauth2client import file, client, tools
SCOPES = ( # iterable or space-delimited string
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/spreadsheets.readonly',
'https://www.googleapis.com/auth/presentations',
)
store = file.Storage('storage.json')
creds = store.get()
if not creds or creds.invalid:
flow = client.flow_from_clientsecrets('client_secret.json', SCOPES)
creds = tools.run_flow(flow, store)
HTTP = creds.authorize(Http())
DRIVE = discovery.build('drive', 'v3', http=HTTP)
SHEETS = discovery.build('sheets', 'v4', http=HTTP)
SLIDES = discovery.build('slides', 'v1', http=HTTP)
Part IV: Serverless (compute) platforms
Proprietary + Confidential
● Quizzes in Forms support (Apr 2017)
● Google Slides Service and Add-ons
(Sep 2017)
● Gmail Add-ons (Oct 2017)
● Command-line tool (CLASP), new
API, developer dashboard (Jan 2018)
● Sheets service upgrades (Apr 2018)
Apps
Script
(Sheets-bound) “Hello World!”
“Hello World!” results
Apps Script intro
goo.gl/1sXeuD
What can you do with this?
function sendMap() {
var sheet = SpreadsheetApp.getActiveSheet();
var address = sheet.getRange("A2").getValue();
var map = Maps.newStaticMap().addMarker(address);
MailApp.sendEmail(EMAIL, "Map", map.getMapUrl());
}
JS
Accessing maps from
spreadsheets?!?
goo.gl/oAzBN9
More than you think...
...with help from Google Maps and Gmail
Apps Script powers add-ons & integrations
- Extend functionality of G Suite editors
- Embed your app within ours!
- 2014: Google Docs, Sheets, Forms
- 2017 Q3: Google Slides
- 2017 Q4: Gmail
- 2018 Q1: Hangouts Chat bots
Generating Google Slides from images
Generating Google Slides from images
var NAME = "My favorite images";
var deck = SlidesApp.getActivePresentation();
function addImageSlide(link, index) {
var slide = deck.appendSlide(SlidesApp.PredefinedLayout.BLANK);
var image = slide.insertImage(link);
}
function main() {
var images = [
"http://www.google.com/services/images/phone-animation-results_2x.png",
"http://www.google.com/services/images/section-work-card-img_2x.jpg",
"http://gsuite.google.com/img/icons/product-lockup.png",
"http://gsuite.google.com/img/home-hero_2x.jpg",
];
var [title, subtitle] = deck.getSlides()[0].getPageElements();
title.asShape().getText().setText(NAME);
subtitle.asShape().getText().setText("Google Apps ScriptnSlides Service demo");
images.forEach(addImageSlide);
}
Introducing
Slides Add-ons
goo.gl/sYL5AM
Gmail Add-ons
‐ Expense reports
‐ Can't we do them
without leaving Gmail?
‐ On Web AND mobile?
Gmail Add-ons
‐ One place to do your expense report
ExpenseIt! Gmail Add-on
function createExpensesCard(opt_prefills, opt_status) {
var card = CardService.newCardBuilder();
card.setHeader(CardService.newCardHeader().setTitle('Log Your Expense'));
var clearForm = CardService.newAction().setFunctionName('clearForm')
.setParameters({'Status': opt_status ? opt_status : ''});
var clearAction = CardService.newCardAction().setText('Clear form')
.setOnClickAction(clearForm);
card.addCardAction(clearAction);
:
var newSheetSection = CardService.newCardSection();
var sheetName = CardService.newTextInput().setFieldName('Sheet Name')
.setTitle('Sheet Name');
var createExpensesSheet = CardService.newAction()
.setFunctionName('createExpensesSheet');
var newSheetButton = CardService.newTextButton().setText('New Sheet')
.setOnClickAction(createExpensesSheet);
newSheetSection.addWidget(sheetName);
newSheetSection.addWidget(CardService.newButtonSet().addButton(newSheetButton));
card.addSection(formSection);
card.addSection(newSheetSection);
return card;
}
Expediting
expense reports
goo.gl/KUVCDu
Simple vote bot
function onMessage(e) {
return createMessage(e.user.displayName, 0);
}
function onCardClick(e) {
// Create a new vote card when 'NEW' button is clicked.
if (e.action.actionMethodName === 'newvote') {
return createMessage(e.user.displayName, 0);
}
// Updates the card in-place when '+1' or '-1' button is clicked.
var voteCount = +e.action.parameters[0].value;
e.action.actionMethodName === 'upvote' ? ++voteCount : --voteCount;
return createMessage(e.user.displayName, voteCount, true);
}
Vote bot interactive card
function createMessage(voter, voteCount, shouldUpdate) {
var parameters = [{key: 'count', value: voteCount.toString()}];
return {
actionResponse: {
type: shouldUpdate ? 'UPDATE_MESSAGE' : 'NEW_MESSAGE'
},
cards: [{
header: {
title: 'Last vote by ' + voter + '!'
},
sections: [{
widgets: [{
textParagraph: {
text: voteCount + ' votes!'
}
}, {
buttons: [{
textButton: {
text: '+1',
onClick: {
action: {
actionMethodName: 'upvote',
parameters: parameters
}
}
}
}, {
textButton: {
text: '-1',
onClick: {
action: {
actionMethodName: 'downvote',
parameters: parameters
}
}
}
}, {
textButton: {
text: 'NEW',
onClick: {
action: {
actionMethodName: 'newvote'
}
}
}
}]
}]
}]
}]
};
}
Build Hangouts
Chat bots
goo.gl/jt3FqK
Part V: Miscellaneous & wrap-up
● Hangouts Chat launch (Feb 2018)
○ Available to G Suite customers
○ Features bot framework and
API... build chat bots to:
■ Automate workflows
■ Query for information
■ Other heavy-lifting
○ Many development and
hosting options
Hangouts
Chat
Traditional API workflow
Traditional APIs vs. Bot architecture
OAuth2
Bot architecture
Hangouts Chat "echo bot" in Python/Flask
from flask import Flask, request, json
app = Flask(__name__)
@app.route('/', methods=['POST'])
def on_event():
event = request.get_json()
msg = {}
if event['type'] == 'MESSAGE':
text = 'Hi {}. You sent: {}'.format(
event['user']['displayName'], event['message']['text'])
msg = {'text': text}
return json.jsonify(msg)
PY
Hangouts Chat "echo bot" in Node.js/CF4FB
const functions = require('firebase-functions');
exports.echoBot = functions.https.onRequest((req, resp) => {
var msg = {};
if (req.body.type === 'MESSAGE') {
msg = {
text: 'Hi ' + req.body.user.displayName +
'. You sent: ' + req.body.message.text
};
}
return resp.send(msg).end();
});
JS
Other Google APIs & platforms
● Firebase
○ firebase.google.com
● Actions on Google
○ developers.google.com/actions
● YouTube
○ Data, Analytics, and Livestreaming APIs
○ developers.google.com/youtube
● Google Maps
○ Maps, Routes, and Places APIs
○ developers.google.com/maps
References
● Keep up-to-date
○ Blogs: cloud.google.com/blog, developers.googleblog.com,
gsuite-developers.googleblog.com, cloudplatform.googleblog.com
○ Twitters: @wescpy, @GoogleDevs, @GSuiteDevs, @GCPcloud, @GoogleCloud
● Get started today! (introductory hands-on, self-paced codelabs)
○ GCP codelabs: codelabs.developers.google.com/?cat=Cloud
○ G Suite (REST) APIs: g.co/codelabs/gsuite-apis-intro (Drive API)
○ Google Apps Script: g.co/codelabs/apps-script-intro
○ Other G Suite: codelabs.developers.google.com/?cat=Apps
● Read more
○ G Suite dev docs: developers.google.com/gsuite
○ GCP dev docs: cloud.google.com/docs
● NEXT '19 session: Build with All of Google Cloud: G Suite + GCP Interoperability
○ cloud.withgoogle.com/next/sf/sessions?session=DEV212
More references
● API developers console: console.developers.google.com
● GCP developers console: cloud.developers.google.com
● Google APIs Client Libraries: developers.google.com/api-client-library
○ Python: pip{,3} install -U google-api-python-client
○ Special versions for Django, Google App Engine
○ Node.js: npm install googleapis
● Developer video series
○ G Suite Dev Show: goo.gl/JpBQ40
○ Launchpad Online: goo.gl/kFMUa6
● Relevant videos
○ goo.gl/RbyTFD (new Google APIs project setup)
○ goo.gl/KMfbeK (common Python OAuth2 boilerplate code review)
○ goo.gl/ZIgf8k (APIs intro codelab [Drive API])
Thank you!
Questions?
Wesley Chun
Developer Advocate, Google Cloud
@wescpy & @GoogleDevs
twitter.com/{wescpy,googledevs}

Exploring Google (Cloud) APIs with Python & JavaScript

  • 1.
    Exploring Google (Cloud)APIs with Python & JavaScript Wesley Chun Developer Advocate Google Cloud
  • 3.
    G Suite DevShow goo.gl/JpBQ40 About the speaker ● Developer Advocate, Google Cloud ● Mission: enable current & future developers to be successful using Google Cloud and other Google developer tools, APIs, and platforms ● Videos: host of the G Suite Dev Show on YouTube ● Blogs: developers.googleblog.com & gsuite-developers.googleblog.com ● Twitters: @wescpy, @GCPcloud, @GoogleDevs, @GSuiteDevs ● Background ● Software engineer & architect for 20+ years ● One of the original Yahoo!Mail engineers ● Author of bestselling "Core Python" books (corepython.com) ● Teacher and technical instructor since 1983 (all ages) ● Fellow of the Python Software Foundation ● AB Mathematics & CMP Music, UC Berkeley; MSCS UC Santa Barbara
  • 4.
    Agenda ● Introduction ● Gettingstarted with Google API projects ● Google Cloud (HTTP-based) REST APIs ● Serverless (compute) platforms ● Miscellaneous & wrap-up Part I: Introduction
  • 5.
  • 7.
    Part II: GettingStarted with Google API projects
  • 8.
    The first wordon Security Authentication ("authn") vs authorization ("authz") ● authn: you are who you say you are ○ login & passwd ○ handprint authentication ○ retina scan ● authz: okay, you are who you say you are, but can you haz data? ○ OAuth2 - mostly authz, but some authn ○ Mostly about 3rd-party access to data ○ Users must give YOUR app access to THEIR data ○ Most of the time when you see "auth", it refers to authz Developers Console (devconsole)
  • 13.
    OAuth2 scenarios ● Varieson application type (where apps located) ○ Web (server) applications ○ Installed (mobile & desktop computers) apps ○ Service accounts (cloud/server-to-server) ○ Client-side (JavaScript) apps ○ Limited-input devices (game consoles, printers, cameras, etc.) ○ developers.google.com/identity/protocols/OAuth2 ○ TODAY: command-line script == "Installed"
  • 16.
    Google APIs client librariesfor many languages; demos in developers.google.com/ api-client-library Accessing API client libraries ● Python ○ pip install -U google-api-python-client ○ developers.google.com/api-client-library/ python/start/get_started ● JavaScript ("browser"/client-side) ○ Use gapi.client.request ○ developers.google.com/api-client-library/ javascript/start/start-js ● JavaScript (Node.js/server-side) ○ npm install googleapis ○ googleapis.github.io/google-api-nodejs-client
  • 17.
    SIMPLE AUTHORIZED Which do youchoose? Simple API access
  • 18.
    Google OAuth2 process ●Goal: valid access token == 3rd-party API data access ● How OAuth2 works (in general) ○ Step 1: Create OAuth2 credentials in DevConsole ○ Step 2: Send credentials to get access & refresh tokens ○ Step 3: Use access token to make authorized API calls ○ Step 4: Access tokens expire; use refresh to get new one ● developers.google.com/identity/protocols/OAuth2 ● Other APIs will be similar with some differences Authorized API access
  • 19.
    OAuth2 HTTP-based REST APIs1 HTTP 2 Google APIs request-response workflow 1. Application makes request 2. Request received by service 3. Process data, return response 4. Results sent to application a.k.a. client-server model Part III: Google Cloud REST APIs
  • 21.
    Compute Big Data BigQuery Cloud Pub/Sub Cloud Dataproc Cloud Datalab Cloud Pub/Sub Genomics Cloud AI CloudMachine Learning Engine Cloud Vision API Cloud Speech-to-Text Cloud Natural Language API Cloud Translation API Cloud Jobs API Cloud Dataprep Cloud Video Intelligence API Advanced Solutions Lab Compute Engine App Engine Kubernetes Engine GPU Cloud Functions Container- Optimized OS Identity & Security Cloud IAM Cloud Resource Manager Cloud Security Scanner Key Management Service BeyondCorp Data Loss Prevention API Identity-Aware Proxy Security Key Enforcement Cloud AutoML Cloud Text-to-Speech Cloud TPU Dialogflow Enterprise Edition Data Transfer Transfer Appliance Cloud Composer Cloud Security Command Center Networking Virtual Private Cloud Cloud Load Balancing Cloud CDN Dedicated Interconnect Cloud DNS Cloud Network Cloud External IP Addresses Cloud Firewall Rules Cloud Routes Cloud VPN Management Tools Stackdriver Monitoring Logging Error Reporting Trace Debugger Cloud Deployment Manager Cloud Console Cloud Shell Cloud Mobile App Cloud Billing API Cloud APIs Cloud Router Partner Interconnect Cloud Armor Standard Network Tier Premium Network Tier Profiler GCP products and services without a unique icon have a generic hexagon.
  • 22.
    Developer Tools Cloud SDK CloudSource Repositories Maven App Engine Plugin Cloud Tools for IntelliJ Cloud Tools for PowerShell Cloud Tools for Visual Studio Container Registry Cloud Tools for Eclipse Cloud Build API Platform & Ecosystems API Analytics API Monetization Apigee API Platform Apigee Sense Cloud Endpoints Developer Portal Gradle App Engine Plugin IDE plugins Internet of Things Cloud IoT Core Storage & Databases Cloud Storage Cloud Bigtable Cloud Datastore Cloud SQL Cloud Spanner Persistent Disk Cloud Memorystore Cloud Filestore Cloud Test Lab Cloud IoT Edge G Suite APIs
  • 23.
    Proprietary + Confidential ●Launched API v3 (Dec 2015) ○ v2 not deprecated (yet) ● WhatsApp and Google BigQuery integrations (May 2016) ● Team Drives (Jun 2017) Drive Back up your file archives Write your own or see github.com/gsuitedevs/drive-zipextractor (JS)
  • 24.
    Automate photo albums OR List(first 100) files/folders in Google Drive from __future__ import print_function from apiclient import discovery from httplib2 import Http from oauth2client import file, client, tools SCOPES = 'https://www.googleapis.com/auth/drive.readonly.metadata' store = file.Storage('storage.json') creds = store.get() if not creds or creds.invalid: flow = client.flow_from_clientsecrets('client_secret.json', SCOPES) creds = tools.run_flow(flow, store) DRIVE = discovery.build('drive', 'v3', http=creds.authorize(Http())) files = DRIVE.files().list().execute().get('files', []) for f in files: print(f['name'], f['mimeType']) Listing your files goo.gl/ZIgf8k
  • 25.
    Proprietary + Confidential ●Launched API v4 (I/O 2016) ● Support text rotation (Mar 2017) ● Developer metadata (Sep 2017) ● Macro recorder (Apr 2018) Sheets Powerful API: expect UI features available programmatically ● Create new sheets within spreadsheets ● Add data to sheets ● Create new spreadsheets ● Set frozen rows ● Text+number formatting ● Enforce cell validation ● Adjust column sizes ● Apply formulas ● Build pivot tables ● Create charts ● … and more! BEFORE (<= v3) AFTER (>= v4)
  • 26.
    Import/Export: Customized reports,“database,” or both! Try our Node.js customized reporting tool codelab: g.co/codelabs/sheets Migrate SQL data to a Sheet # read SQL data then create new spreadsheet & add rows into it FIELDS = ('ID', 'Customer Name', 'Product Code', 'Units Ordered', 'Unit Price', 'Status') cxn = sqlite3.connect('db.sqlite') cur = cxn.cursor() rows = cur.execute('SELECT * FROM orders').fetchall() cxn.close() rows.insert(0, FIELDS) DATA = {'properties': {'title': 'Customer orders'}} SHEET_ID = SHEETS.spreadsheets().create(body=DATA, fields='spreadsheetId').execute().get('spreadsheetId') SHEETS.spreadsheets().values().update(spreadsheetId=SHEET_ID, range='A1', body={'values': rows}, valueInputOption='RAW').execute() Migrate SQL data to Sheets goo.gl/N1RPwC
  • 27.
    Format cells (frozen& bold row) requests = [ # freeze row 1 {'updateSheetProperties': { 'properties': {'gridProperties': {'frozenRowCount': 1}}, 'fields': 'gridProperties.frozenRowCount', }}, # bold row 1 {'repeatCell': { 'range': {'startRowIndex': 0, 'endRowIndex': 1}, 'cell': {'userEnteredFormat': {'textFormat': {'bold': True}}}, 'fields': 'userEnteredFormat.textFormat.bold', }}, ] SHEETS.spreadsheets().batchUpdate(body={'requests': requests}, spreadsheetId=SHEET_ID, fields='').execute() Formatting cells goo.gl/U6Oljn ● API preview (I/O 2016) ● Launched API v1 (Nov 2016) ● Enhancements “v1.1” (Feb 2017) ● Apps Script support (Sep 2017) Slides
  • 28.
    Try our Node.jsMarkdown-to-Google-Slides generator: github.com/googlesamples/md2googleslides
  • 29.
    Replace text &images from template deck requests = [ # (global) search-and-replace text {'replaceAllText': { 'findText': '{{TITLE}}', 'replaceText': 'Hello World!', }}, # replace text-based image placeholders (global) {'replaceAllShapesWithImage': { 'imageUrl': IMG_URL, # link to product logo 'replaceMethod': 'CENTER_INSIDE', 'containsText': {'text': '{{LOGO}}'}, }}, ] SLIDES.presentations().batchUpdate(body={'requests': requests}, presentationId=DECK_ID, fields='').execute() Replacing text and images goo.gl/o6EFwk Proprietary + Confidential ● Settings endpoints launch (Jul 2016) ● New metadata scope (Nov 2016) ● Gmail Add-ons (Oct 2017) ● Admin SDK Email Settings deprecation (Oct 2018) Gmail
  • 30.
    Display Gmail threads(& # of messages) # get all "busy" mail threads & display (non-blank) Subject lines threads = GMAIL.users().threads().list(userId='me', fields='threads').execute().get('threads', []) for thread in threads: tdata = GMAIL.users().threads().get(userId='me', id=thread['id'], format='metadata', metadataHeaders=['subject'], fields='messages/payload/headers(name,value)').execute() if 'messages' not in tdata: continue if len(tdata['messages']) > 10: msg = tdata['messages'][0]['payload'] subject = '' for header in msg['headers']: if header['name'] == 'Subject': subject = header['value'] break if subject: print('%2d message(s) in thread: %s' % ( len(tdata['messages']), subject)) Gmail threads goo.gl/pFYUQ2 Proprietary + Confidential ● Events on Google Maps (Sep 2016) ● Find a time” on iOS (Sep 2016) ● New Calendar iOS app (Nov 2016) ● New Calendar UI (Oct 2017) ● Hangouts Meet integration in Calendar API (Dec 2017) Calendar
  • 31.
    Create events inCalendar # define event data, then create event TIMEZONE = 'America/Los_Angeles' EVENT = { 'summary': 'Dinner with friends', 'start': {'dateTime': '2017-06-14T19:00:00', 'timeZone': TIMEZONE}, 'end': {'dateTime': '2017-06-14T22:00:00', 'timeZone': TIMEZONE}, 'attendees': [ {'email': '[email protected]'}, {'email': '[email protected]'}, ], } GCAL.events().insert(calendarId='primary', body=EVENT, sendNotifications=True, fields='').execute() Modifying and recurring events goo.gl/J2XkXc Creating events goo.gl/KuYMiq Proprietary + Confidential ● Launched API v1 EAP (NEXT 2018) ○ developers.google.com/docs Docs
  • 32.
    Global search &replace from template doc requests = [ # (global) search-and-replace text {'replaceAllText': { 'containsText': {'text': '{{TITLE}}'}, 'replaceText': 'Hello World!', }}, ] DOCS.documents().batchUpdate(body={'requests': requests}, name='documents/'+DOC_ID, fields='').execute() Google Cloud Platform ● Added support for Standard SQL 2011 (Sep 2016) ● Launched BigQuery Data Transfer Service (Mar 2017) BigQuery
  • 33.
    Top 10 mostcommon Shakespeare words TITLE = "The top 10 most common words in all of Shakespeare's works" QUERY = ''' SELECT LOWER(word) AS word, sum(word_count) AS count FROM [bigquery-public-data:samples.shakespeare] GROUP BY word ORDER BY count DESC LIMIT 10 ''' rsp = BQ.query(body={'query': QUERY}, projectId=PROJ_ID).execute() print('n*** Results for %r:n' % TITLE) for col in rsp['schema']['fields']: # HEADERS print(col['name'].upper(), end='t') print() for row in rsp['rows']: # DATA for col in row['f']: print(col['v'], end='t') print() Google Cloud Platform Top 10 most common Shakespeare words $ python3 bq_shake.py *** Results for "The most common words in all of Shakespeare's works": WORD COUNT the 29801 and 27529 i 21029 to 20957 of 18514 a 15370 you 14010 my 12936 in 11722 that 11519 Google Cloud Platform
  • 34.
    Google Cloud Platform ●API v1 launch (Nov 2016) ● Extended sentiment entity analysis & language support (May 2017) ● Automatic content classification (Sep 2017) ● AutoML Natural Language Beta release (Cloud NEXT 2018) Cloud Natural Language What is it, and how does it work? ● Cloud Natural Language API ○ ML algorithms: analyze unstructured text ○ Sentiment & Entity analysis: derive structure & meaning of text ○ Extract information about people, places, events, and more ○ cloud.google.com/natural-language ● Natural Language API requests & responses (JSON) ○ Request payload: content, type, language, encoding ○ Sentiment analysis score: -1.0 (negative) to 1.0 (positive) + magnitude ○ Entity analysis: entity types & metadata, salience, and mentions ○ Entity sentiment analysis: combination of above pair ○ Syntactic analysis: extract sentences and tokens from content ○ Content classification: identified content categories Google Cloud Platform
  • 35.
    [simple API/API keysample] Simple sentiment & classification analysis text = ''' Google, headquartered in Mountain View, unveiled the new Android phone at the Consumer Electronics Show. Sundar Pichai said in his keynote that users love their new Android phones.''' data = {'type': 'PLAIN_TEXT', 'content': text} NL = discovery.build('language', 'v1', developerKey=API_KEY) sentiment = NL.documents().analyzeSentiment( body={'document': data}).execute().get('documentSentiment') print('TEXT:', text) print('nSENTIMENT: score (%s), magnitude (%s)' % ( sentiment['score'], sentiment['magnitude'])) print('nCATEGORIES:') categories = NL.documents().classifyText( body={'document': data}).execute().get('categories') for c in categories: print ('* %s (%s)' % (c['name'][1:], c['confidence'])) PY Sentiment & classification analysis output $ python3 nl_sent_class.py TEXT: Google, headquartered in Mountain View, unveiled the new Android phone at the Consumer Electronics Show. Sundar Pichai said in his keynote that users love their new Android phones. SENTIMENT: score (0.3), magnitude (0.6) CATEGORIES: * Internet & Telecom (0.76) * Computers & Electronics (0.64) * News (0.56)
  • 36.
    Google Proprietary +Confidential ● YouTube Data API updates & bug fixes (Nov 2016) YouTube [simple API/API key sample] Search YouTube for videos from __future__ import print_function from apiclient import discovery from settings import API_KEY QUERY = 'python -snake' trim = lambda x, ct: ('%s%s' % (x[:ct], '...' if len(x)>ct else '')).ljust(ct+3) print('n** Searching for %r videos...' % QUERY) YOUTUBE = discovery.build('youtube', 'v3', developerKey=API_KEY) res = YOUTUBE.search().list(q=QUERY, type='video', part='id,snippet').execute().get('items', []) for item in res: print('http://youtu.be/%st%s' % ( trim(item['id']['videoId'], 24), trim(item['snippet']['title'], 48)))
  • 37.
    Can use multipleAPIs in same app Simple, authorized, or a mix of both API types; re-use HTTP client from __future__ import print_function from apiclient import discovery from httplib2 import Http from oauth2client import file, client, tools SCOPES = ( # iterable or space-delimited string 'https://www.googleapis.com/auth/drive', 'https://www.googleapis.com/auth/spreadsheets.readonly', 'https://www.googleapis.com/auth/presentations', ) store = file.Storage('storage.json') creds = store.get() if not creds or creds.invalid: flow = client.flow_from_clientsecrets('client_secret.json', SCOPES) creds = tools.run_flow(flow, store) HTTP = creds.authorize(Http()) DRIVE = discovery.build('drive', 'v3', http=HTTP) SHEETS = discovery.build('sheets', 'v4', http=HTTP) SLIDES = discovery.build('slides', 'v1', http=HTTP) Part IV: Serverless (compute) platforms
  • 38.
    Proprietary + Confidential ●Quizzes in Forms support (Apr 2017) ● Google Slides Service and Add-ons (Sep 2017) ● Gmail Add-ons (Oct 2017) ● Command-line tool (CLASP), new API, developer dashboard (Jan 2018) ● Sheets service upgrades (Apr 2018) Apps Script (Sheets-bound) “Hello World!”
  • 39.
    “Hello World!” results AppsScript intro goo.gl/1sXeuD What can you do with this?
  • 40.
    function sendMap() { varsheet = SpreadsheetApp.getActiveSheet(); var address = sheet.getRange("A2").getValue(); var map = Maps.newStaticMap().addMarker(address); MailApp.sendEmail(EMAIL, "Map", map.getMapUrl()); } JS Accessing maps from spreadsheets?!? goo.gl/oAzBN9 More than you think... ...with help from Google Maps and Gmail
  • 41.
    Apps Script powersadd-ons & integrations - Extend functionality of G Suite editors - Embed your app within ours! - 2014: Google Docs, Sheets, Forms - 2017 Q3: Google Slides - 2017 Q4: Gmail - 2018 Q1: Hangouts Chat bots Generating Google Slides from images
  • 42.
    Generating Google Slidesfrom images var NAME = "My favorite images"; var deck = SlidesApp.getActivePresentation(); function addImageSlide(link, index) { var slide = deck.appendSlide(SlidesApp.PredefinedLayout.BLANK); var image = slide.insertImage(link); } function main() { var images = [ "http://www.google.com/services/images/phone-animation-results_2x.png", "http://www.google.com/services/images/section-work-card-img_2x.jpg", "http://gsuite.google.com/img/icons/product-lockup.png", "http://gsuite.google.com/img/home-hero_2x.jpg", ]; var [title, subtitle] = deck.getSlides()[0].getPageElements(); title.asShape().getText().setText(NAME); subtitle.asShape().getText().setText("Google Apps ScriptnSlides Service demo"); images.forEach(addImageSlide); } Introducing Slides Add-ons goo.gl/sYL5AM Gmail Add-ons ‐ Expense reports ‐ Can't we do them without leaving Gmail? ‐ On Web AND mobile?
  • 43.
    Gmail Add-ons ‐ Oneplace to do your expense report ExpenseIt! Gmail Add-on function createExpensesCard(opt_prefills, opt_status) { var card = CardService.newCardBuilder(); card.setHeader(CardService.newCardHeader().setTitle('Log Your Expense')); var clearForm = CardService.newAction().setFunctionName('clearForm') .setParameters({'Status': opt_status ? opt_status : ''}); var clearAction = CardService.newCardAction().setText('Clear form') .setOnClickAction(clearForm); card.addCardAction(clearAction); : var newSheetSection = CardService.newCardSection(); var sheetName = CardService.newTextInput().setFieldName('Sheet Name') .setTitle('Sheet Name'); var createExpensesSheet = CardService.newAction() .setFunctionName('createExpensesSheet'); var newSheetButton = CardService.newTextButton().setText('New Sheet') .setOnClickAction(createExpensesSheet); newSheetSection.addWidget(sheetName); newSheetSection.addWidget(CardService.newButtonSet().addButton(newSheetButton)); card.addSection(formSection); card.addSection(newSheetSection); return card; } Expediting expense reports goo.gl/KUVCDu
  • 44.
    Simple vote bot functiononMessage(e) { return createMessage(e.user.displayName, 0); } function onCardClick(e) { // Create a new vote card when 'NEW' button is clicked. if (e.action.actionMethodName === 'newvote') { return createMessage(e.user.displayName, 0); } // Updates the card in-place when '+1' or '-1' button is clicked. var voteCount = +e.action.parameters[0].value; e.action.actionMethodName === 'upvote' ? ++voteCount : --voteCount; return createMessage(e.user.displayName, voteCount, true); } Vote bot interactive card function createMessage(voter, voteCount, shouldUpdate) { var parameters = [{key: 'count', value: voteCount.toString()}]; return { actionResponse: { type: shouldUpdate ? 'UPDATE_MESSAGE' : 'NEW_MESSAGE' }, cards: [{ header: { title: 'Last vote by ' + voter + '!' }, sections: [{ widgets: [{ textParagraph: { text: voteCount + ' votes!' } }, { buttons: [{ textButton: { text: '+1', onClick: { action: { actionMethodName: 'upvote', parameters: parameters } } } }, { textButton: { text: '-1', onClick: { action: { actionMethodName: 'downvote', parameters: parameters } } } }, { textButton: { text: 'NEW', onClick: { action: { actionMethodName: 'newvote' } } } }] }] }] }] }; } Build Hangouts Chat bots goo.gl/jt3FqK
  • 45.
    Part V: Miscellaneous& wrap-up ● Hangouts Chat launch (Feb 2018) ○ Available to G Suite customers ○ Features bot framework and API... build chat bots to: ■ Automate workflows ■ Query for information ■ Other heavy-lifting ○ Many development and hosting options Hangouts Chat
  • 46.
    Traditional API workflow TraditionalAPIs vs. Bot architecture OAuth2 Bot architecture Hangouts Chat "echo bot" in Python/Flask from flask import Flask, request, json app = Flask(__name__) @app.route('/', methods=['POST']) def on_event(): event = request.get_json() msg = {} if event['type'] == 'MESSAGE': text = 'Hi {}. You sent: {}'.format( event['user']['displayName'], event['message']['text']) msg = {'text': text} return json.jsonify(msg) PY
  • 47.
    Hangouts Chat "echobot" in Node.js/CF4FB const functions = require('firebase-functions'); exports.echoBot = functions.https.onRequest((req, resp) => { var msg = {}; if (req.body.type === 'MESSAGE') { msg = { text: 'Hi ' + req.body.user.displayName + '. You sent: ' + req.body.message.text }; } return resp.send(msg).end(); }); JS Other Google APIs & platforms ● Firebase ○ firebase.google.com ● Actions on Google ○ developers.google.com/actions ● YouTube ○ Data, Analytics, and Livestreaming APIs ○ developers.google.com/youtube ● Google Maps ○ Maps, Routes, and Places APIs ○ developers.google.com/maps
  • 48.
    References ● Keep up-to-date ○Blogs: cloud.google.com/blog, developers.googleblog.com, gsuite-developers.googleblog.com, cloudplatform.googleblog.com ○ Twitters: @wescpy, @GoogleDevs, @GSuiteDevs, @GCPcloud, @GoogleCloud ● Get started today! (introductory hands-on, self-paced codelabs) ○ GCP codelabs: codelabs.developers.google.com/?cat=Cloud ○ G Suite (REST) APIs: g.co/codelabs/gsuite-apis-intro (Drive API) ○ Google Apps Script: g.co/codelabs/apps-script-intro ○ Other G Suite: codelabs.developers.google.com/?cat=Apps ● Read more ○ G Suite dev docs: developers.google.com/gsuite ○ GCP dev docs: cloud.google.com/docs ● NEXT '19 session: Build with All of Google Cloud: G Suite + GCP Interoperability ○ cloud.withgoogle.com/next/sf/sessions?session=DEV212 More references ● API developers console: console.developers.google.com ● GCP developers console: cloud.developers.google.com ● Google APIs Client Libraries: developers.google.com/api-client-library ○ Python: pip{,3} install -U google-api-python-client ○ Special versions for Django, Google App Engine ○ Node.js: npm install googleapis ● Developer video series ○ G Suite Dev Show: goo.gl/JpBQ40 ○ Launchpad Online: goo.gl/kFMUa6 ● Relevant videos ○ goo.gl/RbyTFD (new Google APIs project setup) ○ goo.gl/KMfbeK (common Python OAuth2 boilerplate code review) ○ goo.gl/ZIgf8k (APIs intro codelab [Drive API])
  • 49.
    Thank you! Questions? Wesley Chun DeveloperAdvocate, Google Cloud @wescpy & @GoogleDevs twitter.com/{wescpy,googledevs}