tag:blogger.com,1999:blog-69400433120154608112026-04-15T01:10:51.091-07:00Core Python Programmingwescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.comBlogger25125tag:blogger.com,1999:blog-6940043312015460811.post-28917929174430800082018-05-09T17:00:00.000-07:002018-05-16T01:10:56.742-07:00Creating Hangouts Chat bots with Python<b>NOTE:</b> The code featured here is also available as a <a href="http://goo.gl/jt3FqK">video + overview post</a> as part of <a href="http://goo.gl/JpBQ40">this developers series</a> from Google.<br />
<br />
<h2>
Introduction</h2>
Earlier today at <a href="http://google.com/io">Google I/O</a>, the <a href="http://gsuite.google.com/products/chat">Hangouts Chat</a> team (including yours truly) delivered a <a href="https://events.google.com/io/schedule/?section=may-9&sid=d0330c4e-a5bf-444e-a5a3-9eca8fad4def" target="_blank">featured talk</a> on the bot framework for Hangouts Chat (talk video <a href="http://youtu.be/qBdG6cwnWps" target="_blank">here</a> [~40 mins]). If you missed it several months ago, Google <a href="http://goo.gl/JV55m3">launched</a> the new Hangouts Chat application for <a href="http://gsuite.google.com/pricing.html">G Suite customers</a> (but not consumer Gmail users at this time). This next-generation collaboration platform has several key features not available in "classic Hangouts," including the ability to create chat rooms, search, and the ability to attach files from Google Drive. However for developers, the biggest new feature is a bot framework and API allowing developers to create bots that interact with users inside "spaces," i.e., chat rooms or direct messages (DMs).<br />
<br />
Before getting started with bots, let's review typical API usage where your app must be authorized for data access, i.e., OAuth2. Once permission is granted, your app calls the API, the API services the request and responds back with the desired results, as illustrated here:<br />
<blockquote class="tr_bq" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgPutsOrhGQlmES-kyeq6Ls02SYGiK_Ks6IHB_d-lLZJtvRrK4R6bFTASIfy3HG5IVnOPYOanC-WvDhuwy2WA_gBfSjcsqF0Xt57KwP0jsbfYRjOF-JyOMDceTGBZN9E7vtOn0omWZNsOY/s1600/api-flow.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1550" data-original-width="1038" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgPutsOrhGQlmES-kyeq6Ls02SYGiK_Ks6IHB_d-lLZJtvRrK4R6bFTASIfy3HG5IVnOPYOanC-WvDhuwy2WA_gBfSjcsqF0Xt57KwP0jsbfYRjOF-JyOMDceTGBZN9E7vtOn0omWZNsOY/s320/api-flow.png" width="214" /></a></blockquote>
<br />
Bot architecture is quite different. With bots, recognize that requests come from users in chat rooms or DMs. Users direct messages towards a bot, then Hangouts Chat relays those requests to the bot. The bot performs all the necessary processing (possibly calling other APIs and services), collates the response, and returns it to Chat, which in turn, displays the results to users. Here's the summarized bot workflow:<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgM1FmM8frjbYXtuk26OcPxpNoBeKhyw_xItAl_nk8CyoLLVo3BV9juq-UILCpqlPqePFdMzP9dJuNXzALa4y-3hcnYrUSxTesJt1DeGf88Uv111kJCOZNAIIsPvf1qw3oLb7RBcgI5RcQ/s1600/bot-flow-sync.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1009" data-original-width="1600" height="201" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgM1FmM8frjbYXtuk26OcPxpNoBeKhyw_xItAl_nk8CyoLLVo3BV9juq-UILCpqlPqePFdMzP9dJuNXzALa4y-3hcnYrUSxTesJt1DeGf88Uv111kJCOZNAIIsPvf1qw3oLb7RBcgI5RcQ/s320/bot-flow-sync.png" width="320" /></a></div>
<br />
A key takeaway from this discussion is that normally in your apps, you call APIs. For bots, Hangouts Chat calls <i>you</i>. For this reason, the service is extremely flexible for developers: you can create bots using pretty much any language (not just Python), using your stack of choice, and hosted on any public or private cloud. The only requirement is that as long as Hangouts Chat can HTTP POST to your bot, you're good to go.<br />
<br />
Furthermore, if you think there's something missing in the diagram because you don't see OAuth2 nor API calls, you'd also be correct. Look for both in a future post where I focus on <i>asynchronous</i> responses from Hangouts Chat bots.<br />
<br />
<h2>
Event types & bot processing</h2>
So what does the payload look like when your bot receives a call from Hangouts Chat? The first thing your bot needs to do is to determine the type of event received. It can then process the request based on that event type, and finally, return an appropriate JSON payload back to Hangouts Chat to render within the space. There are currently four event types in Hangouts Chat:<br />
<ol>
<li> <code>ADDED_TO_SPACE</code></li>
<li> <code>REMOVED_FROM_SPACE</code></li>
<li> <code>MESSAGE</code></li>
<li> <code>CARD_CLICKED</code></li>
</ol>
The first pair are for when a bot is added to or removed from a space. In the first case, the bot would likely send a welcome message like, "Thanks for adding me to this room," and perhaps give some instructions on how to communicate with the bot. When a bot is removed from a space, it can no longer communicate with chat room participants, so there's no response message sent in this case; the most likely action here would be to log that the bot was removed from the space.<br />
<br />
The 3rd message type is likely the most common scenario, where a bot has been added to a room, and now a human user is sending it a request. The other common type would be the last one. Rather than a message, this event occurs when users click on a UI card element. The bot's job here is to invoke the "callback" associated with the card element clicked. A number of things can happen here: the bot can return a text message (or nothing at all), a UI card can be updated, or a new card can be returned.<br />
<br />
All of what we described above is realized in the pseudocode below (whether you choose to implement in Python or any other supported language) and helpful in preparing you to review <a href="http://developers.google.com/hangouts">the official documentation</a>:
<br />
<pre><b>def</b> process_event(req, rsp):
event = json.loads(req['body']) # event received
<b>if</b> event['type'] == 'REMOVED_FROM_SPACE':
# no response as bot removed from room
<b>return</b>
<b>elif</b> event['type'] == 'ADDED_TO_SPACE':
# bot added to room; send welcome message
msg = {'text': 'Thanks for adding me to this room!'}
<b>elif</b> event['type'] == 'MESSAGE':
# message received during normal operations
msg = respond_to_message(event['message']['text'])
<b>elif</b> event['type'] == 'CARD_CLICKED':
# result from user-click on card UI
action = event['action']
msg = respond_to_click(
action['actionMethodName'], action['parameters'])
<b>else</b>:
<b>return</b>
rsp.send(json.dumps(msg))
</pre>
Regardless of implementation language, you still need to make the necessary tweaks to run on the hosting platform you choose. Our example uses Google App Engine (GAE) for hosting; below you'll see the obvious similarities between our actual bot app and this pseudocode.
<br />
<br />
<h2>
Polling chat room users for votes</h2>
For a basic voting bot, we need a vote counter, buttons for users to upvote or downvote, and perhaps some reset or "new vote" button. To implement it, we need to unpack the inbound JSON object, determine the event type, then process each event type with actions like this:
<br />
<ol>
<li> <code>ADDED_TO_SPACE</code>
— ignore.. we don't do anything unless asked by users</li>
<li> <code>REMOVED_FROM_SPACE</code>
— no action (bot removed)</li>
<li> <code>MESSAGE</code>
— start new vote</li>
<li> <code>CARD_CLICKED</code>
— process "upvote", "downvote", or "newvote" request</li>
</ol>
Here's a handler implementation for Python App Engine using its <code>webapp2</code> micro framework:
<br />
<pre><span style="font-size: small;"><b>class</b> VoteBot(webapp2.RequestHandler):
<b>def</b> post(self):
event = json.loads(self.request.body)
user = event['user']['displayName']
<b>if</b> event['type'] == 'CARD_CLICKED':
method = event['action']['actionMethodName']
<b>if</b> method == 'newvote':
body = create_message(user)
<b>else</b>:
delta = 1 <b>if</b> method == 'upvote' <b>else</b> -1
vote_count = int(event['action']['parameters'][0]['value']) + delta
body = create_message(user, vote_count, <b>True</b>)
<b>elif</b> event['type'] == 'MESSAGE':
body = create_message(user)
<b>else</b>: # no response for ADDED_TO_SPACE or REMOVED_FROM_SPACE
<b>return</b>
self.response.headers['Content-Type'] = 'application/json'
self.response.out.write(json.dumps(body)) </span></pre>
If you've never written an app using <code>webapp2</code>, don't fret, as this example is easily portable to Flask or other web framework. Why didn't I write it with Flask to begin with? Couple of reasons: 1) the <code>webapp2</code> framework comes native with App Engine... no need to go download/install it, and 2) because it's native, I don't need to deploy any other files other than <code>bot.py</code> and its <code>app.yaml</code> config file whereas with Flask, you're uploading another ~1300 files in addition this pair. (More on this towards the end of this post.)<br />
<br />
Compare this real app to the pseudocode... similar, right? Our bot is a bit more complex in that it renders UI cards, so the <code>create_message()</code> function will need to do more than just return a plain text response. Instead, it must generate and return the JSON markup rendering the card in the Hangouts Chat UI:<br />
<pre><span style="font-size: small;"><b>def</b> create_message(voter, vote_count=0, should_update=<b>False</b>):
PARAMETERS = [{'key': 'count', 'value': str(vote_count)}]
<b>return</b> {
'actionResponse': {
'type': 'UPDATE_MESSAGE' <b>if</b> should_update <b>els</b>e 'NEW_MESSAGE'
},
'cards': [{
'header': {'title': 'Last vote by %s!' % voter},
'sections': [{
'widgets': [{
'textParagraph': {'text': '%d votes!' % vote_count}
}, {
'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',
}
}
}
}]
}]
}]
}]
}
</span></pre>
<h2>
Finishing touches</h2>
The last thing App Engine needs is an <code>app.yaml</code> configuration file:
<br />
<pre>runtime: python27
api_version: 1
threadsafe: true
handlers:
- url: /.*
script: bot.app
</pre>
If you've deployed apps to GAE before, you know you need to create a project in the <a href="http://console.cloud.google.com/">Google Cloud Developers Console</a>. You also need to have a project for bots, however you can use the same project for both your use of the <a href="https://console.cloud.google.com/apis/library/chat.googleapis.com">Hangouts Chat bot framework</a> (link only works for G Suite customers) as well as hosting your App Engine-based bot.<br />
<br />
Once you've deployed your bot to GAE, go to the <a href="https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat">Hangouts Chat API configuration tab</a>, and add the HTTP endpoint for your App Engine-based bot to the Conenctions Settings section:<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgTSZFkUhmvzYq-CowbGwXTPMyggc5eKROjPZzdIQnmZNwuBqy-CIr_rlgafQ7neEKn_L4ULBqbt-fSUoi-aYu5HMMd3owRzh7twlJ-BE9L3kG99G7xZZMdOSWXe7psbCae_HjfSDzkgfI/s1600/bot-cxn-settings.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="900" data-original-width="854" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgTSZFkUhmvzYq-CowbGwXTPMyggc5eKROjPZzdIQnmZNwuBqy-CIr_rlgafQ7neEKn_L4ULBqbt-fSUoi-aYu5HMMd3owRzh7twlJ-BE9L3kG99G7xZZMdOSWXe7psbCae_HjfSDzkgfI/s320/bot-cxn-settings.png" width="303" /></a></div>
<br />
<div>
As you can see, there are several options here for where Hangouts Chat can post messages destined for bots: standard HTTPS bots like our App Engine example, <a href="http://developers.google.com/apps-script">Google Apps Script</a> (a customized JavaScript-in-the-cloud platform with built-in G Suite integration [<a href="http://goo.gl/1sXeuD">intro video</a>]), or <a href="http://cloud.google.com/pubsub">Google Cloud Pub/Sub</a>. Pub/Sub is the message queue proxy for when your bot is hosted on-premise behind a firewall, requiring you to register a pull subscription to retrieve bot messages from Hangouts Chat.</div>
<div>
<br /></div>
<div>
Once you've published your bot, add the bot to a room or @mention it in a DM. Send it a message, and it returns an interactive vote card like what you see below. Cast some votes or create a new vote to test drive your new bot.</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjRnM9xHdcqA1J1cwv4bI-4X77G3udq-FQ-RB7hGLkeeVwDwB0pheF7ocDxzzEpfNDaKOuuXmvNyIhHWgxavjXl5Z5UlWnenhlbfM2zBQ7qU0sMe5UcGJILJJYsZMVl8cdPJldFE_34Tfw/s1600/vote-bot-TO.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="570" data-original-width="1166" height="195" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjRnM9xHdcqA1J1cwv4bI-4X77G3udq-FQ-RB7hGLkeeVwDwB0pheF7ocDxzzEpfNDaKOuuXmvNyIhHWgxavjXl5Z5UlWnenhlbfM2zBQ7qU0sMe5UcGJILJJYsZMVl8cdPJldFE_34Tfw/s400/vote-bot-TO.png" width="400" /></a></div>
<div>
<br /></div>
<h2>
Conclusion</h2>
Congrats for creating your first Python bot! Below is the entire <code>bot.py</code> file for your convenience. (At this time, Python App Engine <i>standard</i> only supports Python 2, so if you want to run Python 3 instead, use the <a href="https://cloud.google.com/appengine/docs/flexible/python/runtime">Python App Engine <i>flexible</i> environment</a>.) Also don't forget the <code>app.yaml</code> configuration file above.<br />
<pre><span style="font-size: small;"><b>import</b> json
<b>import</b> webapp2
<b>def</b> create_message(voter, vote_count=0, should_update=<b>False</b>):
PARAMETERS = [{'key': 'count', 'value': str(vote_count)}]
<b>return</b> {
'actionResponse': {
'type': 'UPDATE_MESSAGE' <b>if</b> should_update <b>else</b> 'NEW_MESSAGE'
},
'cards': [{
'header': {'title': 'Last vote by %s!' % voter},
'sections': [{
'widgets': [{
'textParagraph': {'text': '%d votes!' % vote_count}
}, {
'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',
}
}
}
}]
}]
}]
}]
}
<b>class</b> VoteBot(webapp2.RequestHandler):
<b>def</b> post(self):
event = json.loads(self.request.body)
user = event['user']['displayName']
<b>if</b> event['type'] == 'CARD_CLICKED':
method = event['action']['actionMethodName']
<b>if</b> method == 'newvote':
body = create_message(user)
<b>else</b>:
delta = 1 <b>if</b> method == 'upvote' <b>else</b> -1
vote_count = int(event['action']['parameters'][0]['value']) + delta
body = create_message(user, vote_count, <b>True</b>)
<b>elif</b> event['type'] == 'MESSAGE':
body = create_message(user)
<b>else</b>: # no response for ADDED_TO_SPACE or REMOVED_FROM_SPACE
<b>return</b>
self.response.headers['Content-Type'] = 'application/json'
self.response.out.write(json.dumps(body))
app = webapp2.WSGIApplication([
('/', VoteBot),
], debug=<b>True</b>)
</span></pre>
Yep, the entire bot is made up of just these 2 files. While porting to Flask is fairly straightforward and Flask apps are supported by App Engine, <a href="https://cloud.google.com/appengine/docs/standard/python/getting-started/python-standard-env">Flask itself doesn't come with App Engine</a> (quickstart <a href="https://cloud.google.com/python/getting-started/hello-world">here</a>), so you'll need to upload all of Flask (in addition to those 2 files) to host a Flask version of your bot on GAE.<br />
<br />
Both files are also available from this app's <a href="https://github.com/gsuitedevs/hangouts-chat-samples/tree/master/python/vote-text-bot" target="_blank">GitHub repo</a> which you can fork. I'm happy to entertain PRs for any bugs you find or ways we can simplify this code even more. Below are some additional resources for learning about Hangouts Chat bots and GAE:
<br />
<ul>
<li><a href="http://developers.google.com/hangouts/chat/concepts/bots" target="_blank">Hangouts Chat bots concepts page</a></li>
<li> <a href="http://developers.google.com/hangouts/chat/how-tos/bots-develop" target="_blank">Creating new Hangouts Chat bots guide</a></li>
<li> <a href="http://developers.google.com/hangouts/chat/how-tos/cards-onclick" target="_blank">Hangouts Chat guide to interactive cards</a></li>
<li> <a href="http://developers.google.com/hangouts/chat/samples" target="_blank">Other Hangouts Chat sample bots</a></li>
<li> <a href="http://developers.google.com/hangouts/chat" target="_blank">Official Hangouts Chat developer documentation</a>—everything you need to know and more!</li>
<li> <a href="http://cloud.google.com/appengine/docs/standard/python" target="_blank">Python App Engine (standard) documentation</a></li>
<li> <a href="http://cloud.google.com/appengine" target="_blank">Google App Engine product information</a></li>
</ul>
Python's a great language to implement bots with because not only can you create something quickly, but choose any platform to host it on, Google App Engine, Amazon EC2/Google Compute Engine, or any cloud that runs Python apps.<br />
<br />
<h2>
Epilogue: code challenge</h2>
While usable, this vote bot can be significantly improved. Once you get the basic bot working, I recommend the following enhancements as exercises for the reader:<br />
<ol>
<li> Support vote topics: users starting new votes must state topic in bot message; use as card header</li>
<li> Add images (like the Node.js version of our <a href="http://developers.google.com/hangouts/chat/how-tos/cards-onclick">vote bot in the docs</a> hosted on <a href="http://cloud.google.com/functions">Google Cloud Functions</a>)</li>
<li> Track users who have voted (you decide on implementation)</li>
<li> Don't let the vote count to go below zero</li>
<li> Allow downvotes only from users who have at least one upvote</li>
<li> Port your working bot from <code>webapp2</code> to Flask</li>
</ol>
<!--[if gte mso 9]><xml>
<w:LatentStyles DefLockedState="false" LatentStyleCount="156">
</w:LatentStyles>
</xml><![endif]--><!--[if gte mso 10]>
<style>
/* Style Definitions */
table.MsoNormalTable
{mso-style-name:"Table Normal";
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-parent:"";
mso-padding-alt:0in 5.4pt 0in 5.4pt;
mso-para-margin:0in;
mso-para-margin-bottom:.0001pt;
mso-pagination:widow-orphan;
font-size:10.0pt;
font-family:"Times New Roman";
mso-ansi-language:#0400;
mso-fareast-language:#0400;
mso-bidi-language:#0400;}
</style>
<![endif]-->wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com0tag:blogger.com,1999:blog-6940043312015460811.post-64108726747417079062017-06-26T11:36:00.000-07:002017-12-15T16:26:42.021-08:00Modifying events with the Google Calendar API<b>NOTE:</b> The code featured here is also available as a <a href="http://goo.gl/Dy584h">video + overview post</a> as part of <a href="http://goo.gl/JpBQ40">this developers series</a> from Google.<br />
<br />
<h2>
Introduction</h2>
In <a href="http://wescpy.blogspot.com/2015/09/creating-events-in-google-calendar.html">an earlier post</a>, I introduced Python developers to adding events to users' calendars using the Google Calendar API. However, while
being able to insert events is "interesting," it's only half the picture. If you want to give your users a more
complete experience, modifying those events is a must-have. In this post, you'll learn how to modify existing
events, and as a bonus, learn how to implement repeating events too.
<br />
<br />
In order to modify events, we need the
full Calendar API scope:
<br />
<ul>
<li><code>'https://www.googleapis.com/auth/calendar'</code>—read-write access to Calendar</li>
</ul>
Skipping the OAuth2 boilerplate, once you have valid authorization credentials, create a service endpoint to the Calendar API like this:
<br />
<pre>GCAL = discovery.build('calendar', 'v3',
http=creds.authorize(Http()))</pre>
Now you can send the API requests using this endpoint.
<br />
<br />
<h2>
Using the Google Calendar API</h2>
Our sample script requires an existing Google Calendar event, so either create one programmatically with <code>events().insert()</code> & save its ID as we showed you in that earlier post, or use <code>events().list()</code> or <code>events().get()</code> to get the ID of an existing event.
<br />
<br />
While you can use an offset from GMT/UTC, such as the <code>GMT_OFF</code> variable from the event insert post, today's
code sample "upgrades" to a more general <a href="http://iana.org/time-zones">IANA timezone</a> solution. For Pacific Time, it's <code>"America/Los_Angeles"</code>. The reason for this change is to allow events that survive across Daylight Savings Time shifts. IOW, a dinner at 7PM/1900 stays at 7PM as we cross fall and spring boundaries. This is especially important for events that repeat throughout the year. Yes, we <b>are</b> discussing recurrence in this post too, so it's particularly relevant.
<br />
<br />
<h2>
Modifying calendar events</h2>
In the <a href="http://wescpy.blogspot.com/2015/09/creating-events-in-google-calendar.html">other post</a>, the <code>EVENT</code> body constitutes an "event record" containing the information necessary to create a calendar entry—it consists of the event name, start & end times, and invitees. That record is an API <i>resource</i> which you created/accessed with the Calendar API via <code>events().insert()</code>. (What do you think the "R" in "URL" stands for anyway?!?) The Calendar API adheres to RESTful semantics in that the HTTP verbs match the actions you perform against a resource.<br />
<br />
In today's scenario, let's assume that dinner from the other post didn't work out, but that you want to reschedule it. Furthermore, not only do you want to make that dinner happen again, but because you're good friends, you've made a commitment to do dinner every other month for the rest of the year, then see where things stand. Now that we know what we want, we have a choice.
<br />
<br />
There are two ways to modifying existing events in Calendar:
<br />
<ol>
<li> <code>events().patch()</code> (HTTP PATCH)—"patch" 1 or more fields in resource</li>
<li> <code>events().update()</code> (HTTP PUT)—replace/rewrite entire resource</li>
</ol>
Do you just update that resource with <code>events().patch()</code> or do you replace the entire resource with <code>events().update()</code>? To answer that question, ask yourself, "How many fields am I updating?" In our case, we only want to change the date and make this event repeat, so PATCH is a better solution. If instead, you also wanted to rename the event or switch dinner to another set of friends, you'd then be changing all the fields, so PUT would be a better solution in <i>that</i> case.
<br />
<br />
Using PATCH means you're just providing the deltas between the original & updated event, so the <code>EVENT</code> body this time reflects just those changes:
<br />
<pre><span style="font-size: small;">TIMEZONE = 'America/Los_Angeles'
EVENT = {
'start': {'dateTime': '2017-07-01T19:00:00', 'timeZone': TIMEZONE},
'end': {'dateTime': '2017-07-01T22:00:00', 'timeZone': TIMEZONE},
'recurrence': ['RRULE:FREQ=MONTHLY;INTERVAL=2;UNTIL=20171231']
}</span></pre>
<h2>
Repeating events</h2>
Something you <b>haven't</b> seen before is how to do repeating events. To do this, you need to define what’s known as a <i>recurrence rule</i> ("RRULE"), which answers the question of how often an event repeats. It looks somewhat cryptic but follows <a href="http://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html">the RFC 5545 Internet standard</a> which you can basically decode like this:
<br />
<ul>
<li> <code>FREQ=MONTHLY</code>—event to occur on a monthly basis...</li>
<li> <code>INTERVAL=2</code>—... but every two months (every other month)</li>
<li> <code>UNTIL=20171231</code>—... until this date</li>
</ul>
There are many ways events can repeat, so I suggest you look at all the examples at the RFC link above.
<br />
<br />
<h2>
Finishing touches</h2>
Finally, provide the <code>EVENT_ID</code> and call <code>events().patch()</code>:<br />
<pre>EVENT_ID = <i>YOUR_EVENT_ID_STR_HERE</i> # use your own!
e = GCAL.events().patch(calendarId='primary', eventId=EVENT_ID,
sendNotifications=True, body=EVENT).execute()
</pre>
Keep in mind that in real life, your users may be accessing your app from their desktop or mobile devices, so you need to ensure you don't override an earlier change. In this regard, developers should use the <a href="https://www.blogger.com/developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match">If-Match header</a> along with an <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag">ETag value</a> to validate unique requests. For more information, check out the <a href="http://developers.google.com/google-apps/calendar/v3/version-resources#conditional_modification">conditional modification page</a> in the official docs.
<br />
<br />
The one remaining thing is to confirm on-screen that the calendar event was updated successfully. We do that by checking the return value—it should be an <a href="https://developers.google.com/google-apps/calendar/v3/reference/events">Event object</a> with all the existing details as well as the modified fields:
<br />
<pre>print('''\
*** %r event (ID: %s) modified:
Start: %s
End: %s
Recurring (rule): %s
''' % (e['summary'].encode('utf-8'), e['id'], e['start']['dateTime'],
e['end']['dateTime'], e['recurrence'][0]))
</pre>
That's pretty much the entire script save for the OAuth2 boilerplate code we've explored previously. The script is posted below in its entirety, and if you add a valid event ID and
run it, depending on the date/times you use, you'll see something like this:
<br />
<pre>$ python gcal_modify.py
*** 'Dinner with friends' event (ID: <i>YOUR_EVENT_ID_STR_HERE</i>) modified:
Start: 2017-07-01T19:00:00-07:00
End: 2017-07-01T22:00:00-07:00
Recurring (rule): RRULE:FREQ=MONTHLY;UNTIL=20171231;INTERVAL=2
</pre>
It also works with Python 3 with one slight nit/difference being the "b" prefix on from the event name due to converting from Unicode to <code>bytes</code>:<br />
<code>*** b'Dinner with friends' event...</code>
<br />
<br />
<h2>
Conclusion</h2>
Now you know how to modify events as well as make them repeat. To complete the example, below is the entire script for your convenience which runs on both Python 2 <b>and</b> Python 3 (unmodified!):<br />
<pre><span style="font-size: small;"><b>from</b> __future__ <b>import</b> print_function
<b>from</b> apiclient.discovery <b>import</b> build
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
SCOPES = 'https://www.googleapis.com/auth/calendar'
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> creds.invalid:
flow = client.flow_from_clientsecrets('client_secret.json', SCOPES)
creds = tools.run_flow(flow, store)
CAL = build('calendar', 'v3', http=creds.authorize(Http()))
TIMEZONE = 'America/Los_Angeles'
EVENT = {
'start': {'dateTime': '2017-07-01T19:00:00', 'timeZone': TIMEZONE},
'end': {'dateTime': '2017-07-01T22:00:00', 'timeZone': TIMEZONE},
'recurrence': ['RRULE:FREQ=MONTHLY;INTERVAL=2;UNTIL=20171231']
}
EVENT_ID = YOUR_EVENT_ID_STR_HERE
e = GCAL.events().patch(calendarId='primary', eventId=EVENT_ID,
sendNotifications=True, body=EVENT).execute()
print('''\
*** %r event (ID: %s) modified:
Start: %s
End: %s
Recurring (rule): %s
''' % (e['summary'].encode('utf-8'), e['id'], e['start']['dateTime'],
e['end']['dateTime'], e['recurrence'][0]))
</span></pre>
You can now customize this code for your own needs, for a mobile frontend, a server-side backend, or to access other Google APIs. If you want to learn more about using the Google Calendar API, check out the following resources:
<br />
<ul>
<li> <a href="http://goo.gl/GuH3L4" target="_blank">Creating great experiences with email markup</a>—video</li>
<li> <a href="http://developers.google.com/google-apps/calendar/quickstart/python" target="_blank">Creating events with the Calendar API</a>—video & blog post</li>
<li> <a href="http://wescpy.blogspot.com/2015/09/creating-events-in-google-calendar.html" target="_blank">Creating events with the Calendar API in Python</a>—blog post</li>
<li> <a href="http://developers.google.com/google-apps/calendar/quickstart/python" target="_blank">Official Calendar API Python quickstart</a>—list the next 10 events in your calendar</li>
<li> <a href="http://developers.google.com/google-apps/calendar" target="_blank">Official Calendar API documentation</a>—everything you need to know and more!</li>
<li> <a href="https://developers.google.com/gmail/markup/google-calendar" target="_blank">Email markup documentation</a> for Google Calendar</li>
</ul>
<br />
<br />
<!--[if gte mso 9]><xml>
<w:LatentStyles DefLockedState="false" LatentStyleCount="156">
</w:LatentStyles>
</xml><![endif]--><!--[if gte mso 10]>
<style>
/* Style Definitions */
table.MsoNormalTable
{mso-style-name:"Table Normal";
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-parent:"";
mso-padding-alt:0in 5.4pt 0in 5.4pt;
mso-para-margin:0in;
mso-para-margin-bottom:.0001pt;
mso-pagination:widow-orphan;
font-size:10.0pt;
font-family:"Times New Roman";
mso-ansi-language:#0400;
mso-fareast-language:#0400;
mso-bidi-language:#0400;}
</style>
<![endif]-->wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com1tag:blogger.com,1999:blog-6940043312015460811.post-79543368816031089242017-06-01T10:00:00.005-07:002023-09-21T22:31:38.892-07:00Managing Shared (formerly Team) Drives with Python and the Google Drive API<b>2023 UPDATE</b>: We are working to put updated versions of all the code into GitHub... stay tuned. The link will provided in all posts once the code sample(s) is(are) available.<div></div><b><br /></b><div><b>2019 UPDATE:</b> "G Suite" is now called "Google Workspace", "Team Drives" is now known as "Shared Drives", and the corresponding <span style="font-family: courier;">supportsTeamDrives</span> flag has been renamed to <span style="font-family: courier;">supportsAllDrives</span>. Please take note of these changes regarding the post below.<div><br /></div><div><div><div><b>NOTE 1:</b> Team Drives is only available for <a href="http://gsuite.google.com/pricing.html">G Suite Business Standard users or higher</a>. If you're developing an application for Team Drives, you'll need similar access. <!--The code featured here is also available as a <a href="http:">video + overview post</a> as part of <a href="http://goo.gl/JpBQ40">this series</a--><br />
<b>NOTE 2:</b> The code featured here is also available as a <a href="http://goo.gl/2XakzB">video + overview post</a> as part of <a href="http://goo.gl/JpBQ40">this series</a>.<br />
<br />
<h2>
Introduction</h2>
Team Drives is a <a href="https://blog.google/products/g-suite/introducing-new-enterprise-ready-tools-google-drive/">relatively new </a>feature from the Google Drive team, created to solve some of the issues of a user-centric system in larger organizations. Team Drives are owned by an organization rather than a user and with its use, locations of files and folders won't be a mystery any more. While your users do have to be a G Suite Business (or higher) customer to use Team Drives, the good news for developers is that you won't have to write new apps from scratch or learn a completely different API.<br />
<br />
Instead, Team Drives features are accessible through the same <a href="http://developers.google.com/drive">Google Drive API</a> you've come to know so well with Python. In this post, we'll demonstrate a sample Python app that performs core features that all developers should be familiar with. By the time you've finished reading this post and the sample app, you should know how to:<br />
<ul>
<li>Create Team Drives</li>
<li>Add members to Team Drives</li>
<li>Create a folder in Team Drives</li>
<li>Import/upload files to Team Drives folders</li>
</ul>
<br />
<h2>
Using the Google Drive API</h2>
The demo script requires creating files and folders, so you do need full read-write access to Google Drive. The scope you need for that is:<br />
<ul>
<li><code>'https://www.googleapis.com/auth/drive'</code> — Full (read-write) access to Google Drive</li>
</ul>
If you're new to using Google APIs, we recommend reviewing <a href="http://goo.gl/cdm3kZ">earlier posts & videos</a> covering the setting up projects and the authorization boilerplate so that we can focus on the main app. Once we've authorized our app, assume you have a service endpoint to the API and have assigned it to the <code>DRIVE</code> variable.<br />
<br />
<h2>
Create Team Drives</h2>
New Team Drives can be created with <code>DRIVE.teamdrives().create()</code>. Two things are required to create a Team Drive: 1) you should name your Team Drive. To make the create process idempotent, you need to create a unique request ID so that any number of identical calls will still only result in a single Team Drive being created. It's recommended that developers use a language-specific UUID library. For Python developers, that's the <code>uuid</code> module. From the API response, we return the new Team Drive's ID. Check it out:<br />
<pre><b>def</b> create_td(td_name):
request_id = str(uuid.uuid4())
body = {'name': td_name}
<b>return</b> DRIVE.teamdrives().create(body=body,
requestId=request_id, fields='id').execute().get('id')
</pre>
<h2>
Add members to Team Drives</h2>
To add members/users to Team Drives, you only need to create a new permission, which can be done with <code>DRIVE.permissions().create()</code>, similar to how you would share a file in regular Drive with another user. The pieces of information you need for this request are the ID of the Team Drive, the new member's email address as well as the desired role... choose from: "organizer", "owner", "writer", "commenter", "reader". Here's the code:<br />
<pre><b>def</b> add_user(td_id, user, role='commenter'):
body = {'type': 'user', 'role': role, 'emailAddress': user}
<b>return</b> DRIVE.permissions().create(body=body, fileId=td_id,
supportsTeamDrives=True, fields='id').execute().get('id')
</pre>
Some additional notes on permissions: the user can only be bestowed permissions equal to or less than the person/admin running the script... IOW, they cannot grant someone else greater permission than what they have. Also, if a user has a certain role in a Team Drive, they can be granted greater access to individual elements in the Team Drive. Users who are not members of a Team Drive can still be granted access to Team Drive contents on a per-file basis.<br />
<br />
<h2>
Create a folder in Team Drives</h2>
Nothing to see here! Yep, creating a folder in Team Drives is identical to creating a folder in regular Drive, with <code>DRIVE.files().create()</code>. The only difference is that you pass in a Team Drive ID rather than regular Drive folder ID. Of course, you also need a folder name too. Here's the code:<br />
<pre><b>def</b> create_td_folder(td_id, folder):
body = {'name': folder, 'mimeType': FOLDER_MIME, 'parents': [td_id]}
<b>return</b> DRIVE.files().create(body=body,
supportsTeamDrives=True, fields='id').execute().get('id')
</pre>
<h2>
Import/upload files to Team Drives folders</h2>
Uploading files to a Team Drives folder is also identical to to uploading to a normal Drive folder, and also done with <code>DRIVE.files().create()</code>. Importing is slightly different than uploading because you're uploading a file <b>and</b> converting it to a G Suite/Google Apps document format, i.e., uploading CSV as a Google Sheet, or plain text or Microsoft Word&reg; file as Google Docs. In the sample app, we tackle the former:<br />
<pre><b>def</b> import_csv_to_td_folder(folder_id, fn, mimeType):
body = {'name': fn, 'mimeType': mimeType, 'parents': [folder_id]}
<b>return</b> DRIVE.files().create(body=body, media_body=fn+'.csv',
supportsTeamDrives=True, fields='id').execute().get('id')
</pre>
The secret to importing is the MIMEtype. That tells Drive whether you want conversion to a G Suite/Google Apps format (or not). The same is true for exporting. The import and export MIMEtypes supported by the Google Drive API can be found in my SO answer <a href="http://stackoverflow.com/a/38406284/305689">here</a>.<br /><br />
<h2>
Driver app</h2>
All these functions are great but kind-of useless without being called by a main application, so here we are:<br />
<pre>FOLDER_MIME = 'application/vnd.google-apps.folder'
SOURCE_FILE = 'inventory' # on disk as 'inventory.csv'
SHEETS_MIME = 'application/vnd.google-apps.spreadsheet'
td_id = create_td('Corporate shared TD')
print('** Team Drive created')
perm_id = add_user(td_id, 'email@example.com')
print('** User added to Team Drive')
folder_id = create_td_folder(td_id, 'Manufacturing data')
print('** Folder created in Team Drive')
file_id = import_csv_to_td_folder(folder_id, SOURCE_FILE, SHEETS_MIME)
print('** CSV file imported as Google Sheets in Team Drives folder')
</pre>
The first set of variables represent some MIMEtypes we need to use as well as the CSV file we're uploading to Drive and requesting it be converted to Google Sheets format. Below those definitions are calls to all four functions described above.<br />
<br />
<h2>
Conclusion</h2>
If you run the script, you should get output that looks something like this, with each <code>print()</code> representing each API call:<br />
<pre>$ python3 td_demo.py
** Team Drive created
** User added to Team Drive
** Folder created in Team Drive
** CSV file imported as Google Sheets in Team Drives folder
</pre>
When the script has completed, you should have a new Team Drives folder called "Corporate shared TD", and within, a folder named "Manufacturing data" which contains a Google Sheets file called "inventory".<br />
<br />
Below is the entire script for your convenience which runs on both Python 2 <b>and</b> Python 3 (unmodified!)—by using, copying, and/or modifying this code or any other piece of source from this blog, you implicitly agree to its <a href="http://apache.org/licenses/LICENSE-2.0">Apache2 license</a>:<br />
<pre><span style="font-size: small;"><b>from</b> __future__ <b>import</b> print_function
<b>import</b> uuid
<b>from</b> apiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
SCOPES = 'https://www.googleapis.com/auth/drive'
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> 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()))
<b>def</b> create_td(td_name):
request_id = str(uuid.uuid4()) # random unique UUID string
body = {'name': td_name}
<b>return</b> DRIVE.teamdrives().create(body=body,
requestId=request_id, fields='id').execute().get('id')
<b>def</b> add_user(td_id, user, role='commenter'):
body = {'type': 'user', 'role': role, 'emailAddress': user}
<b>return</b> DRIVE.permissions().create(body=body, fileId=td_id,
supportsTeamDrives=True, fields='id').execute().get('id')
<b>def</b> create_td_folder(td_id, folder):
body = {'name': folder, 'mimeType': FOLDER_MIME, 'parents': [td_id]}
<b>return</b> DRIVE.files().create(body=body,
supportsTeamDrives=True, fields='id').execute().get('id')
<b>def</b> import_csv_to_td_folder(folder_id, fn, mimeType):
body = {'name': fn, 'mimeType': mimeType, 'parents': [folder_id]}
<b>return</b> DRIVE.files().create(body=body, media_body=fn+'.csv',
supportsTeamDrives=True, fields='id').execute().get('id')
FOLDER_MIME = 'application/vnd.google-apps.folder'
SOURCE_FILE = 'inventory' # on disk as 'inventory.csv'... CHANGE!
SHEETS_MIME = 'application/vnd.google-apps.spreadsheet'
td_id = create_td('Corporate shared TD')
print('** Team Drive created')
perm_id = add_user(td_id, 'email@example.com') # CHANGE!
print('** User added to Team Drive')
folder_id = create_td_folder(td_id, 'Manufacturing data')
print('** Folder created in Team Drive')
file_id = import_csv_to_td_folder(folder_id, SOURCE_FILE, SHEETS_MIME)
print('** CSV file imported as Google Sheets in Team Drives folder')
</span></pre>
As with our other code samples, you can now customize it to learn more about the API, integrate into other apps for your own needs, for a mobile frontend, sysadmin script, or a server-side backend!<br />
<br />
<h2>
Code challenge</h2>
Write a simple application that moves folders (and its files or folders) in regular Drive to Team Drives. Each folder you move should be a corresponding folder in Team Drives. Remember that files in Team Drives can only have one parent, and the same goes for folders.</div></div></div></div>wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com0tag:blogger.com,1999:blog-6940043312015460811.post-11051174257428788302017-02-22T14:39:00.000-08:002017-02-28T22:59:49.284-08:00Adding text & shapes with the Google Slides API<b>NOTE:</b> The code featured here is also available as a <a href="http://goo.gl/BPfiy9">video + overview post</a> as part of <a href="http://goo.gl/JpBQ40">this series</a>.<br />
<br />
<h2>
Introduction</h2>
This is the fourth entry highlighting primary use cases of the <a href="http://developers.google.com/slides">Google Slides API</a> with Python; check back in the archives to access the first three. Today, we're focused on some of the basics, like adding text to slides. We'll also cover adding shapes, and as a bonus, adding text <b>in</b>to shapes!<br />
<br />
<h2>
Using the Google Slides API</h2>
The demo script requires creating a new slide deck (and adding a new slide) so you need the read-write scope for Slides:<br />
<ul>
<li><code>'https://www.googleapis.com/auth/presentations'</code> — Read-write access to Slides and Slides presentation properties</li>
</ul>
If you're new to using Google APIs, we recommend reviewing <a href="http://goo.gl/cdm3kZ">earlier posts & videos</a> covering the setting up projects and the authorization boilerplate so that we can focus on the main app. Once we've authorized our app, assume you have a service endpoint to the API and have assigned it to the <code>SLIDES</code> variable.<br />
<br />
<h2>
Create new presentation & get its objects' IDs</h2>
A new slide deck can be created with <code>SLIDES.presentations().create()</code>—or alternatively with the <a href="http://developers.google.com/drive/web">Google Drive API</a> which we won't do here. From the API response, we save the new deck's ID along with the IDs of the title and subtitle textboxes on the default title slide:<br />
<pre>rsp = SLIDES.presentations().create(
body={'title': 'Adding text formatting DEMO'}).execute()
deckID = rsp['presentationId']
titleSlide = rsp['slides'][0] # title slide object IDs
titleID = titleSlide['pageElements'][0]['objectId']
subtitleID = titleSlide['pageElements'][1]['objectId']</pre>
The title slide only has two elements on it, the title and subtitle textboxes, returned in that order, hence why we grab them at indexes 0 and 1 respectively.<br />
<br />
<h2>
Generating our own unique object IDs</h2>
In the next steps, we generate our own unique object IDs. We'll first explain what those objects are followed by why you'd want to create your own object IDs rather than letting the API create default IDs for the same objects.<br />
<br />
As we've done in previous posts on the Slides API, we create one new slide with the "main point" layout. It has one notable object, a "large-ish" textbox and nothing else. We'll create IDs for the slide itself and another for its textbox. Next, we'll (use the API to) "draw" 3 shapes on this slide, so we'll create IDs for each of those. That's 5 (document-unique) IDs total. Now let's discuss why you'd "roll your own" IDs.<br />
<br />
<h3>
Why and how to generate our own IDs</h3>
It's advantageous for all developers to minimize the overall number of calls to Google APIs. While most of services provided through the APIs are free, they'll have some quota to prevent abuse (Slides API <a href="https://developers.google.com/slides/limits">quotas page</a> FYI). So how does creating our own IDs help reduce API calls?<br />
<br />
Passing in object IDs is optional for "create" calls. Providing your own ID lets you create an object and modify it using additional requests within the same API call to <code>SLIDES.presentations().batchUpdate()</code>. If you <i>don't</i> provide your own object IDs, the API will generate a unique one for you.<br />
<br />
Unfortunately, this means that instead of one API call, you'll need one to <i>create</i> the object, likely another to <i>get</i> that object to determine its ID, and yet another to <i>update</i> that object using the ID you just fetched. Separate API calls to create, get, and update means (at least) 3x more than if you provided your own IDs (where you can do create & update with a single API call; no get necessary).<br />
<br />
Here are a few things to know when rolling your own IDs:<br />
<ul>
<li> IDs must start with an alphanumeric character or underscore (matches regex <code>[a-zA-Z0-9_]</code>)</li>
<li> Any remaining characters can also include a hyphen or colon (matches regex <code>[a-zA-Z0-9_-:]</code>)</li>
<li> The length of the ID must conform to: 5 ≤ <code>len(<i>ID</i>)</code> ≤ 50.</li>
<li> Object IDs must be unique across all objects in a presentation.</li>
</ul>
<br />
You'll somehow need to ensure your IDs are unique or use UUIDs (<a href="http://en.wikipedia.org/wiki/Universally_unique_identifier">universally unique identifiers</a>) for which most languages have libraries for. Examples: Java developers can use <code>java.util.UUID.randomUUID().toString()</code> while Python users can import the <code>uuid</code> module plus any extra work to get UUID string values:<br />
<pre>import uuid
gen_uuid = lambda : str(uuid.uuid4()) # get random UUID string</pre>
Finally, be aware that if an object is modified in the UI, its ID <i>may change</i>. For more information, review the "Working with object IDs" section in <a href="https://developers.google.com/slides/how-tos/overview#working_with_object_ids">the Slides API Overview page</a>.<br />
<br />
<h3>
Back to sample app</h3>
All that said, let's go back to the code and generate those 5 random object IDs we promised earlier:<br />
<pre>mpSlideID = gen_uuid() # mainpoint IDs
mpTextboxID = gen_uuid()
smileID = gen_uuid() # shape IDs
str24ID = gen_uuid()
arwbxID = gen_uuid()
</pre>
With that, we're ready to create the requests array (<code>reqs</code>) to send to the API.<br />
<br />
<h3>
Create "main point" slide</h3>
The first request creates the "main point" slide...<br />
<pre>reqs = [
{'createSlide':
'objectId': mpSlideID,
'slideLayoutReference': {'predefinedLayout': 'MAIN_POINT'},
'placeholderIdMappings': [{
'objectId': mpTextboxID,
'layoutPlaceholder': {'type': 'TITLE', 'index': 0}
}],
}},
</pre>
...where...<br />
<ul>
<li><code>objectID</code>—our generated ID we're assigning to the newly-created slide</li>
<li><code>slideLayoutReference</code>—new slide layout type ("main point")</li>
<li><code>placeholderIdMappings</code>—array of IDs ([inner] <code>objectId</code>) for each of the page elements and which object (<code>layoutPlaceholder</code>) they should map or be assigned to</li>
</ul>
The page elements created on the new slide (depends [obviously] on the layout chosen); "main point" only has the one textbox, hence why <code>placeholderIdMappings</code> only has one element.<br />
<br />
<h2>
Add title slide and main point textbox text</h2>
The next requests fill in the title & subtitle in the default title slide and also the textbox on the main point slide.<br />
<pre>{'insertText': {'objectId': titleID, 'text': 'Adding text and shapes'}},
{'insertText': {'objectId': subtitleID, 'text': 'via the Google Slides API'}},
{'insertText': {'objectId': mpTextboxID, 'text': 'text & shapes'}},
</pre>
The first pair use IDs that were generated by the Slides API when the presentation was created while the main point textbox ID was generated by us.<br />
<br />
<h2>
Create three shapes</h2>
Above, we created IDs for three shapes, a "smiley face," a 24-point star, and a double arrow box (<code>smileID</code>, <code>str24ID</code>, <code>arwbxID</code>). The request for the first looks like this:<br />
<pre>{'createShape': {
'objectId': smileID,
'shapeType': 'SMILEY_FACE',
'elementProperties': {
"pageObjectId": mpSlideID,
'size': {
'height': {'magnitude': 3000000, 'unit': 'EMU'},
'width': {'magnitude': 3000000, 'unit': 'EMU'}
},
'transform': {
'unit': 'EMU', 'scaleX': 1.3449, 'scaleY': 1.3031,
'translateX': 4671925, 'translateY': 450150,
},
},
}}
</pre>
The JSON for the other two shapes are similar, with differences being: the object ID, the <code>shapeType</code>, and the transform. You can see the corresponding requests for the other shapes in the full source code at the bottom of this post, so we won't display them here as the descriptions will be nearly identical.<br />
<br />
<h2>
Size & transform for slide objects</h2>
When placing or manipulating objects on slides, key element properties you must provide are the sizes and transforms. These are components you must either use some math to create or derive from pre-existing objects. Resizing, rotating, and similar operations require some basic knowledge of matrix math. Take a look at <a href="http://developers.google.com/slides/how-tos/transform">the Page Elements page</a> in the official docs as well as <a href="http://developers.google.com/slides/concepts/transforms">the Transforms concept guide</a> for more details.<br />
<br />
Deriving from pre-existing objects: if you're short on time, don't want to deal with the math, or perhaps thinking something like, "Geez, I just want to draw a smiley face on a slide." One common pattern then, is to bring up the Slides UI, create a blank slide & place your image or draw your shape the <i>way</i> you want, with the <i>size</i> you want, & putting it exactly <i>where</i> you want. For example:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEguLcbhcxk_LmxZeY4h2wobBfFzEE1qNLNPHmCmxtfEjFYNI4GAfVDOYm0NNb94VKPs9l7G8PRoAbKwa4VH9vNrs_D6QOF5J9O2qMDxpwkuKaWFI-tOd32I7Xu50fxHA66F2G12kxsBvQ0/s1600/gs006sc2.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEguLcbhcxk_LmxZeY4h2wobBfFzEE1qNLNPHmCmxtfEjFYNI4GAfVDOYm0NNb94VKPs9l7G8PRoAbKwa4VH9vNrs_D6QOF5J9O2qMDxpwkuKaWFI-tOd32I7Xu50fxHA66F2G12kxsBvQ0/s1600/gs006sc2.gif" /></a></div>
<br />
Once you have that desired shape (and size and location), you can use the API (either <a href="http://developers.google.com/slides/reference/rest/v1/presentations/get">presentations.get</a> or <a href="http://developers.google.com/slides/reference/rest/v1/presentations.pages/get">presentations.pages.get</a>) to read that object's size and transform then drop both of those into your application so the API creates a new shape in the exact way, mirroring what you created in the UI. For the smiley face above, the JSON payload we got back from one of the "get" calls could look something like:<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjIJIg-FVVoD0wVNZ9pVQxMIGnlzQ-ehxAIthGOnARrAK6SupUycVeZQ1dHhJKyBz0CGDrvcHZmckXS05HpbOoDx_upH5qU6mSWK36yqJs3tAy5XTRaMxboF9hC-UFJ0AstCIHnQ0EegO8/s1600/js-code-get-hl.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjIJIg-FVVoD0wVNZ9pVQxMIGnlzQ-ehxAIthGOnARrAK6SupUycVeZQ1dHhJKyBz0CGDrvcHZmckXS05HpbOoDx_upH5qU6mSWK36yqJs3tAy5XTRaMxboF9hC-UFJ0AstCIHnQ0EegO8/s1600/js-code-get-hl.png" /></a></div>
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
If you scroll back up to the <code>createShape</code> request, you'll see we used those exact values. Note: because the 3 shapes are all in different locations and sizes, expect the corresponding values for each shape to be different.<br />
<br />
<h2>
Bonus: adding text to shapes</h2>
Now that you know how to add text and shapes, it's only fitting that we show you how to add text <i>in</i>to shapes. The good news is that the technique is no different than adding text to textboxes or even tables. So with the shape IDs, our final set of requests along with the <code>batchUpdate()</code> call looks like this:
<br />
<pre> {'insertText': {'objectId': smileID, 'text': 'Put the nose somewhere here!'}},
{'insertText': {'objectId': str24ID, 'text': 'Count 24 points on this star!'}},
{'insertText': {'objectId': arwbxID, 'text': "An uber bizarre arrow box!"}},
] # end of 'reqs'
SLIDES.presentations().batchUpdate(body={'requests': reqs},
presentationId=deckID).execute()</pre>
<h2>
Conclusion</h2>
If you run the script, you should get output that looks something like this, with each <code>print()</code> representing each API call:<br />
<pre>$ python3 slides_shapes_text.py
** Create new slide deck & set up object IDs
** Create "main point" slide, add text & interesting shapes
DONE
</pre>
When the script has completed, you should have a new presentation with a title slide and a main point slide with shapes which should look something like this:<br />
<br />
Below is the entire script for your convenience which runs on both Python 2 <b>and</b> Python 3 (unmodified!)—by using, copying, and/or modifying this code or any other piece of source from this blog, you implicitly agree to its <a href="http://apache.org/licenses/LICENSE-2.0">Apache2 license</a>:<br />
<pre><span style="font-size: small;"><b>from</b> __future__ <b>import</b> print_function
<b>import</b> uuid
<b>from</b> apiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
gen_uuid = lambda : str(uuid.uuid4()) # get random UUID string
SCOPES = 'https://www.googleapis.com/auth/presentations',
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> creds.invalid:
flow = client.flow_from_clientsecrets('client_secret.json', SCOPES)
creds = tools.run_flow(flow, store)
SLIDES = discovery.build('slides', 'v1', http=creds.authorize(Http()))
print('** Create new slide deck & set up object IDs')
rsp = SLIDES.presentations().create(
body={'title': 'Adding text & shapes DEMO'}).execute()
deckID = rsp['presentationId']
titleSlide = rsp['slides'][0] # title slide object IDs
titleID = titleSlide['pageElements'][0]['objectId']
subtitleID = titleSlide['pageElements'][1]['objectId']
mpSlideID = gen_uuid() # mainpoint IDs
mpTextboxID = gen_uuid()</span></pre>
<pre><span style="font-size: small;">smileID = gen_uuid() # shape IDs
</span></pre>
<pre><span style="font-size: small;">str24ID = gen_uuid()
arwbxID = gen_uuid()
print('** Create "main point" slide, add text & interesting shapes')
reqs = [
# create new "main point" layout slide, giving slide & textbox IDs
{'createSlide': {
'objectId': mpSlideID,
'slideLayoutReference': {'predefinedLayout': 'MAIN_POINT'},
'placeholderIdMappings': [{
'objectId': mpTextboxID,
'layoutPlaceholder': {'type': 'TITLE', 'index': 0}
}],
}},
# add title & subtitle to title slide; add text to main point slide textbox
{'insertText': {'objectId': titleID, 'text': 'Adding text and shapes'}},
{'insertText': {'objectId': subtitleID, 'text': 'via the Google Slides API'}},
{'insertText': {'objectId': mpTextboxID, 'text': 'text & shapes'}},
# create smiley face
{'createShape': {
'objectId': smileID,
'shapeType': 'SMILEY_FACE',
'elementProperties': {
"pageObjectId": mpSlideID,
'size': {
'height': {'magnitude': 3000000, 'unit': 'EMU'},
'width': {'magnitude': 3000000, 'unit': 'EMU'}
},
'transform': {
'unit': 'EMU', 'scaleX': 1.3449, 'scaleY': 1.3031,
'translateX': 4671925, 'translateY': 450150,
},
},
}},
# create 24-point star
{'createShape': {
'objectId': str24ID,
'shapeType': 'STAR_24',
'elementProperties': {
"pageObjectId": mpSlideID,
'size': {
'height': {'magnitude': 3000000, 'unit': 'EMU'},
'width': {'magnitude': 3000000, 'unit': 'EMU'}
},
'transform': {
'unit': 'EMU', 'scaleX': 0.7079, 'scaleY': 0.6204,
'translateX': 2036175, 'translateY': 237350,
},
},
}},
# create double left & right arrow w/textbox
{'createShape': {
'objectId': arwbxID,
'shapeType': 'LEFT_RIGHT_ARROW_CALLOUT',
'elementProperties': {
"pageObjectId": mpSlideID,
'size': {
'height': {'magnitude': 3000000, 'unit': 'EMU'},
'width': {'magnitude': 3000000, 'unit': 'EMU'}
},
'transform': {
'unit': 'EMU', 'scaleX': 1.1451, 'scaleY': 0.4539,
'translateX': 1036825, 'translateY': 3235375,
},
},
}},
# add text to all 3 shapes
{'insertText': {'objectId': smileID, 'text': 'Put the nose somewhere here!'}},
{'insertText': {'objectId': str24ID, 'text': 'Count 24 points on this star!'}},
{'insertText': {'objectId': arwbxID, 'text': "An uber bizarre arrow box!"}},
]
SLIDES.presentations().batchUpdate(body={'requests': reqs},
presentationId=deckID).execute()
print('DONE')
</span></pre>
As with our other code samples, you can now customize it to learn more about the API, integrate into other apps for your own needs, for a mobile frontend, sysadmin script, or a server-side backend!<br />
<br />
<h2>
Code challenge</h2>
Create a 2x3 or 3x4 table on a slide and add text to each "cell." This should be a fairly easy exercise, especially if you look at the <a href="http://developers.google.com/slides/samples/tables">Table Operations documentation</a>. <b>HINT</b>: you'll be using <code>insertText</code> with just an extra field, <code>cellLocation</code>. <b>EXTRA CREDIT</b>: generalize your solution so that you're grabbing cells from a Google Sheet and "import" them into a table on a slide. <b>HINT</b>: look for the <a href="http://goo.gl/Fa34S3">earlier post</a> where we describe how to create slides from spreadsheet data.wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com4tag:blogger.com,1999:blog-6940043312015460811.post-54382149463892729062016-12-13T22:54:00.000-08:002016-12-14T13:45:34.326-08:00Formatting text with the Google Slides API<b>NOTE:</b> The code covered in this post are also available in a <a href="http://goo.gl/zkbilV">video walkthrough</a>.<br />
<br />
<h2>
Introduction</h2>
If you know something about public speaking, you're aware that the most effective presentations are those which have more images and less text. As a developer of applications that auto-generate slide decks, this is even more critical as you must ensure that your code creates the most <i>compelling</i> presentations possible for your users.<br />
<br />
This means that any text featured in those slide decks <i>must</i> be more impactful. To that end, it's important you know how to format any text you <i>do</i> have. That's the exact subject of today's post, showing you how to format text in a variety of ways using Python and the <a href="http://developers.google.com/slides">Google Slides API</a>.<br />
<br />
The API is fairly new, so if you're unfamiliar with it, check out <a href="http://goo.gl/o6EFwk">the launch post</a> and take a peek at <a href="http://developers.google.com/slides/how-tos/overview">the API overview page</a> to acclimate yourself to it first. You can also read related posts (and videos) explaining how to <a href="http://goo.gl/lMkRUX">replace text & images</a> with the API or how to <a href="http://goo.gl/Fa34S3">generate slides from spreadsheet data</a>. If you're ready-to-go, let's move on!<br />
<br />
<h2>
Using the Google Slides API</h2>
The demo script requires creating a new slide deck so you need the read-write scope for Slides:<br />
<ul>
<li><code>'https://www.googleapis.com/auth/presentations'</code> — Read-write access to Slides and Slides presentation properties</li>
</ul>
If you're new to using Google APIs, we recommend reviewing <a href="http://goo.gl/cdm3kZ">earlier posts & videos</a> covering the setting up projects and the authorization boilerplate so that we can focus on the main app. Once we've authorized our app, assume you have a service endpoint to the API and have assigned it to the <code>SLIDES</code> variable.<br />
<br />
<h2>
Create deck & set up new slide for text formatting</h2>
A new slide deck can be created with <code>SLIDES.presentations().create()</code>—or alternatively with the <a href="http://developers.google.com/drive/web">Google Drive API</a> which we won't do here. We'll name it, "Slides text formatting DEMO" and save its ID along with the IDs of the title and subtitle textboxes on the auto-created title slide:<br />
<pre>DATA = {'title': 'Slides text formatting DEMO'}
rsp = SLIDES.presentations().create(body=DATA).execute()
deckID = rsp['presentationId']
titleSlide = rsp['slides'][0]
titleID = titleSlide['pageElements'][0]['objectId']
subtitleID = titleSlide['pageElements'][1]['objectId']</pre>
The title slide only has two elements on it, the title and subtitle textboxes, returned in that order, hence why we grab them at indexes 0 and 1 respectively. Now that we have a deck, let's add a slide that has a single (largish) textbox. The slide layout with that characteristic that works best for our demo is the "main point" template:<br />
<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjsKNJ8aN-L7S4vh6bdrlzBOZaBkHvcnTrhjMU75PDCAxnYxGejvLlm8_SOAl8qcydjftJxw9y3HVaMFsqXNQ8pwHUPlGrEVO5PbltUXpJX-QMZkSiOJe75-rjgdzvPHltF_sd4UukIKx4/s1600/slide-layouts2.png" imageanchor="1"><img border="0" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjsKNJ8aN-L7S4vh6bdrlzBOZaBkHvcnTrhjMU75PDCAxnYxGejvLlm8_SOAl8qcydjftJxw9y3HVaMFsqXNQ8pwHUPlGrEVO5PbltUXpJX-QMZkSiOJe75-rjgdzvPHltF_sd4UukIKx4/s400/slide-layouts2.png" width="365" /></a><br />
<br />
While we're at it, let's also add the title & subtitle on the title slide. Here's the snippet that builds and executes all three requests:<br />
<pre>print('** Create "main point" layout slide & add titles')
reqs = [
{'createSlide':
{'slideLayoutReference': {'predefinedLayout': 'MAIN_POINT'}}},
{'insertText':
{'objectId': titleID, 'text': 'Formatting text'}},
{'insertText':
{'objectId': subtitleID, 'text': 'via the Google Slides API'}},
]
rsp = SLIDES.presentations().batchUpdate(body={'requests': reqs},
presentationId=deckID).execute().get('replies')
slideID = rsp[0]['createSlide']['objectId']
</pre>
The requests are sent in the order you see above, and responses come back in the same order. We don't care much about the 'insertText' directives, but we do want to get the ID of the newly-created slide. In the array of 3 returned responses, that slideID comes first.<br />
<br />
Why do we need the slide ID? Well, since we're going to be using the one textbox on that slide, the only way to get the ID of that textbox is by doing a <code>presentations().pages().get()</code> call to fetch all the objects on that slide. Since there's only one "page element," the textbox in question, we make that call and save the first (and only) object's ID:<br />
<pre>print('** Fetch "main point" slide title (textbox) ID')
rsp = SLIDES.presentations().pages().get(presentationId=deckID,
pageObjectId=slideID).execute().get('pageElements')
textboxID = rsp[0]['objectId']
</pre>
Armed with the textbox ID, we're ready to add our text and format it!
<br />
<br />
<h2>
Formatting text</h2>
The last part of the script starts by inserting seven (short) paragraphs of text—then format different parts of that text (in a variety of ways). Take a look here, then we'll discuss below:<br />
<pre>reqs = [
# add 6 paragraphs
{'insertText': {
'text': 'Bold 1\nItal 2\n\tfoo\n\tbar\n\t\tbaz\n\t\tqux\nMono 3',
'objectId': textboxID,
}},
# shrink text from 48pt ("main point" textbox default) to 32pt
{'updateTextStyle': {
'objectId': textboxID,
'style': {'fontSize': {'magnitude': 32, 'unit': 'PT'}},
'textRange': {'type': 'ALL'},
'fields': 'fontSize',
}},
# change word 1 in para 1 ("Bold") to bold
{'updateTextStyle': {
'objectId': textboxID,
'style': {'bold': True},
'textRange': {'type': 'FIXED_RANGE', 'startIndex': 0, 'endIndex': 4},
'fields': 'bold',
}},
# change word 1 in para 2 ("Ital") to italics
{'updateTextStyle': {
'objectId': textboxID,
'style': {'italic': True},
'textRange': {'type': 'FIXED_RANGE', 'startIndex': 7, 'endIndex': 11},
'fields': 'italic'
}},
# change word 1 in para 7 ("Mono") to Courier New
{'updateTextStyle': {
'objectId': textboxID,
'style': {'fontFamily': 'Courier New'},
'textRange': {'type': 'FIXED_RANGE', 'startIndex': 36, 'endIndex': 40},
'fields': 'fontFamily'
}},
# bulletize everything
{'createParagraphBullets': {
'objectId': textboxID,
'textRange': {'type': 'ALL'},
}},
]</pre>
After the text is inserted, the first operation this code performs is to change the font size of all the text inserted ('ALL' means to format the entire text range) to 32 pt. The main point layout specifies a default font size of 48 pt, so this request shrinks the text so that everything fits and doesn't wrap. The 'fields' parameter specifies that only the 'fontSize' attribute is affected by this command, meaning leave others such as the font type, color, etc., alone.<br />
<br />
The next request bolds the first word of the first paragraph. Instead of 'ALL', the exact range for the first word is given. (NOTE: the end index is excluded from the range, so that's why it must be 4 instead of 3, or you're going to lose one character.) In this case, it's the "Bold" word from the first paragraph, "Bold 1". Again, 'fields' is present to indicate that only the font size should be affected by this request while everything else is left alone. The next directive is nearly identical except for italicizing the first word ("Ital") of the second paragraph ("Ital 2").<br />
<br />
After this we have a text style request to alter the font of the first word ("Mono") in the last paragraph ("Mono 3") to Courier New. The only other difference is that 'fields' is now 'fontFamily' instead of a flag. Finally, bulletize all paragraphs. Another call to <code>SLIDES.presentations().batchUpdate()</code> and we're done.<br />
<br />
<h2>
Conclusion</h2>
If you run the script, you should get output that looks something like this, with each <code>print()</code> representing execution of key parts of the application:<br />
<pre>$ python3 slides_format_text.py
** Create new slide deck
** Create "main point" layout slide & add titles
** Fetch "main point" slide title (textbox) ID
** Insert text & perform various formatting operations
DONE
</pre>
When the script has completed, you should have a new presentation with these slides:<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiTg6nr4Sqm905Q4aPdq_nLTtCwTwnbIeFKnkZShhc-xdiW6l6lkXpuv_7odR3MGrxyTG_Gtq66kPpWuuvYl3y95ecYaoyLmD5z7UqOCFFOEAVvizNMc6nnGNrYQfT6o9L47gyYzr4bDFw/s1600/LP027-title.png" imageanchor="1"><img border="0" height="220" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiTg6nr4Sqm905Q4aPdq_nLTtCwTwnbIeFKnkZShhc-xdiW6l6lkXpuv_7odR3MGrxyTG_Gtq66kPpWuuvYl3y95ecYaoyLmD5z7UqOCFFOEAVvizNMc6nnGNrYQfT6o9L47gyYzr4bDFw/s400/LP027-title.png" width="400" /></a><br />
<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEikNUfgXYXDfqzxt3OyZGCWzTx75KLIF0F5DyCQoMC800-wF0ETFWT6up9gm7QfxQZrA7X_HA4HljoYrswFOXjJBZqArt0T3W0OoJsxJG3HOEgJ0UFbiC_r6CKv6cvwFJYDBiqshKP9wh4/s1600/LP027-data.png" imageanchor="1"><img border="0" height="218" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEikNUfgXYXDfqzxt3OyZGCWzTx75KLIF0F5DyCQoMC800-wF0ETFWT6up9gm7QfxQZrA7X_HA4HljoYrswFOXjJBZqArt0T3W0OoJsxJG3HOEgJ0UFbiC_r6CKv6cvwFJYDBiqshKP9wh4/s400/LP027-data.png" width="400" /></a><br />
<br />
Below is the entire script for your convenience which runs on both Python 2 <b>and</b> Python 3 (unmodified!)—by using, copying, and/or modifying this code or any other piece of source from this blog, you implicitly agree to its <a href="http://apache.org/licenses/LICENSE-2.0">Apache2 license</a>:<br />
<pre><span style="font-size: small;"><b>from</b> __future__ <b>import</b> print_function
<b>from</b> apiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
SCOPES = 'https://www.googleapis.com/auth/presentations',
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> creds.invalid:
flow = client.flow_from_clientsecrets('client_secret.json', SCOPES)
creds = tools.run_flow(flow, store)
SLIDES = discovery.build('slides', 'v1', http=creds.authorize(Http()))
print('** Create new slide deck')
DATA = {'title': 'Slides text formatting DEMO'}
rsp = SLIDES.presentations().create(body=DATA).execute()
deckID = rsp['presentationId']
titleSlide = rsp['slides'][0]
titleID = titleSlide['pageElements'][0]['objectId']
subtitleID = titleSlide['pageElements'][1]['objectId']
print('** Create "main point" layout slide & add titles')
reqs = [
{'createSlide': {'slideLayoutReference': {'predefinedLayout': 'MAIN_POINT'}}},
{'insertText': {'objectId': titleID, 'text': 'Formatting text'}},
{'insertText': {'objectId': subtitleID, 'text': 'via the Google Slides API'}},
]
rsp = SLIDES.presentations().batchUpdate(body={'requests': reqs},
presentationId=deckID).execute().get('replies')
slideID = rsp[0]['createSlide']['objectId']
print('** Fetch "main point" slide title (textbox) ID')
rsp = SLIDES.presentations().pages().get(presentationId=deckID,
pageObjectId=slideID).execute().get('pageElements')
textboxID = rsp[0]['objectId']
print('** Insert text & perform various formatting operations')
reqs = [
# add 7 paragraphs
{'insertText': {
'text': 'Bold 1\nItal 2\n\tfoo\n\tbar\n\t\tbaz\n\t\tqux\nMono 3',
'objectId': textboxID,
}},
# shrink text from 48pt ("main point" textbox default) to 32pt
{'updateTextStyle': {
'objectId': textboxID,
'style': {'fontSize': {'magnitude': 32, 'unit': 'PT'}},
'textRange': {'type': 'ALL'},
'fields': 'fontSize',
}},
# change word 1 in para 1 ("Bold") to bold
{'updateTextStyle': {
'objectId': textboxID,
'style': {'bold': True},
'textRange': {'type': 'FIXED_RANGE', 'startIndex': 0, 'endIndex': 4},
'fields': 'bold',
}},
# change word 1 in para 2 ("Ital") to italics
{'updateTextStyle': {
'objectId': textboxID,
'style': {'italic': True},
'textRange': {'type': 'FIXED_RANGE', 'startIndex': 7, 'endIndex': 11},
'fields': 'italic'
}},
# change word 1 in para 6 ("Mono") to Courier New
{'updateTextStyle': {
'objectId': textboxID,
'style': {'fontFamily': 'Courier New'},
'textRange': {'type': 'FIXED_RANGE', 'startIndex': 36, 'endIndex': 40},
'fields': 'fontFamily'
}},
# bulletize everything
{'createParagraphBullets': {
'objectId': textboxID,
'textRange': {'type': 'ALL'},
}},
]
SLIDES.presentations().batchUpdate(body={'requests': reqs},
presentationId=deckID).execute()
print('DONE')
</span></pre>
As with our other code samples, you can now customize it to learn more about the API, integrate into other apps for your own needs, for a mobile frontend, sysadmin script, or a server-side backend!wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com1tag:blogger.com,1999:blog-6940043312015460811.post-7679377627429606562016-12-06T15:59:00.000-08:002017-02-13T15:32:41.251-08:00Modifying email signatures with the Gmail API<b>NOTE:</b> The content here is also available as a <a href="http://goo.gl/GbAOvb">video and overview post</a>, part of <a href="http://goo.gl/JpBQ40">this series</a>.<br />
<b><br /></b>
<b>UPDATE (Feb 2017):</b> Tweaked the code sample as the <code>isPrimary</code> flag may be missing from non-primary aliases; also added link above to video.<br />
<br />
<h2>
Introduction</h2>
In <a href="http://goo.gl/OfCbOz">a previous post</a>, I introduced Python developers to the <a href="http://developers.google.com/gmail">Gmail API</a> with a tutorial on how to search for threads with a minimum number of messages. Today, we'll explore another part of the API, covering the <a href="http://goo.gl/xY2g9O">settings endpoints that were added in mid-2016</a>. What's the big deal? Well, you couldn't use the API to read nor modify user settings before, and now you can!<br />
<br />
One example all of us can relate to is your personal email signature. Wouldn't it be great if we could modify it programmatically, say to include some recent news about you (perhaps a Tweet other social post), or maybe some random witty quote? You could then automate it to change once a quarter, or even hourly if you like being truly random!<br />
<br />
<h2>
Using the Gmail API</h2>
Our simple Python script won't be sending email nor reading user messages, so the only authorization scope needed is the one that accesses basic user settings (there's another for more <i>sensitive</i> user settings):<br />
<ul>
<li><code>https://www.googleapis.com/auth/gmail.settings.basic</code> — Manage basic Gmail user settings</li>
</ul>
See <a href="http://developers.google.com/gmail/api/auth/scopes">the documentation for a list of all Gmail API scopes</a> and what each of them mean. Since we've fully <a href="http://wescpy.blogspot.com/2014/11/authorized-google-api-access-from-python.html">covered</a> the authorization boilerplate in earlier posts and videos, including how to connect to the Gmail API, we're going to skip that here and jump right to the action. You can copy the boilerplate from other scripts you've written. Regardless, be sure to create an service endpoint to the API:<br />
<br />
<code>GMAIL = discovery.build('gmail', 'v1',<br />
http=creds.authorize(Http()))</code><br />
<br />
<h2>
What are "sendAs" email addresses?</h2>
First, a quick word about "sendAs" email addresses. Gmail lets you send email from addresses <i>other than</i> your actual Gmail address (considered your primary address). This lets you manage multiple accounts from the same Gmail user interface. (As expected, you need to own or otherwise have access to the alternate email addresses in order to do this.) However, most people only use their primary address, so you may not know about it. You can learn more about sendAs addresses <a href="http://support.google.com/mail/answer/22370">here</a> and <a href="http://support.google.com/a/answer/1710338">here</a>.<br />
<br />
Now you may be tempted to use the term "alias," especially because that word was mentioned in those Help pages you just looked at right? However for now, I'd recommend trying to avoid that terminology as it refers to something else in a G Suite/Google Apps context. Can't you see how we already got distracted from the core reason for this post? See, you almost forgot about email signatures already, right? If you stick with "sender addresses" or "sendAs email addresses," there won’t be any confusion.<br />
<br />
<h2>
Using a "Quote of the Day" in your email signature</h2>
The Python script we're exploring in this post sets a "Quote of the Day" (or "QotD" for short) as the signature of your primary sendAs address. Where does the QotD come from? Well, it can be as simple (and boring) as this function that returns a hardcoded string:<br />
<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmu60bNh43Tc8kzfWPHLxT9nGxedsRvfVID3GHwpoEXdJYry9KOn4hwEymtvWhstwm7iRrEGo7bbKg7JSEhzc_dhNK7Z3ftYTXJI068nvwMOOmSEM9r_HhCDVNf4BOqQMAX-9BMBusxmg/s1600/lp022-qotd-txt.png" imageanchor="1"><img border="0" height="37" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmu60bNh43Tc8kzfWPHLxT9nGxedsRvfVID3GHwpoEXdJYry9KOn4hwEymtvWhstwm7iRrEGo7bbKg7JSEhzc_dhNK7Z3ftYTXJI068nvwMOOmSEM9r_HhCDVNf4BOqQMAX-9BMBusxmg/s400/lp022-qotd-txt.png" width="400" /></a><br />
<br />
Cute but not very random right? A better idea is to choose from a number of quotes you have in a relational database w/columns for quotes & authors. Here’s some sample code for data in a SQLite database:<br />
<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZma_Qj5lF4TXAjI8pb2q8jCcMBl_rdPloSqKXXj0aOT0aRLgCtg7nwTsXjAAFYMZFLKuK3MrypwLvx-ixJDayy36c5NnFpDyUPXfHu1yswxL1QT8eC_9qnriWL3Ua_2gtWFBxI_ACzQ4/s1600/lp022-qotd-sql.png" imageanchor="1"><img border="0" height="118" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZma_Qj5lF4TXAjI8pb2q8jCcMBl_rdPloSqKXXj0aOT0aRLgCtg7nwTsXjAAFYMZFLKuK3MrypwLvx-ixJDayy36c5NnFpDyUPXfHu1yswxL1QT8eC_9qnriWL3Ua_2gtWFBxI_ACzQ4/s400/lp022-qotd-sql.png" width="400" /></a><br />
<br />
More random, which is cool, but this particular snippet isn't efficient because we’re selecting all rows and <i>then</i> choosing a quote randomly. Obviously there's a better way if a database is your data source. I prefer using a web service instead, coming in the form of a REST API. The code snippet here does just that:<br />
<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjcinDcoDexkQ4oFmJA8KswQFspotMUghOlkJc647y0Ux7kqobFeSoQIhBPOkMP1n0A3fJVW7b2YtYoFBIq6w5iO5l-rdYWr8rfznWww7lSGKuHC1uQrC1Ypo7g1-lVR6nXm8apycSYcl4/s1600/lp022-qotd-api.png" imageanchor="1"><img border="0" height="240" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjcinDcoDexkQ4oFmJA8KswQFspotMUghOlkJc647y0Ux7kqobFeSoQIhBPOkMP1n0A3fJVW7b2YtYoFBIq6w5iO5l-rdYWr8rfznWww7lSGKuHC1uQrC1Ypo7g1-lVR6nXm8apycSYcl4/s400/lp022-qotd-api.png" width="400" /></a><br />
<br />
You only need to find a quote-of-the-day service and provide its URL on line 8 that returns a JSON payload. Obviously you'll need a bit more scaffolding if this were a real service, but in this pseudocode example, you can assume that using <code>urllib.{,request.}urlopen()</code> works where the service sends back an empty string upon failure. To play it safe, this snippet falls back to the hardcoded string we saw earlier if the service doesn't return a quote, which comes back as a 2-tuple representing quote and author, respectively.<br />
<br />
<h2>
Setting your new email signature</h2>
Now that we're clear on the source for the QotD, we can focus on actually setting it as your new email signature. To do that, we need to get all of your sender (sendAs email) addresses—the goal is only to change your primary addresses (and none of the others if you have any):<br />
<pre>addresses = GMAIL.users().settings().sendAs().list(userId='me',
fields='sendAs(isPrimary,sendAsEmail)').execute().get('sendAs')
</pre>
As in <a href="http://goo.gl/OfCbOz">our other Gmail example</a>, a <code>userId</code> of <code>'me'</code> indicates the currently-authenticated user. The API will return a number of attributes. If know exactly which ones we want, we can specify them in with the <code>fields</code> attribute so as to control size of the return payload which may contribute to overall latency. In our case, we're requesting just the <code>sendAs.isPrimary</code> flag and <code>sendAs.sendAsEmail</code>, the actual email address string of the sender addresses. What's returned is a Python list consisting of all of your sendAs email addresses, which we cycle through to find the primary address:<br />
<pre><b>for</b> address <b>in</b> addresses:
<b>if</b> address.get('isPrimary'):
<b>break</b>
</pre>
One of your sender addresses must be primary, so unless there's a bug in Gmail, when control of the <code><b>for</b></code> loop concludes, <code>address</code> will point to your primary sender address. Now all you have to do is set the signature and confirm to the user:<br />
<pre>rsp = GMAIL.users().settings().sendAs().patch(userId='me',
sendAsEmail=address['sendAsEmail'], body=DATA).execute()
print("Signature changed to '%s'" % rsp['signature'])</pre>
If you only have <i>one</i> sender address, there's no need request all the addresses and loop through them looking for the primary address as we did above. In such circumstances, that entire request and loop are extraneous... just pass your email address as the <code>sendAsEmail</code> argument, like this:<br />
<pre>rsp = GMAIL.users().settings().sendAs().patch(userId='me',
sendAsEmail=<i>YOUR_EMAIL_ADDR_HERE</i>, body=DATA).execute()
</pre>
<h2>
Conclusion</h2>
That's all there is... just 26 lines of code. If we use the static string <code>qotd()</code> function above, your output when running this script will look like this:<br />
<pre>$ python gmail_change_sig.py # or python3
Signature changed to '&quot;I heart cats.&quot; ~anonymous'
$</pre>
Below is the entire script for your convenience which runs on both Python 2 <b>and</b> Python 3 (unmodified!). By using, copying, and/or modifying this code or any other piece of source from this blog, you implicitly agree to its <a href="http://apache.org/licenses/LICENSE-2.0">Apache2 license</a>:<br />
<pre><span style="font-size: small;"><b>from</b> __future__ <b>import</b> print_function
<b>from</b> apiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
<b>import</b> qotd
DATA = {'signature': qotd.qotd()} # quote source up-to-you!
SCOPES = 'https://www.googleapis.com/auth/gmail.settings.basic'
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> creds.invalid:
flow = client.flow_from_clientsecrets('client_secret.json', SCOPES)
creds = tools.run_flow(flow, store)
GMAIL = discovery.build('gmail', 'v1', http=creds.authorize(Http()))
# this entire block optional if you only have one sender address
addresses = GMAIL.users().settings().sendAs().list(userId='me',
fields='sendAs(isPrimary,sendAsEmail)').execute().get('sendAs')
<b>for</b> address <b>in</b> addresses:
<b>if</b> address.get('isPrimary'):
<b>break</b>
rsp = GMAIL.users().settings().sendAs().patch(userId='me',
sendAsEmail=address['sendAsEmail'], body=DATA).execute()
print("Signature changed to '%s'" % rsp['signature'])</span></pre>
As with our other code samples, you can now customize for your own needs, for a mobile frontend, sysadmin script, or a server-side backend, perhaps accessing other Google APIs.<br />
<br />
<h3>
Code challenge</h3>
Want to exercise your newfound knowledge of using the Gmail API's settings endpoints? Write a script that uses the API to manage filters or configure a vacation responder. <b>HINT</b>: take a look at <a href="http://developers.google.com/gmail/api">the official Gmail API docs</a>, including the pages specific to <a href="http://developers.google.com/gmail/api/guides/filter_settings">filters</a> and <a href="http://developers.google.com/gmail/api/guides/vacation_settings">vacation settings</a>.wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com2tag:blogger.com,1999:blog-6940043312015460811.post-79267359725279681882016-11-29T14:33:00.000-08:002016-11-30T10:58:14.831-08:00Generating slides from spreadsheet data<b>NOTE:</b> The code covered in this post are also available in a <a href="http://goo.gl/Yb06ZC">video walkthrough</a>.<br />
<br />
<br />
<h2>
Introduction</h2>
A common use case when you have data in a spreadsheet or database, is to find ways of making that data more visually appealing to others. This is the subject of today's post, where we'll walk through a simple Python script that generates presentation slides based on data in a spreadsheet using both the Google Sheets and Slides APIs.<br />
<br />
Specifically, we'll take all spreadsheet cells containing values and create an equivalent table on a slide with that data. The Sheet also features a pre-generated pie chart added from the <a href="http://goo.gl/GmDAPp">Explore in Google Sheets</a> feature that we'll import into a blank slide. Not only do we do that, but if the data in the Sheet is updated (meaning the chart is as well), then so can the imported chart image in the presentation. These are just two examples of generating slides from spreadsheet data. The example Sheet we're getting the data from for this script looks like this:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_O8-wuq2T0jj6_S3p2j2ImdRfSmMOY6hhcPnJvSCZYTbdRd4jkY2XTmlncHBIT3kNQZTFrdkJcwc9x1CdokRPiDD1EQPPb5NSrkTDg7HFchVLApaj6_1e_hHOYf51Wxfoj6skspAFSxw/s1600/LP026-toys-chart.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="355" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_O8-wuq2T0jj6_S3p2j2ImdRfSmMOY6hhcPnJvSCZYTbdRd4jkY2XTmlncHBIT3kNQZTFrdkJcwc9x1CdokRPiDD1EQPPb5NSrkTDg7HFchVLApaj6_1e_hHOYf51Wxfoj6skspAFSxw/s400/LP026-toys-chart.png" width="400" /></a></div>
<br />
The data in this Sheet originates from <a href="http://g.co/codelabs/sheets">the Google Sheets API codelab</a>. In the codelab, this <a href="https://github.com/googlecodelabs/sheets-api/blob/master/start/db.sqlite">data lives in a SQLite</a> relational database, and in <a href="http://wescpy.blogspot.com/2016/06/using-new-google-sheets-api.html">the previous post</a> covering how to migrate SQL data to Google Sheets, we "imported" that data into the Sheet we're using. As mentioned before, the pie chart comes from the Explore feature.<br />
<br />
<h2>
Using the Google Sheets & Slides APIs</h2>
The scopes needed for this application are the read-only scope for Sheets (to read the cell contents and the pie chart) and the read-write scope for Slides since we're creating a new presentation:<br />
<ul>
<li><code>'https://www.googleapis.com/auth/spreadsheets.readonly'</code> — Read-only access to Google Sheets and properties</li>
<li><code>'https://www.googleapis.com/auth/presentations'</code> — Read-write access to Slides and Slides presentation properties</li>
</ul>
If you're new to using Google APIs, we recommend reviewing <a href="http://goo.gl/cdm3kZ">earlier posts & videos</a> covering the setting up projects and the authorization boilerplate so that we can focus on the main app. Once we've authorized our app, two service endpoints are created, one for each API. The one for Sheets is saved to the <code>SHEETS</code> variable while the one for Slides goes to <code>SLIDES</code>.<br />
<br />
<h2>
Start with Sheets</h2>
The first thing to do is to grab all the data we need from the Google Sheet using the Sheets API. You can either supply your own Sheet with your own chart, or you can run the script from <a href="http://wescpy.blogspot.com/2016/06/using-new-google-sheets-api.html">the earlier post</a> mentioned earlier to create an identical Sheet as above. In either case, you need to provide the Sheet ID to read from, which is saved to the <code>sheetID</code> variable. Using its ID, we call <code>spreadsheets().values().get()</code> to pull out all the cells (as rows & columns) from the Sheet and save it to <code>orders</code>:<br />
<pre>sheetID = '. . .' # use your own!
orders = SHEETS.spreadsheets().values().get(range='Sheet1',
spreadsheetId=sheetID).execute().get('values')
</pre>
The next step is to call <code>spreadsheets().get()</code> to get all the sheets in the Sheet —there's only one, so grab it at index 0. Since this sheet only has one chart, we also use index 0 to get <i>that</i>:<br />
<pre>sheet = SHEETS.spreadsheets().get(spreadsheetId=sheetID,
ranges=['Sheet1']).execute().get('sheets')[0]
chartID = sheet['charts'][0]['chartId']
</pre>
That's it for Sheets. Everything from here on out takes places in Slides.<br />
<br />
<h2>
Create new Slides presentation</h2>
A new slide deck can be created with <code>SLIDES.presentations().create()</code>—or alternatively with the <a href="http://developers.google.com/drive/web">Google Drive API</a> which we won't do here. We'll name it, "Generating slides from spreadsheet data DEMO" and save its (new) ID along with the IDs of the title and subtitle textboxes on the (one) title slide created in the new deck:<br />
<pre>DATA = {'title': 'Generating slides from spreadsheet data DEMO'}
rsp = SLIDES.presentations().create(body=DATA).execute()
deckID = rsp['presentationId']
titleSlide = rsp['slides'][0]
titleID = titleSlide['pageElements'][0]['objectId']
subtitleID = titleSlide['pageElements'][1]['objectId']</pre>
<h2>
Create slides for table & chart</h2>
A mere title slide doesn't suffice as we need a place for the cell data as well as the pie chart, so we'll create slides for each. While we're at it, we might as well fill in the text for the presentation title and subtitle. These requests are self-explanatory as you can see below in the <code>reqs</code> variable. The <code>SLIDES.presentations().batchUpdate()</code> method is then used to send the four commands to the API. Upon return, save the IDs for both the cell table slide as well as the blank slide for the chart:<br />
<pre><span style="font-size: small;">reqs = [
{'createSlide': {'slideLayoutReference': {'predefinedLayout': 'TITLE_ONLY'}}},
{'createSlide': {'slideLayoutReference': {'predefinedLayout': 'BLANK'}}},
{'insertText': {'objectId': titleID, 'text': 'Importing Sheets data'}},
{'insertText': {'objectId': subtitleID, 'text': 'via the Google Slides API'}},
]
rsp = SLIDES.presentations().batchUpdate(body={'requests': reqs},
presentationId=deckID).execute().get('replies')
tableSlideID = rsp[0]['createSlide']['objectId']
chartSlideID = rsp[1]['createSlide']['objectId']
</span></pre>
Note the order of the requests. The create-slide requests come first followed by the text inserts. Responses that come back from the API are returned in the same order as they were sent, hence why the cell table slide ID comes back first (index 0) followed by the chart slide ID (index 1). The text inserts don't have any meaningful return values and are thus ignored.
<br />
<br />
<h2>
Filling out the table slide</h2>
Now let's focus on the table slide. There are two things we need to accomplish. In the previous set of requests, we asked the API to create a "title only" slide, meaning there's (only) a textbox for the slide title. The next snippet of code gets all the page elements on that slide so we can get the ID of that textbox, the only thing <i>on</i> that page:
<br />
<pre>rsp = SLIDES.presentations().pages().get(presentationId=deckID,
pageObjectId=tableSlideID).execute().get('pageElements')
textboxID = rsp[0]['objectId'] </pre>
On this slide, we need to add the cell table for the Sheet data, so a create-table request takes care of that. The required elements in such a call include the ID of the slide the table should go on as well as the total number of rows and columns desired. Fortunately all that are available from <code>tableSlideID</code> and <code>orders</code> saved earlier. Oh, and add a title for this table slide too. Here's the code:<br />
<pre>reqs = [
{'createTable': {
'elementProperties': {'pageObjectId': tableSlideID},
'rows': len(orders),
'columns': len(orders[0])},
},
{'insertText': {'objectId': textboxID, 'text': 'Toy orders'}},
]
rsp = SLIDES.presentations().batchUpdate(body={'requests': reqs},
presentationId=deckID).execute().get('replies')
tableID = rsp[0]['createTable']['objectId']</pre>
Another call to <code>SLIDES.presentations().batchUpdate()</code> and we're done, saving the ID of the newly-created table. Next, we'll fill in each cell of that table.<br />
<br />
<h2>
Populate table & add chart image</h2>
The first set of requests needed now fill in each cell of the table. The most compact way to issue these requests is with a double-<code><b>for</b></code> loop list comprehension. The first loops over the rows while the second loops through each column (of each row). Magically, this creates all the text insert requests needed.<br />
<pre>reqs = [
{'insertText': {
'objectId': tableID,
'cellLocation': {'rowIndex': i, 'columnIndex': j},
'text': str(data),
}} for i, order in enumerate(orders) for j, data in enumerate(order)]
</pre>
The final request "imports" the chart from the Sheet onto the blank slide whose ID we saved earlier. Note, while the dimensions below seem completely arbitrary, be assured we're using the same size & transform as a blank rectangle we drew on the slide earlier (and read those values from). The alternative would be to use math to come up with your object dimensions. Here is the code we're talking about, followed by the actual call to the API:
<br />
<pre>reqs.append({'createSheetsChart': {
'spreadsheetId': sheetID,
'chartId': chartID,
'linkingMode': 'LINKED',
'elementProperties': {
'pageObjectId': chartSlideID,
'size': {
'height': {'magnitude': 7075, 'unit': 'EMU'},
'width': {'magnitude': 11450, 'unit': 'EMU'}
},
'transform': {
'scaleX': 696.6157,
'scaleY': 601.3921,
'translateX': 583875.04,
'translateY': 444327.135,
'unit': 'EMU',
},
},
}})
SLIDES.presentations().batchUpdate(body={'requests': reqs},
presentationId=deckID).execute()
</pre>
Once all the requests have been created, send them to the Slides API then we're done. (In the actual app, you'll see we've sprinkled various <code>print()</code> calls to let the user knows which steps are being executed.<br />
<br />
<h2>
Conclusion</h2>
The entire script clocks in at just under 100 lines of code... see below. If you run it, you should get output that looks something like this:<br />
<pre>$ python3 slides_table_chart.py
** Fetch Sheets data
** Fetch chart info from Sheets
** Create new slide deck
** Create 2 slides & insert slide deck title+subtitle
** Fetch table slide title (textbox) ID
** Create table & insert table slide title
** Fill table cells & create linked chart to Sheets
DONE
</pre>
When the script has completed, you should have a new presentation with these 3 slides:<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhM6kMNeEcxgM9CJh0z4riRXvorMY56h4BKAia0u0NI-5447bRzDLbpe5tt4Kgi9tHeK1VH0cF-KCtCEJfpRI9iI689zJZRCtn3CQZW3s6FcdiSy4u0CoSLk-dxsLvfVyYJ0ssrKUQ8lOQ/s1600/LP026-slide1.png" imageanchor="1"><img border="0" height="218" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhM6kMNeEcxgM9CJh0z4riRXvorMY56h4BKAia0u0NI-5447bRzDLbpe5tt4Kgi9tHeK1VH0cF-KCtCEJfpRI9iI689zJZRCtn3CQZW3s6FcdiSy4u0CoSLk-dxsLvfVyYJ0ssrKUQ8lOQ/s400/LP026-slide1.png" width="400" /></a><br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrGWewss-vbLEIFnHChAN4VJoVKy_NVmAIiM9gwmIZBb-OKXx86b2WjsdVkKnrnr7StLZehSoTV3QyXBTWm0FTqmCZGy9rp6Zft-phksbhMLJIIKNxfgjWHe6b_oTVBaLnm6vQGn_Vco4/s1600/LP026-slide2.png" imageanchor="1"><img border="0" height="221" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrGWewss-vbLEIFnHChAN4VJoVKy_NVmAIiM9gwmIZBb-OKXx86b2WjsdVkKnrnr7StLZehSoTV3QyXBTWm0FTqmCZGy9rp6Zft-phksbhMLJIIKNxfgjWHe6b_oTVBaLnm6vQGn_Vco4/s400/LP026-slide2.png" width="400" /></a><br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhiQ3mlPmGwO7QUaSRbxo97Dpk8_c82aNV9GhFtIxQDny7JH7Ku7A9FVeRo5xKbonylmqvop1RbPCpOaOrhmqNEeySAqrAkPxEyVzyMOGiFIns7Vzoq_vTWuPk60G05SKp1dAdDQjUDp4Y/s1600/LP026-slide3.png" imageanchor="1"><img border="0" height="217" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhiQ3mlPmGwO7QUaSRbxo97Dpk8_c82aNV9GhFtIxQDny7JH7Ku7A9FVeRo5xKbonylmqvop1RbPCpOaOrhmqNEeySAqrAkPxEyVzyMOGiFIns7Vzoq_vTWuPk60G05SKp1dAdDQjUDp4Y/s400/LP026-slide3.png" width="400" /></a><br /><br />
Below is the entire script for your convenience which runs on both Python 2 <b>and</b> Python 3 (unmodified!). If I were to divide the script into major sections, they would be represented by each of the <code>print()</code> calls above. Here's the complete script—by using, copying, and/or modifying this code or any other piece of source from this blog, you implicitly agree to its <a href="http://apache.org/licenses/LICENSE-2.0">Apache2 license</a>:<br />
<pre><span style="font-size: small;"><b>from</b> __future__ <b>import</b> print_function
<b>from</b> apiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
SCOPES = (
'https://www.googleapis.com/auth/spreadsheets.readonly',
'https://www.googleapis.com/auth/presentations',
)
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> creds.invalid:
flow = client.flow_from_clientsecrets('client_secret.json', SCOPES)
creds = tools.run_flow(flow, store)
HTTP = creds.authorize(Http())
SHEETS = discovery.build('sheets', 'v4', http=HTTP)
SLIDES = discovery.build('slides', 'v1', http=HTTP)
print('** Fetch Sheets data')
sheetID = '. . .' # use your own!
orders = SHEETS.spreadsheets().values().get(range='Sheet1',
spreadsheetId=sheetID).execute().get('values')
print('** Fetch chart info from Sheets')
sheet = SHEETS.spreadsheets().get(spreadsheetId=sheetID,
ranges=['Sheet1']).execute().get('sheets')[0]
chartID = sheet['charts'][0]['chartId']
print('** Create new slide deck')
DATA = {'title': 'Generating slides from spreadsheet data DEMO'}
rsp = SLIDES.presentations().create(body=DATA).execute()
deckID = rsp['presentationId']
titleSlide = rsp['slides'][0]
titleID = titleSlide['pageElements'][0]['objectId']
subtitleID = titleSlide['pageElements'][1]['objectId']
print('** Create 2 slides & insert slide deck title+subtitle')
reqs = [
{'createSlide': {'slideLayoutReference': {'predefinedLayout': 'TITLE_ONLY'}}},
{'createSlide': {'slideLayoutReference': {'predefinedLayout': 'BLANK'}}},
{'insertText': {'objectId': titleID, 'text': 'Importing Sheets data'}},
{'insertText': {'objectId': subtitleID, 'text': 'via the Google Slides API'}},
]
rsp = SLIDES.presentations().batchUpdate(body={'requests': reqs},
presentationId=deckID).execute().get('replies')
tableSlideID = rsp[0]['createSlide']['objectId']
chartSlideID = rsp[1]['createSlide']['objectId']
print('** Fetch table slide title (textbox) ID')
rsp = SLIDES.presentations().pages().get(presentationId=deckID,
pageObjectId=tableSlideID).execute().get('pageElements')
textboxID = rsp[0]['objectId']
print('** Create table & insert table slide title')
reqs = [
{'createTable': {
'elementProperties': {'pageObjectId': tableSlideID},
'rows': len(orders),
'columns': len(orders[0])},
},
{'insertText': {'objectId': textboxID, 'text': 'Toy orders'}},
]
rsp = SLIDES.presentations().batchUpdate(body={'requests': reqs},
presentationId=deckID).execute().get('replies')
tableID = rsp[0]['createTable']['objectId']
print('** Fill table cells & create linked chart to Sheets')
reqs = [
{'insertText': {
'objectId': tableID,
'cellLocation': {'rowIndex': i, 'columnIndex': j},
'text': str(data),
}} <b>for</b> i, order <b>in</b> enumerate(orders) <b>for</b> j, data <b>in</b> enumerate(order)]
reqs.append({'createSheetsChart': {
'spreadsheetId': sheetID,
'chartId': chartID,
'linkingMode': 'LINKED',
'elementProperties': {
'pageObjectId': chartSlideID,
'size': {
'height': {'magnitude': 7075, 'unit': 'EMU'},
'width': {'magnitude': 11450, 'unit': 'EMU'}
},
'transform': {
'scaleX': 696.6157,
'scaleY': 601.3921,
'translateX': 583875.04,
'translateY': 444327.135,
'unit': 'EMU',
},
},
}})
SLIDES.presentations().batchUpdate(body={'requests': reqs},
presentationId=deckID).execute()
print('DONE')
</span></pre>
As with our other code samples, you can now customize it to learn more about the API, integrate into other apps for your own needs, for a mobile frontend, sysadmin script, or a server-side backend!<br />
<br />
<h2>
Code challenge</h2>
Given the knowledge you picked up from this post and its code sample, augment the script with another call to the Sheets API that updates the number of toys ordered by one of the customers, then add the corresponding call to the Slides API that refreshes the linked image based on the changes made to the Sheet (and chart). <b>EXTRA CREDIT:</b> Use the Google Drive API to monitor the Sheet so that any updates to toy orders will result in an "automagic" update of the chart image in the Slides presentation.wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com3tag:blogger.com,1999:blog-6940043312015460811.post-76419636950656712812016-11-09T10:00:00.000-08:002016-12-02T01:33:03.193-08:00Replacing text & images with the Google Slides API with Python<b>NOTE:</b> The code covered in this post are also available in a <a href="http://goo.gl/o6EFwk">video walkthrough</a> however the code here differs slightly, featuring some minor improvements to the code in the video.<br />
<br />
<h2>
Introduction</h2>
One of the critical things developers have not been able to do previously was access <a href="http://docs.google.com/presentation">Google Slides</a> presentations programmatically. To address this "shortfall," the Slides team <a href="http://goo.gl/CG86Vn">pre-announced their first API a few months ago at Google I/O 2016</a>—also see <a href="http://youtu.be/Gk-xpjgUwx4">full announcement video</a> (40+ mins). In early November, the G Suite product team <a href="http://gsuite-developers.googleblog.com/2016/11/introducing-google-slides-api.html">officially launched the API</a>, finally giving <i>all</i> developers access to build or edit Slides presentations from their applications.<br />
<br />
In this post, I'll walk through a simple example featuring an existing Slides presentation template with a single slide. On this slide are placeholders for a presentation name and company logo, as illustrated below:<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhL_P5RagNA1G-7X7ekw4yExLmY14EFqqze6O6dB1oCTTrusPlDhI3Ji1-HqMhkeNwKw7Vsku4FPWlH5sWHVXODPkBKDiHH7Fded7hl7zsocETUeJx8Rxpo2jc1KJ2Qn5LDMM4V_wI1yiw/s1600/LP025-before.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="226" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhL_P5RagNA1G-7X7ekw4yExLmY14EFqqze6O6dB1oCTTrusPlDhI3Ji1-HqMhkeNwKw7Vsku4FPWlH5sWHVXODPkBKDiHH7Fded7hl7zsocETUeJx8Rxpo2jc1KJ2Qn5LDMM4V_wI1yiw/s400/LP025-before.png" width="400" /></a></div>
<br />
One of the obvious use cases that will come to mind is to take a presentation template replete with "variables" and placeholders, and auto-generate decks from the same source but created with different data for different customers. For example, here's what a "completed" slide would look like after the proxies have been replaced with "real data:"<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEguvNQd0ftyErjhpkpbHu_b55rLh1BBeVArGMiB2MtQYZV2xH_6kvqOSgi9Lj-jgOIitNfEBWB-NvYEYqXyCkTd-WuLOuepIOwEeoqloXg8XNIWJrSVOhGMx-itAI6DtBfwD_U9Z-kH4Q8/s1600/LP025-after.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="227" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEguvNQd0ftyErjhpkpbHu_b55rLh1BBeVArGMiB2MtQYZV2xH_6kvqOSgi9Lj-jgOIitNfEBWB-NvYEYqXyCkTd-WuLOuepIOwEeoqloXg8XNIWJrSVOhGMx-itAI6DtBfwD_U9Z-kH4Q8/s400/LP025-after.png" width="400" /></a></div>
<br />
<h2>
Using the Google Slides API</h2>
We need to edit/write into a Google Slides presentation, meaning the read-write scope from all Slides API scopes below:<br />
<ul>
<li><code>'https://www.googleapis.com/auth/presentations'</code> — Read-write access to Slides and Slides presentation properties</li>
<li><code>'https://www.googleapis.com/auth/presentations.readonly'</code> — View-only access to Slides presentations and properties</li>
<li><code>'https://www.googleapis.com/auth/drive'</code> — Full access to users' files on Google Drive</li>
</ul>
Why is the Google Drive API scope listed above? Well, think of it this way: APIs like the Google Sheets and Slides APIs were created to perform spreadsheet and presentation operations. However, importing/exporting, copying, and sharing are all <i>file</i>-based operations, thus where the Drive API fits in. If you need a review of <i>its</i> scopes, check out <a href="http://developers.google.com/drive/v3/web/about-auth#OAuth2Authorizing">the Drive auth scopes page</a> in the docs. Copying a file requires the full Drive API scope, hence why it's listed above. If you're not going to copy any files and only performing actions with the Slides API, you can of course leave it out.<br />
<br />
Since we've fully <a href="http://wescpy.blogspot.com/2014/11/authorized-google-api-access-from-python.html">covered</a> the authorization boilerplate fully in earlier posts and videos, we're going to skip that here and jump right to the action.<br />
<br />
<h2>
Getting started</h2>
What are we doing in today's code sample? We start with a slide template file that has "variables" or placeholders for a title and an image. The application code will go then replace these proxies with the actual desired text and image, with the goal being that this scaffolding will allow you to automatically generate multiple slide decks but "tweaked" with "real" data that gets substituted into each slide deck.<br />
<br />
The title slide template file is <code>TMPFILE</code>, and the image we're using as the company logo is the Google Slides product icon whose filename is stored as the <code>IMG_FILE</code> variable in my Google Drive. Be sure to use your own image and template files! These definitions plus the scopes to be used in this script are defined like this:<br />
<pre>IMG_FILE = 'google-slides.png' # use your own!
TMPLFILE = 'title slide template' # use your own!
SCOPES = (
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/presentations',
)</pre>
Skipping past most of the OAuth2 boilerplate, let's move ahead to creating the API service endpoints. The Drive API name is (of course) <code>'drive'</code>, currently on <code>'v3'</code>, while the Slides API is <code>'slides'</code> and <code>'v1'</code> in the following call to create a signed HTTP client that's shared with a pair of calls to the <code>apiclient.discovery.build()</code> function to create the API service endpoints:<br />
<pre>HTTP = creds.authorize(Http())
DRIVE = discovery.build('drive', 'v3', http=HTTP)
SLIDES = discovery.build('slides', 'v1', http=HTTP)
</pre>
<h2>
Copy template file</h2>
The first step of the "real" app is to find and copy the template file <code>TMPLFILE</code>. To do this, we'll use <code>DRIVE.files().list()</code> to query for the file, then grab the first match found. Then we'll use <code>DRIVE.files().copy()</code> to copy the file and name it <code>'Google Slides API template DEMO'</code>:
<br />
<pre><span style="font-size: small;">rsp = DRIVE.files().list(q="name='%s'" % TMPLFILE).execute().get('files')[0]
DATA = {'name': 'Google Slides API template DEMO'}
print('** Copying template %r as %r' % (rsp['name'], DATA['name']))
DECK_ID = DRIVE.files().copy(body=DATA, fileId=rsp['id']).execute().get('id')
</span></pre>
<h2>
Find image placeholder</h2>
Next, we'll ask the Slides API to get the data on the first (and only) slide in the deck. Specifically, we want the dimensions of the image placeholder. Later on, we will use those properties when replacing it with the company logo, so that it will be automatically resized and centered into the same spot as the image placeholder.<br />
The <code>SLIDES.presentations().get()</code> method is used to read the presentation metadata. Returned is a payload consisting of everything in the presentation, the masters, layouts, and of course, the slides themselves. We only care about the slides, so we get that from the payload. And since there's only <i>one</i> slide, we grab it at index 0. Once we have the slide, we're loop through all of the elements on that page and stop when we find the rectangle (image placeholder):<br />
<pre>print('** Get slide objects, search for image placeholder')
slide = SLIDES.presentations().get(presentationId=DECK_ID
).execute().get('slides')[0]
obj = None
for obj in slide['pageElements']:
if obj['shape']['shapeType'] == 'RECTANGLE':
break
</pre>
<h2>
Find image file</h2>
At this point, the <code>obj</code> variable points to that rectangle. What are we going to replace it with? The company logo, which we now query for using the Drive API:
<br />
<pre>print('** Searching for icon file')
rsp = DRIVE.files().list(q="name='%s'" % IMG_FILE).execute().get('files')[0]
print(' - Found image %r' % rsp['name'])
img_url = '%s&access_token=%s' % (
DRIVE.files().get_media(fileId=rsp['id']).uri, creds.access_token) </pre>
The query code is similar to when we searched for the template file earlier. The trickiest thing about this snippet is that we need a full URL that points directly to the company logo. We use the <code>DRIVE.files().get_media()</code> method to create that request but <i>don't</i> execute it. Instead, we dig inside the request object itself and grab the file's URI and merge it with the current access token so what we're left with is a valid URL that the Slides API can use to read the image file and create it in the presentation.<br />
<br />
<h2>
Replace text and image</h2>
Back to the Slides API for the final steps: replace the title (text variable) with the desired text, add the company logo with the same size and transform as the image placeholder, and delete the image placeholder as it's no longer needed:
<br />
<pre>print('** Replacing placeholder text and icon')
reqs = [
{'replaceAllText': {
'containsText': {'text': '{{NAME}}'},
'replaceText': 'Hello World!'
}},
{'createImage': {
'url': img_url,
'elementProperties': {
'pageObjectId': slide['objectId'],
'size': obj['size'],
'transform': obj['transform'],
}
}},
{'deleteObject': {'objectId': obj['objectId']}},
]
SLIDES.presentations().batchUpdate(body={'requests': reqs},
presentationId=DECK_ID).execute()
print('DONE')
</pre>
Once all the requests have been created, send them to the Slides API then let the user know everything is done.<br />
<br />
<h2>
Conclusion</h2>
That's the entire script, just under 60 lines of code. If you watched the video, you may notice a few minor differences in the code. One is use of the <code>fields</code> parameter in the Slides API calls. They represent the use of field masks, which is a separate topic on its own. As you're learning the API now, it may cause unnecessary confusion, so it's okay to disregard them for now. The other difference is an improvement in the <code>replaceAllText</code> request—the old way in the video is now deprecated, so go with what we've replaced it with in this post.<br />
<br />
If your template slide deck and image is in your Google Drive, and you've modified the filenames and run the script, you should get output that looks something like this:<br />
<pre>$ python3 slides_template.py
** Copying template 'title slide template' as 'Google Slides API template DEMO'
** Get slide objects, search for image placeholder
** Searching for icon file
- Found image 'google-slides.png'
** Replacing placeholder text and icon
DONE
</pre>
Below is the entire script for your convenience which runs on both Python 2 <b>and</b> Python 3 (unmodified!). If I were to divide the script into major sections, they would be:<br />
<ul>
<li>Get creds & build API service endpoints</li>
<li>Copy template file</li>
<li>Get image placeholder size & transform (for replacement image later)</li>
<li>Get secure URL for company logo</li>
<li>Build and send Slides API requests to...</li>
<ul>
<li>Replace slide title variable with "Hello World!"</li>
<li>Create image with secure URL using placeholder size & transform</li>
<li>Delete image placeholder</li>
</ul>
</ul>
Here's the complete script—by using, copying, and/or modifying this code or any other piece of source from this blog, you implicitly agree to its <a href="http://apache.org/licenses/LICENSE-2.0">Apache2 license</a>:<br />
<pre><span style="font-size: small;"><b>from</b> __future__ <b>import</b> print_function
<b>from</b> apiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
IMG_FILE = 'google-slides.png' # use your own!
TMPLFILE = 'title slide template' # use your own!
SCOPES = (
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/presentations',
)
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> 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)
SLIDES = discovery.build('slides', 'v1', http=HTTP)
rsp = DRIVE.files().list(q="name='%s'" % TMPLFILE).execute().get('files')[0]
DATA = {'name': 'Google Slides API template DEMO'}
print('** Copying template %r as %r' % (rsp['name'], DATA['name']))
DECK_ID = DRIVE.files().copy(body=DATA, fileId=rsp['id']).execute().get('id')
print('** Get slide objects, search for image placeholder')
slide = SLIDES.presentations().get(presentationId=DECK_ID,
fields='slides').execute().get('slides')[0]
obj = None
<b>for</b> obj <b>in</b> slide['pageElements']:
<b>if</b> obj['shape']['shapeType'] == 'RECTANGLE':
<b>break</b>
print('** Searching for icon file')
rsp = DRIVE.files().list(q="name='%s'" % IMG_FILE).execute().get('files')[0]
print(' - Found image %r' % rsp['name'])
img_url = '%s&access_token=%s' % (
DRIVE.files().get_media(fileId=rsp['id']).uri, creds.access_token)
print('** Replacing placeholder text and icon')
reqs = [
{'replaceAllText': {
'containsText': {'text': '{{NAME}}'},
'replaceText': 'Hello World!'
}},
{'createImage': {
'url': img_url,
'elementProperties': {
'pageObjectId': slide['objectId'],
'size': obj['size'],
'transform': obj['transform'],
}
}},
{'deleteObject': {'objectId': obj['objectId']}},
]
SLIDES.presentations().batchUpdate(body={'requests': reqs},
presentationId=DECK_ID).execute()
print('DONE')
</span></pre>
As with our other code samples, you can now customize it to learn more about the API, integrate into other apps for your own needs, for a mobile frontend, sysadmin script, or a server-side backend!<br />
<br />
<h2>
Code challenge</h2>
Add more slides and/or text variables and modify the script replace them too. <b>EXTRA CREDIT</b>: Change the image-based image placeholder to a text-based image placeholder, say a textbox with the text, "{{COMPANY_LOGO}}" and use the <code>replaceAllShapesWithImage</code> request to perform the image replacement. By making this one change, your code should be simplified from the image-based image replacement solution we used in this post.wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com4tag:blogger.com,1999:blog-6940043312015460811.post-64889692144898416322016-09-28T12:40:00.001-07:002016-09-28T19:32:13.525-07:00Formatting cells in Google Sheets with Python<h2>
Introduction</h2>
One of the critical things that developers have not been able to do in previous versions of the Google Sheets API is to format cells... <i>that's</i> a big deal! Anyway, the past is the past, and I choose to look ahead. In my <a href="http://wescpy.blogspot.com/2016/06/using-new-google-sheets-api.html">earlier post on the Google Sheets API</a>, I introduced Sheets API v4 with a tutorial on how to transfer data from a SQL database to a Sheet. You'd do that primarily to make database data more presentable rather than deliberately switching to a storage mechanism with weaker querying capabilities. At the very end of that post, I challenged readers to try formatting. If you got stuck, confused, or haven't had a chance yet, today's your lucky day. One caveat is that there's more JavaScript in this post than Python... you've been warned!<br />
<br />
<h2>
Using the Google Sheets API</h2>
We need to write (formatting) into a Google Sheet, so we need the same scope as last time, read-write:<br />
<ul>
<li><code>'https://www.googleapis.com/auth/spreadsheets'</code> — Read-write access to Sheets and Sheet properties</li>
</ul>
Since we've fully <a href="http://wescpy.blogspot.com/2014/11/authorized-google-api-access-from-python.html">covered</a> the authorization boilerplate fully in earlier posts and videos, including how to connect to the Sheets API, we're going to skip that here and jump right to the action.<br />
<br />
<h2>
Formatting cells in Google Sheets</h2>
The way the API works, in general, is to take one or more commands, and execute them on the Sheet. This comes in the form of individual requests, either to cells, a Sheet, or the entire spreadsheet. A group if requests is organized as a JavaScript array. Each request in the array is represented by a JSON object. Yes, this part of the post may seem like a long exercise in JavaScript, but stay with me here. Continuing... once your array is complete, you send all the requests to the Sheet using the <code>SHEETS.spreadsheets().batchUpdate()</code> command. Here's pseudocode sending 5 commands to the API:<br />
<pre>SHEET_ID = . . .
reqs = {'requests': [
{'updateSheetProperties':
. . .
{'repeatCell':
. . .
{'setDataValidation':
. . .
{'sortRange':
. . .
{'addChart':
. . .
]}
SHEETS.spreadsheets().batchUpdate(
spreadsheetId=SHEET_ID, body=reqs).execute()
</pre>
What we're executing will be similar. The target spreadsheet will be the one you get when you run the code from <a href="http://wescpy.blogspot.com/2016/06/using-new-google-sheets-api.html">the previous post</a>, only without the timestamp in its title as it's unnecessary:<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh8zEn_7R2zA0z4aV8z931P4BH_yQIcWBxYPXGPrzCRC1EdLsU9yueElYNYiYxXjtngh4koYJ3m_3ZOTugyvHT5G_sB6KPfB2SRamXqf_3PsoSZ2kVa4GIk_s60m0c1bK0ZZuRLXSiVe-0/s1600/Screen+Shot+2016-09-27+at+19.21.34.png" imageanchor="1"><img border="0" height="167" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh8zEn_7R2zA0z4aV8z931P4BH_yQIcWBxYPXGPrzCRC1EdLsU9yueElYNYiYxXjtngh4koYJ3m_3ZOTugyvHT5G_sB6KPfB2SRamXqf_3PsoSZ2kVa4GIk_s60m0c1bK0ZZuRLXSiVe-0/s400/Screen+Shot+2016-09-27+at+19.21.34.png" width="400" /></a><br />
<br />
Once you've run the earlier script and created a Sheet of your own, be sure to assign it to the <code>SHEET_ID</code> variable. The goal is to send enough formatting commands to arrive at the same spreadsheet but with improved visuals:<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEinfpD3wslYvsmxXYVCEaEFQVXPLOANSQYkrbbFgEpmCFZfh825RCxskBCRHco0AydZnzAGmW3MXuMgpkhpALmLfcfgXli8LRC_xeC5g54yxvHXR8FTV9kjL6QM9YdQhizz3-1_6WdV0-4/s1600/Screen+Shot+2016-09-27+at+19.25.16.png" imageanchor="1"><img border="0" height="165" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEinfpD3wslYvsmxXYVCEaEFQVXPLOANSQYkrbbFgEpmCFZfh825RCxskBCRHco0AydZnzAGmW3MXuMgpkhpALmLfcfgXli8LRC_xeC5g54yxvHXR8FTV9kjL6QM9YdQhizz3-1_6WdV0-4/s400/Screen+Shot+2016-09-27+at+19.25.16.png" width="400" /></a><br />
<br />
Four (4) requests are needed to bring the original Sheet to this state:<br />
<ol>
<li> Set the top row as "frozen", meaning it doesn't scroll even when the data does</li>
<li> Also bold the first row, as these are the column headers</li>
<li> Format column E as US currency with dollar sign & 2 decimal places</li>
<li> Set data validation for column F, requiring values from a fixed set</li>
</ol>
<h2>
Creating Sheets API requests</h2>
As mentioned before, each request is represented by a JSON object, cleverly disguised as Python dictionaries in this post, and the entire request array is implemented as a Python list. Let's take a look at what it takes to together the individual requests:
<br />
<br />
<h3>
Frozen rows</h3>
Frozen rows is a Sheet property, so in order to change it, users must employ the <code>updateSheetProperties</code> command. Specifically, <code>frozenRowCount</code> is a grid property, meaning the field that must be updated is <code>gridProperties.</code><code>frozenRowCount</code>, set to 1. Here's the Python dict (that gets converted to a JSON object) representing this request:
<br />
<pre>{'updateSheetProperties': {
'properties': {'gridProperties': {'frozenRowCount': 1}},
'fields': 'gridProperties.frozenRowCount',
}},
</pre>
The <code>properties</code> attribute specifies what is changing and what the new value is. The <code>fields</code> property serves as an attribute "mask." It's how you specify what to alter and what to leave alone when applying a change. In this case, both the <code>properties</code> and <code>fields</code> attributes refer to the same thing: the frozen row count grid property. If you leave out the <code>fields</code> attribute here, sure the frozen row count would be set but all other grid properties would be undone, not such a good side effect. It's okay if it doesn't make a lot of sense yet... there are more examples coming.
<br />
<br />
<h3>
Bold formatting</h3>
Text formatting, such as bold or italics, is a cell operation. Since we want to apply this formatting to multiple cells, the correct command is, <code>repeatCell</code>. Specifically, what needs to be changed about a cell? A cell's <code>userEnteredFormat.textFormat.bold</code> attribute. This is a simple Boolean value, so we set it to <code>True</code>. The <code>fields</code> masks are as described above... we need to tell the API to explicitly change the just <code>userEnteredFormat.textFormat.bold</code> attribute. Everything else should stay as-is.<br />
<br />
The last thing we need is to tell the API <i>which</i> cells in the Sheet should be formatted. For this we have <code>range</code>. This attribute tells the API what Sheet (by ID) and which cells (column and row ranges) in that Sheet to format. Above, you see that we want to bold just one row, row #1. Similarly, there are currently six columns to bold, A-F.<br />
<br />
However, like most modern computer systems, the API supports start and end index values beginning with zero... not alphabetic column names nor rows consisting of whole numbers, <i>and</i> the range is exclusive of the end index, meaning it goes up to but does <i>not</i> include the ending row or column. For row 1, this means a starting index of 0 and an ending index of 1. Similarly, columns A-F have start & end index value of 0 and 6, respectively. Visually, here's how you compare traditional row & column values to 0-based index counting:<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEghndy90UTNSPosaNAZtJpGXqxCAOahMEesfzIfjhyumMfXn9KAWepZdt4wUl1Rvcx4_jqY2pd5mAyZy2Gf1jmyBGxdSq8xUXJAGvrNCx2yPPZV6_FgEKTIMahe1WCHcMyMXwlQS5SkWco/s1600/lp021-index-values.png" imageanchor="1"><img border="0" height="254" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEghndy90UTNSPosaNAZtJpGXqxCAOahMEesfzIfjhyumMfXn9KAWepZdt4wUl1Rvcx4_jqY2pd5mAyZy2Gf1jmyBGxdSq8xUXJAGvrNCx2yPPZV6_FgEKTIMahe1WCHcMyMXwlQS5SkWco/s640/lp021-index-values.png" width="640" /></a><br />
<br />
Here's the dict representing <i>this</i> request:
<br />
<pre>{'repeatCell': {
'range': {
'sheetId': 0,
'startColumnIndex': 0,
'endColumnIndex': 6,
'startRowIndex': 0,
'endRowIndex': 1
},
'cell': {'userEnteredFormat': {'textFormat': {'bold': True}}},
'fields': 'userEnteredFormat.textFormat.bold',
}},
</pre>
Before we move on, let's talk about some shortcuts we can make. The ID of the first Sheet created for you is 0. If that's the Sheet you're using, then you can omit passing the Sheet ID. Similarly, the starting row and column indexes default to 0, so you can leave those out too if those are the values to be used. Finally, while an ending column index of 6 works, it won't if more columns are added later. It's best if you just omit the ending index altogether, meaning you want that entire row formatted. All this means that the only thing in the range you need is the ending row index. Instead of the above, your request can be shortened to:<br />
<pre>{'repeatCell': {
'range': {'endRowIndex': 1},
'cell': {'userEnteredFormat': {'textFormat': {'bold': True}}},
'fields': 'userEnteredFormat.textFormat.bold',
}},
</pre>
<br />
<h3>
Range quiz</h3>
Okay, now that you know about ranges, take this quiz: assumming the Sheet ID is 0, what are the starting and ending column and row indexes for the four cells highlighted in blue in this Sheet?<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhL7KL_bMzAUnAxlWYEz25khwNX66qLQWg7H7eE_XvWT7uU2t8Lbu65e2VsqpY1kMNMZg3aJyZL1X6tiBCg6atUSV98DolormtxoX6sFpx9Q9z7z10kMVyvFsRAbmfEIeH9HzJ9Xr4jLQc/s1600/lp021-range-sample.png" imageanchor="1"><img border="0" height="144" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhL7KL_bMzAUnAxlWYEz25khwNX66qLQWg7H7eE_XvWT7uU2t8Lbu65e2VsqpY1kMNMZg3aJyZL1X6tiBCg6atUSV98DolormtxoX6sFpx9Q9z7z10kMVyvFsRAbmfEIeH9HzJ9Xr4jLQc/s640/lp021-range-sample.png" width="640" /></a><br />
<br />
If you went with starting and ending column indexes of 3 and 5 and row indexes of 2 and 4, then you're right on the money and ready for more!
<br />
<br />
<h3>
Currency formatting</h3>
Currency formatting is similar to text formatting, only with numbers, meaning that instead of <code>userEnteredFormat.textFormat</code>, you'd be setting a cell's <code>userEnteredFormat.numberFormat</code> attribute. The command is also <code>repeatCell</code>. Clearly the starting and ending column indexes should be 4 and 5 with a starting row index of 1. But just like the cell bolding we did above, there's no need to restrict ourselves to just the 5 rows of data as more may be coming. Yes, it's best to leave off the ending row index so that the rest of the column is formatted. The only thing you need to learn is how to format cells using US currency, but that's pretty easy to do after a quick look at <a href="https://developers.google.com/sheets/guides/formats#number_format_patterns">the docs on formatting numbers</a>:<br />
<pre>{'repeatCell': {
'range': {
'startRowIndex': 1,
'startColumnIndex': 4,
'endColumnIndex': 5,
},
'cell': {
'userEnteredFormat': {
'numberFormat': {
'type': 'CURRENCY',
'pattern': '"$"#,##0.00',
},
},
},
'fields': 'userEnteredFormat.numberFormat',
}}
</pre>
<h3>
More on <code>fields</code></h3>
One caveat to our sample app here is that all of the <code>fields</code> mask only have a single value, the one we want to change, but that's not always the case. There may be situations where you want to effect a variety of changes to cells. To see more examples of <code>fields</code>, check out <a href="http://developers.google.com/sheets/samples/formatting">this page in the docs featuring more formatting examples</a>. To learn more about how masking works, check out <a href="https://developers.google.com/sheets/guides/batchupdate#field_masks">this page</a> and <a href="https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#fieldmask">this one too</a>.<br />
<br />
<h3>
Cell validation</h3>
The final formatting request implements cell validation on column F as well as restricting their possible values. The command used here is <code>setDataValidation</code>. The range is similar to that of currency formatting, only for column F, meaning a starting row index of 1, and starting and ending column indexes of 5 and 6, respectively. The rule implements the restriction. Similar to other spreadsheet software, you can restrict cell values in any number of ways, as outlined by <a href="https://developers.google.com/sheets/reference/rest/v4/spreadsheets#conditiontype">the ConditionType documentation page</a>. Ours is to allow for one of three possible values, so the ConditionType is <code>ONE_OF_LIST</code>.<br />
<br />
When you restrict cell values, you can choose to allow but flag it (weak enforcement) or disallow any value outside of what you specify (strict enforcement). If you wish to employ strict enforcement, you need to pass in a strict attribute with a <code>True</code> value. The default is weak enforcement, or <code>False</code>. In either case, users entering invalid values will get a default warning message that the input is not allowed. If you prefer a custom message over the default option, you can pass that to the API as the <code>inputMessage</code> attribute. I prefer the system default and elect not to use it here. Here are the 4 combinations of what shows up when you use or don't use <code>inputMessage</code> with strict and weak enforcement:<br />
<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjFE7BOtut0OnbIP2XdCtN_BpajjnGMfV6eH_L6BP8sq-wm8xMKDYbetThSRIwYTOkg8weWcyVevg6ZZAH2IdKHnSwx-xU5ebYqHPkx-fz23IEY3EsTZoP6jbwosgeY_R7WcG6ZDfd8aYQ/s1600/lp021-allow-default.png" imageanchor="1"><img border="0" height="140" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjFE7BOtut0OnbIP2XdCtN_BpajjnGMfV6eH_L6BP8sq-wm8xMKDYbetThSRIwYTOkg8weWcyVevg6ZZAH2IdKHnSwx-xU5ebYqHPkx-fz23IEY3EsTZoP6jbwosgeY_R7WcG6ZDfd8aYQ/s400/lp021-allow-default.png" width="400" /></a><br />
<b><i>No <code>inputMessage</code> (default) + weak enforcement</i></b><br />
<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEibNeR9SdzwBvE9-LjSmdAE-sDsqP45S3GDVbi8UazMD4RpJhWnFF5sXkyGjOk7J920atEKSMQFkzbB9p8S2SMtPkzNI3EyjQoIAsnFQVEHLhv8-6CdpgIXhsEspbjEE-P3_M20nKT9M90/s1600/lp021-allow-mymsg.png" imageanchor="1"><img border="0" height="125" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEibNeR9SdzwBvE9-LjSmdAE-sDsqP45S3GDVbi8UazMD4RpJhWnFF5sXkyGjOk7J920atEKSMQFkzbB9p8S2SMtPkzNI3EyjQoIAsnFQVEHLhv8-6CdpgIXhsEspbjEE-P3_M20nKT9M90/s400/lp021-allow-mymsg.png" width="400" /></a><br />
<b><i>With <code>inputMessage</code> (my custom msg) + weak enforcement</i></b><br />
<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgfY3NDx9b8-HsNnZtGn5lJz8QRUBll2M6nl9OB749XmPyXFImhvk9oQ887_4FcEM_tv1dbmTKZe3K0JhNoRKSXhznTXnt_P08HRiyxyFfNq-4I_MzoTqtkfXS-tQWZ82nvC4duOf8A6Gk/s1600/lp021-strict-default.png" imageanchor="1"><img border="0" height="140" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgfY3NDx9b8-HsNnZtGn5lJz8QRUBll2M6nl9OB749XmPyXFImhvk9oQ887_4FcEM_tv1dbmTKZe3K0JhNoRKSXhznTXnt_P08HRiyxyFfNq-4I_MzoTqtkfXS-tQWZ82nvC4duOf8A6Gk/s400/lp021-strict-default.png" width="400" /></a><br />
<b><i>No <code>inputMessage</code> (default) + strict enforcement</i></b><br />
<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj06W8uwhBHIwAqydaZn-xf_ahpH0Tky8JkeqS7pXeSVcn85RAR4GIJQXvtUy51qaWLzDftzjycBZ73HZxTwk6GO61gTmF-4tbsOPGzTYQDLIJeAZhyROw7w8CTAu91mNJOro-a_B9h7ik/s1600/lp021-strict-mymsg.png" imageanchor="1"><img border="0" height="125" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj06W8uwhBHIwAqydaZn-xf_ahpH0Tky8JkeqS7pXeSVcn85RAR4GIJQXvtUy51qaWLzDftzjycBZ73HZxTwk6GO61gTmF-4tbsOPGzTYQDLIJeAZhyROw7w8CTAu91mNJOro-a_B9h7ik/s400/lp021-strict-mymsg.png" width="400" /></a><br />
<b><i>With <code>inputMessage</code> (my custom msg) + weak enforcement</i></b><br />
<br />
The last attribute you can send is <code>showCustomUi</code>. If the <code>showCustomUi</code> flag is set to <code>True</code>, the Sheets user interface will display a small pulldown menu listing the values accepted by the cell. It's a pretty poor user experience <i>without</i> it (because users won’t know what the available choices are), so I recommend you always use it too. With that, this request looks like this:<br />
<pre>{'setDataValidation': {
'range': {
'startRowIndex': 1,
'startColumnIndex': 5,
'endColumnIndex': 6,
},
'rule': {
'condition': {
'type': 'ONE_OF_LIST',
'values': [
{'userEnteredValue': 'PENDING'},
{'userEnteredValue': 'SHIPPED'},
{'userEnteredValue': 'DELIVERED'},
]
},
#'inputMessage': 'Select PENDING, SHIPPED, or DELIVERED',
#'strict': True,
'showCustomUi': True,
},
}}
</pre>
Since we're not modifying cell attributes, but instead focusing on validation, you'll notice there's no <code>fields</code> mask in these types of requests.<br />
<br />
<h3>
Running our script</h3>
Believe it or not, that's the bulk of this application. With the <code>reqs</code> list of these four requests, the last line of code calls the Sheets API exactly like the pseudocode above. Now you can simply run it:
<br />
<pre>$ python sheets_cell_format.py # or python3
$
</pre>
There's no output from this script, so you should only expect that your Sheet will be formatted once it has completed. If you bring up the Sheet in the user interface, you should see the changes happening in near real-time:<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhescTRNPMyPLxvAc5BQwREq043n8R544gTFtQSm7bn-zgjuv1cto9JgR280CcBQIDx6uaMGiNGSozcIJY8RlmX9RKf7YYW_p7d6exIRY4_VitBbEgbSNRaxsDvazCcmLoqXA6mQa_PDUE/s1600/sheets_cell_format.gif" imageanchor="1"><img border="0" height="264" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhescTRNPMyPLxvAc5BQwREq043n8R544gTFtQSm7bn-zgjuv1cto9JgR280CcBQIDx6uaMGiNGSozcIJY8RlmX9RKf7YYW_p7d6exIRY4_VitBbEgbSNRaxsDvazCcmLoqXA6mQa_PDUE/s640/sheets_cell_format.gif" width="640" /></a><br />
<br />
<h2>
Conclusion</h2>
Below is the entire script for your convenience which runs on both Python 2 <b>and</b> Python 3 (unmodified!):<br />
<pre><span style="font-size: small;"><b>from</b> apiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
SCOPES = 'https://www.googleapis.com/auth/spreadsheets'
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> creds.invalid:
flow = client.flow_from_clientsecrets('client_id.json', SCOPES)
creds = tools.run_flow(flow, store)
SHEETS = discovery.build('sheets', 'v4', http=creds.authorize(Http()))
SHEET_ID = ... # add your Sheet ID here
reqs = {'requests': [
# frozen row 1
{'updateSheetProperties': {
'properties': {'gridProperties': {'frozenRowCount': 1}},
'fields': 'gridProperties.frozenRowCount',
}},
# embolden row 1
{'repeatCell': {
'range': {'endRowIndex': 1},
'cell': {'userEnteredFormat': {'textFormat': {'bold': True}}},
'fields': 'userEnteredFormat.textFormat.bold',
}},
# currency format for column E (E2:E7)
{'repeatCell': {
'range': {
'startRowIndex': 1,
'endRowIndex': 6,
'startColumnIndex': 4,
'endColumnIndex': 5,
},
'cell': {
'userEnteredFormat': {
'numberFormat': {
'type': 'CURRENCY',
'pattern': '"$"#,##0.00',
},
},
},
'fields': 'userEnteredFormat.numberFormat',
}},
# validation for column F (F2:F7)
{'setDataValidation': {
'range': {
'startRowIndex': 1,
'endRowIndex': 6,
'startColumnIndex': 5,
'endColumnIndex': 6,
},
'rule': {
'condition': {
'type': 'ONE_OF_LIST',
'values': [
{'userEnteredValue': 'PENDING'},
{'userEnteredValue': 'SHIPPED'},
{'userEnteredValue': 'DELIVERED'},
]
},
#'inputMessage': 'Select PENDING, SHIPPED, or DELIVERED',
#'strict': True,
'showCustomUi': True,
},
}},
]}
res = SHEETS.spreadsheets().batchUpdate(
spreadsheetId=SHEET_ID, body=reqs).execute()
</span></pre>
As with our other code samples, you can now customize for your own needs, for a mobile frontend, sysadmin script, or a server-side backend, perhaps accessing other Google APIs.<br />
<br />
<h3>
Code challenge</h3>
Once you fully grasp this sample and are ready for a challenge: Use the API to create a column "G" with a "Total Cost" header in cell G1, set cell G2 with the formula to calculate the cost based on toys ordered & cost in columns D & E then and create an <code>autoFill</code> request to replicate that formula down column G. When you're done, the right-hand side of your Sheet now looks like this:
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjlh09QxuL_Q5HaJ_iLYxSNZIjMImu4wXdcJoQlYj29tTdfj9tYIsrySLBEGl90Awb5oRct3jXRSksyx7rfrkEKfrB694MtjEB6pjAqtdxyu7H7s1EnrKkEz58x7lS3HamiMv9I5LPGjG4/s1600/lp021-challenge.png" imageanchor="1"><img border="0" height="264" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjlh09QxuL_Q5HaJ_iLYxSNZIjMImu4wXdcJoQlYj29tTdfj9tYIsrySLBEGl90Awb5oRct3jXRSksyx7rfrkEKfrB694MtjEB6pjAqtdxyu7H7s1EnrKkEz58x7lS3HamiMv9I5LPGjG4/s640/lp021-challenge.png" width="640" /></a><br />
<br />
Here are some steps you can take to achieve this improvement:<br />
<ol>
<li> Create column G with a "Total Cost" header in cell G1; make sure it's <b>bold</b> too (or do you have to?)</li>
<li> Set cell G2 with formula <code>=MULTIPLY(D2,E2)</code></li>
<li> Use <code>autoFill</code> command to copy formula from G2 down the entire column (HINT: you only need the <code>range</code> attribute)</li>
</ol>
You're now well under way to being able to writing useful applications with the Sheets API!wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com7tag:blogger.com,1999:blog-6940043312015460811.post-88739308168116296392016-07-11T11:01:00.000-07:002017-04-26T10:12:27.050-07:00Exporting a Google Sheet spreadsheet as CSV<h2>
Introduction</h2>
Today, we'll follow-up to my <a href="http://wescpy.blogspot.com/2016/06/using-new-google-sheets-api.html">earlier post on the Google Sheets API</a> and multiple posts (<a href="http://goo.gl/cdm3kZ">first</a>, <a href="http://goo.gl/A3kb6o">second</a>, <a href="http://goo.gl/nxuR9w">third</a>) on the Google Drive API by answering one common question: How do you download a Google Sheets spreadsheet as a CSV file? The "FAQ"ness of the question itself as well as various versions of Google APIs has led to many similar StackOverflow questions: <a href="http://stackoverflow.com/questions/3287651">one</a>, <a href="http://stackoverflow.com/questions/5683358">two</a>, <a href="http://stackoverflow.com/questions/11619805">three</a>, <a href="http://stackoverflow.com/questions/22298685">four</a>, <a href="http://stackoverflow.com/questions/35557603">five</a>, just to list a few. Let's answer this question definitively and walk through a Python code sample that does exactly that. The main assumption is that you have a Google Sheet file in your Google Drive named "inventory".<br />
<br />
<h2>
Choosing the <i>right</i> API</h2>
Upon first glance, developers may think the <a href="http://developers.google.com/sheets">Google Sheets API</a> is the one to use. Unfortunately that isn't the case. The Sheets API is the one to use for spreadsheet-oriented operations, such as inserting data, reading spreadsheet rows, managing individual tab/sheets within a spreadsheet, cell formatting, creating charts, adding pivot tables, etc., It isn't meant to perform file-based requests like exporting a Sheet in CSV (comma-separated values) format. For file-oriented operations with a Google Sheet, you would use the <a href="http://developers.google.com/drive">Google Drive API</a>.<br />
<br />
<h2>
Using the Google Drive API</h2>
As mentioned earlier, Google Drive features <a href="https://developers.google.com/drive/web/scopes" target="_blank">numerous API scopes of authorization</a>. As usual, we always recommend you use the most restrictive scope possible that allows your app to do its work. You'll request fewer permissions from your users (which makes them happier), and it also makes your app more secure, possibly preventing modifying, destroying, or corrupting data, or perhaps inadvertently going over quotas. Since we're only exporting a Google Sheets file from Google Drive, the only scope we need is:<br />
<ul>
<li><code>'https://www.googleapis.com/auth/drive.readonly'</code> — Read-only access to file content or metadata</li>
</ul>
The earlier post I wrote on the Google Drive API featured sample code that exported an uploaded Google Docs file as PDF and download <i>that</i> from Drive. This post will not only feature a change to exporting a Google Sheets file in CSV format, but also demonstrate one additional feature of the Drive API: querying<br />
<br />
Since we've fully <a href="http://wescpy.blogspot.com/2014/11/authorized-google-api-access-from-python.html">covered</a> the authorization boilerplate fully in earlier posts and videos, we're going to skip that here and jump right to the action, creating of a service endpoint to Drive. The API name is (of course <code>'drive'</code>, and the current version of the API is 3, so use the string <code>'v3'</code> in this call to the <code>apiclient.discovey.build()</code> function:<br />
<br />
<code><span style="font-size: small;">DRIVE = discovery.build('drive', 'v3', http=creds.authorize(Http()))</span></code><br />
<br />
<h2>
Query and export files from Google Drive</h2>
While unnecessary, we'll create a few string constants representing the filename, source and destination file MIME types to make the code easier to understand:<br />
<pre>FILENAME = 'inventory'
SRC_MIMETYPE = 'application/vnd.google-apps.spreadsheet'
DST_MIMETYPE = 'text/csv'
</pre>
In this simple example, we're only going to export one Google Sheets file as CSV, arbitrarily choosing a file named, "inventory." So to perform the query, you need both the filename and its MIME type, "application/vnd.google-apps.spreadsheet". Query components are conjoined with the "and" keyword, so your query string will look like this: <code>q='name="%s" and mimeType="%s"' % (FILENAME, SRC_MIMETYPE).</code><br />
<br />
Since there may be more than one Google Sheets file named 'inventory". we opt for newest one and thus need to sort all matching files in descending order of last modification time then name if "mtime"s are identical via an "order by" clause: <code>orderBy='modifiedTime desc,name'</code>. Here is the complete call to <code>DRIVE.files().list()</code> to issue the query:<br />
<pre><span style="font-size: small;">files = DRIVE.files().list(
q='name="%s" and mimeType="%s"' % (FILENAME, SRC_MIMETYPE),
orderBy='modifiedTime desc,name').execute().get('files', [])
</span></pre>
If any files match, the payload will contain a 'files' key, else we default to an empty list and display to the user on the last line that no files were found. Otherwise, grab the first match, the most recently-modified 'inventory' file, create a suitable CSV filename from it, and change all spaces to underscores:<br />
<br />
<code><span style="font-size: small;">fn = '%s.csv' % os.path.splitext(files[0]['name'].replace(' ', '_'))[0]</span></code><br />
<br />
The final Drive API call requests an export of 'inventory' as a CSV file, and if successful, the downloaded data is written with the filename above. In either case, the user is notified of success or failure of the export:<br />
<pre><span style="font-size: small;">data = DRIVE.files().export(fileId=files[0]['id'], mimeType=DST_MIMETYPE).execute()
<b>if</b> data:
<b>with</b> open(fn, 'wb') <b>as</b> f:
f.write(data)
print('DONE')
<b>else</b>:
print('ERROR (could not download file)')
</span></pre>
Note that if downloading as CSV, the Drive API only exports of the first sheet in a Sheets file... you won't get any others. However, it does support 3 <a href="https://developers.google.com/drive/v3/web/manage-downloads">other download formats</a> that will get you all the sheets.<br />
<br />
If you create a Sheets file named 'inventory', run the script, grant the script access to your Google Drive (via the OAuth2 prompt that pops up in the browser), and then you should get output that looks like this:
<br />
<pre>$ python drive_sheets_csv_export.py # or python3
Exporting "inventory" as "inventory.csv"... DONE</pre>
<h2>
Conclusion</h2>
Below is the entire script for your convenience which runs on both Python 2 <b>and</b> Python 3 (unmodified!). If I were to divide the script into 4 major sections, they would be:<br />
<ul>
<li>Get creds & build Google Drive service endpoint</li>
<li>Source and destination file info</li>
<li>Query Google Drive for matching files</li>
<li>Export most recent matching Sheets file as CSV</li>
</ul>
<br />
Here's the code itself:<br />
<pre><span style="font-size: small;"><b>from</b> __future__ <b>import</b> print_function
<b>import</b> os
<b>from</b> apiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
SCOPES = 'https://www.googleapis.com/auth/drive.readonly'
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> 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()))
FILENAME = 'inventory'
SRC_MIMETYPE = 'application/vnd.google-apps.spreadsheet'
DST_MIMETYPE = 'text/csv'
files = DRIVE.files().list(
q='name="%s" and mimeType="%s"' % (FILENAME, SRC_MIMETYPE),
orderBy='modifiedTime desc,name').execute().get('files', [])
<b>if</b> files:
fn = '%s.csv' % os.path.splitext(files[0]['name'].replace(' ', '_'))[0]
print('Exporting "%s" as "%s"... ' % (files[0]['name'], fn), end='')
data = DRIVE.files().export(fileId=files[0]['id'], mimeType=DST_MIMETYPE).execute()
<b>if</b> data:
<b>with</b> open(fn, 'wb') <b>as</b> f:
f.write(data)
print('DONE')
<b>else</b>:
print('ERROR (could not download file)')
<b>else</b>:
print('!!! ERROR: File not found')
</span></pre>
As with our other code samples, you can now customize for your own needs, for a mobile frontend, sysadmin script, or a server-side backend, perhaps accessing other Google APIs. Hope this helps answer yet another frequently asked question!wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com3tag:blogger.com,1999:blog-6940043312015460811.post-69440825377206276922016-06-09T14:00:00.001-07:002016-10-10T23:08:37.713-07:00Migrating SQL data to Google Sheets using the new Google Sheets API<b>NOTE:</b> The code covered in this post are also available in a <a href="http://goo.gl/N1RPwC">video walkthrough</a>.<br />
<b>UPDATE (Sep 2016):</b> Removed use of <code>argparse</code> module & <code>flags</code> (effective as of Feb 2016).<br />
<br />
<h2>
Introduction</h2>
In this post, we're going to demonstrate how to use the latest generation <a href="http://developers.google.com/sheets">Google Sheets API</a>. <a href="http://goo.gl/ikttYs">Launched</a> at Google I/O 2016 (full talk <a href="http://youtu.be/Gk-xpjgUwx4">here</a>), the Sheets API v4 can do much more than previous versions, bringing it to near-parity with what you can do with the Google Sheets UI (user interface) on desktop and mobile. Below, I'll walk you through a Python script that reads the rows of a relational database representing customer orders for a toy company and pushes them into a Google Sheet. Other API calls we'll make: one to create new Google Sheets with and another that reads the rows from a Sheet. <!--We present two similar scripts, one written in v2 while the other is in v3 so developers can get an idea of the changes and improvements that v3 represents.--><br />
<br />
<a href="http://goo.gl/57Gufk">Earlier posts</a> demonstrated the structure and "how-to" use Google APIs in general, so more recent posts, including this one, focus on solutions and use of specific APIs. Once you review the earlier material, you're ready to start with authorization scopes then see how to use the API itself.<br />
<ul></ul>
<h2>
Google Sheets API authorization & scopes</h2>
Previous versions of the Google Sheets API (formerly called the <a href="http://developers.google.com/google-apps/spreadsheets">Google Spreadsheets API</a>), were part of a group of "<a href="http://developers.google.com/gdata/docs/directory">GData APIs</a>" that implemented the <a href="http://developers.google.com/gdata">Google Data (GData) protocol</a>, an older, less-secure, REST-inspired technology for reading, writing, and modifying information on the web. The new API version falls under the more modern set of <a href="http://developers.google.com/api-client-library/python/apis">Google APIs</a> requiring <a href="http://oauth.net/">OAuth2</a> authorization and whose use is made easier with the <a href="http://developers.google.com/discovery/libraries">Google APIs Client Libraries</a>.<br />
<br />
The current API version features <a href="https://developers.google.com/sheets/guides/authorizing#OAuth2Authorizing" target="_blank">a pair of authorization scopes</a>: read-only and read-write. As usual, we always recommend you use the most restrictive scope possible that allows your app to do its work. You'll request fewer permissions from your users (which makes them happier), and it also makes your app more secure, possibly preventing modifying, destroying, or corrupting data, or perhaps inadvertently going over quotas. Since we're creating a Google Sheet and writing data into it, we <i>must</i> use the read-write scope:<br />
<ul>
<li><code>'https://www.googleapis.com/auth/spreadsheets'</code> — Read/write access to Sheets and Sheet properties</li>
</ul>
<h2>
Using the Google Sheets API</h2>
Let's look at some code that reads rows from a SQLite database and creates a Google Sheet with that data. Since we covered the authorization boilerplate fully in earlier <a href="http://goo.gl/cdm3kZ">posts</a> and <a href="http://goo.gl/KMfbeK">videos</a>, we're going straight to creating a Sheets service endpoint. The API string to use is <code>'sheets'</code> and the version string to use is <code>'v4'</code> as we call the <code>apiclient.discovey.build()</code> function:<br />
<br />
<code><span style="font-size: small;">SHEETS = discovery.build('sheets', 'v4', http=creds.authorize(Http()))</span></code>
<br />
<br />
With the <span style="font-family: monospace;">SHEETS</span> service endpoint in hand, the first thing to do is to create a brand new Google Sheet. Before we use it, one thing to know about the Sheets API is that most calls require a JSON payload representing the data & operations you wish to perform, and you'll see this as you become more familiar with it. For creating new Sheets, it's pretty simple, you don't have to provide anything, in which case you'd pass in an empty (<code>dict</code> as the) <span style="font-family: monospace;">body</span>, but a better bare minimum would be a name for the Sheet, so that's what <code>data</code> is for:<br />
<br />
<code><span style="font-size: small;">data = {'properties': {'title': 'Toy orders [%s]' % time.ctime()}}</span></code>
<br />
<br />
Notice that a Sheet's "title" is part of its "properties," and we also happen to add the timestamp as part of its name. With the payload complete, we call the API with the command to create a new Sheet [<a href="http://developers.google.com/sheets/reference/rest/v4/spreadsheets/create"><code>spreadsheets().create()</code></a>], passing in <span style="font-family: monospace;">data</span> in the (eventual) request <span style="font-family: monospace;">body</span>:
<br />
<br />
<code>res = SHEETS.spreadsheets().create(body=data).execute()</code>
<br />
<br />
Alternatively, you can use the <a href="http://developers.google.com/drive">Google Drive API</a> (<a href="http://wescpy.blogspot.com/2015/12/google-drive-uploading-downloading.html">v2</a> or <a href="http://wescpy.blogspot.com/2015/12/migrating-to-new-google-drive-api-v3.html">v3</a>) to create a Sheet but would also need to pass in the Google Sheets (file) <a href="http://developers.google.com/drive/v3/web/mime-types">MIME type</a>:
<br />
<pre>data = {
'name': 'Toy orders [%s]' % time.ctime(),
'mimeType': 'application/vnd.google-apps.spreadsheet',
}
res = DRIVE.files().create(body=data).execute() # insert() for v2
</pre>
The general rule-of-thumb is that if you're only working with Sheets, you can do all the operations with <i>its</i> API, but if creating files other than Sheets or performing other Drive file or folder operations, you may want to stick with the Drive API. You can also use both or any other Google APIs for more complex applications. We'll stick with just the Sheets API for now. After creating the Sheet, grab and display some useful information to the user:
<br />
<pre>SHEET_ID = res['spreadsheetId']
print('Created "%s"' % res['properties']['title'])
</pre>
You may be wondering: Why do I need to create a Sheet and <i>then</i> make a separate API call to add data to it? Why can't I do this all when creating the Sheet? The answer (to this likely FAQ) is you <i>can</i>, but you would need to construct and pass in a JSON payload representing the entire Sheet—meaning all cells and their formatting—a much larger and more complex data structure than just an array of rows. (Don't believe me? Try it yourself!) This is why we have all of the <code><a href="http://developers.google.com/sheets/reference/rest/v4/spreadsheets.values">spreadsheets().values()</a></code> methods... to simplify uploading or downloading of only values to or from a Sheet.
<br />
<br />
Now let's turn our attention to the simple <a href="http://sqlite.org/">SQLite</a> database file (<code><a href="https://github.com/googlecodelabs/sheets-api/blob/master/start/db.sqlite">db.sqlite</a></code>) available from <a href="http://g.co/codelabs/sheets">the Google Sheets Node.js codelab</a>. The next block of code just connects to the database with the standard library <a href="http://docs.python.org/library/sqlite3" style="font-family: monospace;">sqlite3</a> package, grabs all the rows, adds a header row, and filters the last two (timestamp) columns:
<br />
<pre>FIELDS = ('ID', 'Customer Name', 'Product Code', 'Units Ordered',
'Unit Price', 'Status', 'Created at', 'Updated at')
cxn = sqlite3.connect('db.sqlite')
cur = cxn.cursor()
rows = cur.execute('SELECT * FROM orders').fetchall()
cxn.close()
rows.insert(0, FIELDS)
data = {'values': [row[:6] <b>for</b> row <b>in</b> rows]}
</pre>
When you have a payload (array of row data) you want to stick into a Sheet, you simply pass in those values to <code><a href="http://developers.google.com/sheets/reference/rest/v4/spreadsheets.values/update">spreadsheets().values().update()</a></code> like we do here:
<br />
<pre>SHEETS.spreadsheets().values().update(spreadsheetId=SHEET_ID,
range='A1', body=data, valueInputOption='RAW').execute()
</pre>
The call requires a Sheet's ID and command body as expected, but there are two other fields: the full (or, as in our case, the "upper left" corner of the) range of cells to write to (in <a href="https://developers.google.com/sheets/guides/concepts#a1_notation">A1 notation</a>), and <span style="font-family: monospace;"><a href="https://developers.google.com/sheets/reference/rest/v4/ValueInputOption">valueInputOption</a></span> indicates how the data should be interpreted, writing the raw values ("RAW") or interpreting them as if a user were entering them into the UI ("USER_ENTERED"), possibly converting strings & numbers based on the cell formatting.<br />
<br />
Reading rows out of a Sheet is even easier, the <a href="http://developers.google.com/sheets/reference/rest/v4/spreadsheets.values/get" style="font-family: monospace;">spreadsheets().values().get()</a> call needing only an ID and a range of cells to read:<br />
<pre>print('Wrote data to Sheet:')
rows = SHEETS.spreadsheets().values().get(spreadsheetId=SHEET_ID,
range='Sheet1').execute().get('values', [])
<b>for</b> row <b>in</b> rows:
print(row)
</pre>
The API call returns a <span style="font-family: monospace;">dict</span> which has a 'values' key if data is available, otherwise we default to an empty list so the <code><b>for</b></code> loop doesn't fail.
<br />
<br />
If you run the code (entire script below) and grant it permission to manage your Google Sheets (via the OAuth2 prompt that pops up in the browser), the output you get should look like this:
<br />
<pre>$ python3 sheets-toys.py # or python (2.x)
Created "Toy orders [Thu May 26 18:58:17 2016]" with this data:
['ID', 'Customer Name', 'Product Code', 'Units Ordered', 'Unit Price', 'Status']
['1', "Alice's Antiques", 'FOO-100', '25', '12.5', 'DELIVERED']
['2', "Bob's Brewery", 'FOO-200', '60', '18.75', 'SHIPPED']
['3', "Carol's Car Wash", 'FOO-100', '100', '9.25', 'SHIPPED']
['4', "David's Dog Grooming", 'FOO-250', '15', '29.95', 'PENDING']
['5', "Elizabeth's Eatery", 'FOO-100', '35', '10.95', 'PENDING']
</pre>
<h2>
Conclusion</h2>
Below is the entire script for your convenience which runs on both Python 2 <b>and</b> Python 3 (unmodified!):
<br />
<br />
<pre><span style="font-size: small;">'''sheets-toys.py -- Google Sheets API demo
created Jun 2016 by +Wesley Chun/@wescpy
'''
<b>from</b> __future__ <b>import</b> print_function
<b>import</b> sqlite3
<b>import</b> time
<b>from</b> apiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
SCOPES = 'https://www.googleapis.com/auth/spreadsheets'
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> creds.invalid:
flow = client.flow_from_clientsecrets('client_id.json', SCOPES)
creds = tools.run_flow(flow, store)
SHEETS = discovery.build('sheets', 'v4', http=creds.authorize(Http()))
data = {'properties': {'title': 'Toy orders [%s]' % time.ctime()}}
res = SHEETS.spreadsheets().create(body=data).execute()
SHEET_ID = res['spreadsheetId']
print('Created "%s"' % res['properties']['title'])
FIELDS = ('ID', 'Customer Name', 'Product Code', 'Units Ordered',
'Unit Price', 'Status', 'Created at', 'Updated at')
cxn = sqlite3.connect('db.sqlite')
cur = cxn.cursor()
rows = cur.execute('SELECT * FROM orders').fetchall()
cxn.close()
rows.insert(0, FIELDS)
data = {'values': [row[:6] <b>for</b> row <b>in</b> rows]}
SHEETS.spreadsheets().values().update(spreadsheetId=SHEET_ID,
range='A1', body=data, valueInputOption='RAW').execute()
print('Wrote data to Sheet:')
rows = SHEETS.spreadsheets().values().get(spreadsheetId=SHEET_ID,
range='Sheet1').execute().get('values', [])
<b>for</b> row <b>in</b> rows:
print(row)
</span></pre>
You can now customize this code for your own needs, for a mobile frontend, devops script, or a server-side backend, perhaps accessing other Google APIs. If this example is too complex, check the <a href="http://developers.google.com/sheets/quickstart/python">Python quickstart in the docs</a> that way simpler, only reading data out of an existing Sheet. If you know JavaScript and are ready for something more serious, try <a href="http://g.co/codelabs/sheets">the Node.js codelab</a> where we got the SQLite database from. That's it... hope you find these code samples useful in helping you get started with the latest Sheets API!<br />
<br />
<b>EXTRA CREDIT</b>: Feel free to experiment and try cell formatting or other API features. Challenge yourself as there's a lot more to Sheets than just reading and writing values! <!--[if gte mso 9]><xml>
<w:LatentStyles DefLockedState="false" LatentStyleCount="156">
</w:LatentStyles>
</xml><![endif]--><!--[if gte mso 10]>
<style>
/* Style Definitions */
table.MsoNormalTable
{mso-style-name:"Table Normal";
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-parent:"";
mso-padding-alt:0in 5.4pt 0in 5.4pt;
mso-para-margin:0in;
mso-para-margin-bottom:.0001pt;
mso-pagination:widow-orphan;
font-size:10.0pt;
font-family:"Times New Roman";
mso-ansi-language:#0400;
mso-fareast-language:#0400;
mso-bidi-language:#0400;}
</style>
<![endif]-->wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com2tag:blogger.com,1999:blog-6940043312015460811.post-59448745173415920132015-12-23T10:00:00.000-08:002018-03-11T21:48:58.219-07:00Migrating to Google Drive API v3<b>NOTE:</b> The code covered in this and the previous post are also available in a <a href="http://goo.gl/EySSQV">video walkthrough</a>. <b>Mar 2018 UPDATE:</b> Modernized
the code a bit, shortening it, and changed to R/W scope because drive.file doesn't work
if the file hasn't been created yet. The same fixes were made to the Drive API v2 sample
in the preceding blog post.
<br />
<br />
<h2>
Introduction</h2>
In a <a href="http://goo.gl/A3kb6o">blog post</a> last week, we introduced readers to performing uploads and downloads files to/from Google Drive from a simple Python command-line script. In <a href="http://goo.gl/cR151b">an official Google blog post</a> later that same day, the Google Drive API team announced a new version of the API. Great timing huh? Well, good thing I knew it was coming, so that I could prepare <i>this</i> post for you, which is a primer on how to migrate from the current version of the API (v2) to the new one (v3).<br />
<br />
As stated by the Drive team, v2 isn't being deprecated, and there are no new features in v3, thus migration isn't required. The new version is mainly for new apps/integrations as well as developers with v2 apps who wish to take advantage of the improvements. This post is intended for those in the latter group, covering porting existing apps to v3. Ready? Let's go straight to the action.<br />
<br />
<h2>
Migrating from Google Drive API v2 to v3</h2>
Most of this post will be just examining all the "diffs" between the v2 code sample from <a href="http://goo.gl/A3kb6o">the previous post</a> (renamed from <code>drive_updown.py</code> to <code>drive_updown2.py</code>) and its v3 equivalent (<code>drive_updown3.py</code>). We'll take things step-by-step to provide more details, but let's start with all the diffs first:<br />
<pre><span style="font-size: small;">--- drive_updown2.py 2018-03-11 21:42:33.000000000 -0700
+++ drive_updown3.py 2018-03-11 21:44:57.000000000 -0700
@@ -11,23 +11,24 @@
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', 'v2', http=creds.authorize(Http()))
+DRIVE = discovery.build('drive', 'v3', http=creds.authorize(Http()))
FILES = (
- ('hello.txt', False),
- ('hello.txt', True),
+ ('hello.txt', None),
+ ('hello.txt', 'application/vnd.google-apps.document'),
)
-for filename, convert in FILES:
- metadata = {'title': filename}
- res = DRIVE.files().insert(convert=convert, body=metadata,
- media_body=filename, fields='mimeType,exportLinks').execute()
+for filename, mimeType in FILES:
+ metadata = {'name': filename}
+ if mimeType:
+ metadata['mimeType'] = mimeType
+ res = DRIVE.files().create(body=metadata, media_body=filename).execute()
if res:
print('Uploaded "%s" (%s)' % (filename, res['mimeType']))
if res:
MIMETYPE = 'application/pdf'
- res, data = DRIVE._http.request(res['exportLinks'][MIMETYPE])
+ data = DRIVE.files().export(fileId=res['id'], mimeType=MIMETYPE).execute()
if data:
fn = '%s.pdf' % os.path.splitext(filename)[0]
with open(fn, 'wb') as fh:
</span></pre>
We'll start with the building of the service endpoint, with the trivial change of the API version string from <code>'v2'</code> to <code>'v3'</code>:<br />
<pre>-DRIVE = build('drive', 'v2', http=creds.authorize(Http()))
+DRIVE = build('drive', 'v3', http=creds.authorize(Http()))
</pre>
The next change is the deprecation of the conversion flag. The problem with a Boolean variable is that it limits the possible types of file formats supported. By changing it to a file mimeType instead, the horizons are broadened:<br />
<pre> FILES = (
- ('hello.txt', False),
- ('hello.txt', True),
+ ('hello.txt', None),
+ ('hello.txt', 'application/vnd.google-apps.document'),
)
</pre>
Your next question will be: "What are the mimeTypes for the supported Google Apps document formats?" The answers can be found at <a href="https://developers.google.com/drive/v3/web/mime-types">this page</a> in the official docs. This changes the datatype in our array of 2-tuples, so we need to change the loop variable to reflect this... we'll use the mimeType instead of a conversion flag:<br />
<pre>-for filename, convert in FILES:
+for filename, mimeType in FILES:
</pre>
Another change related to deprecating the <code>convert</code> flag is that the mimeType isn't a parameter to the API call. Instead, it's another piece of metadata, so we need to add <code>mimeType</code> to the metadata object.<br />
<br />
Related to this is a name change: since a file's name is its <b>name</b> and not its <b>title</b>, it makes more sense to use "<code>name</code>" as the metadata value:<br />
<pre>- metadata = {'title': filename}
+ metadata = {'name': filename}
+ if mimeType:
+ metadata['mimeType'] = mimeType
</pre>
Why the <code><b>if</b></code> statement? Not only did v3 see a change to using mimeTypes, but rather than being a parameter like the conversion flag in v2, the mimeType has been moved into the file's metadata, so if we're doing any conversion, we need to add it to our <code>metadata</code> field (then remove the <code>convert</code> parameter down below).<br />
<br />
Next is yet another name change: when creating files on Google Drive, "<code>create()</code>" makes more sense as a method name than "<code>insert()</code>". Reducing the size of payload is another key ingredient of v3. We mentioned in <a href="http://goo.gl/A3kb6o">the previous post</a> that <code>insert()</code> returns more than 30 fields in the response payload unless you use the <code>fields</code> parameter to specify exactly which you wish returned. In v3, the default response payload only returns four fields, including all the ones we need in this script, so use of the <code>fields</code> parameter isn't required any more:<br />
<pre><span style="font-size: small;">- res = DRIVE.files().insert(convert=convert, body=metadata,
- media_body=filename, fields='mimeType,exportLinks').execute()
+ res = DRIVE.files().create(body=metadata, media_body=filename).execute()
</span></pre>
The final improvement we can demonstrate: users no longer have to make an authorized HTTP GET request with a link to export and download a file in an alternate format like PDF®. Instead, it's now a "normal" API call (to the new "<code>export()</code>" method) with the mimeType as a parameter. The only other parameter you need is the file ID, which comes back as part of the (default) response payload when the <code>create()</code> call was made:<br />
<pre><span style="font-size: small;">- res, data = DRIVE._http.request(res['exportLinks'][MIMETYPE])
+ data = DRIVE.files().export(fileId=res['id'], mimeType=MIMETYPE).execute()
</span></pre>
That's it! If you run the script, grant the script access to your Google Drive (via the OAuth2 prompt that pops up in the browser), and then you should get output that looks like this:
<br />
<pre>$ python drive_updown3.py # or python3
Uploaded "hello.txt" (text/plain)
Uploaded "hello.txt" (application/vnd.google-apps.document)
Downloaded "hello.pdf" (application/pdf)
</pre>
<br />
<h2>
Conclusion</h2>
The entire v2 script (<span style="font-family: monospace;">drive_updown2.py</span>) was spelled out in full in <a href="http://goo.gl/A3kb6o">the previous post</a>, and it hasn't changed since then. Below is the v3 script (<span style="font-family: monospace;">drive_updown3.py</span>) for your convenience which runs on both Python 2 <b>and</b> Python 3 (unmodified!):<br />
<pre><span style="font-size: small;">#!/usr/bin/env python
<b>from</b> __future__ <b>import</b> print_function
<b>import</b> os
<b>from</b> apiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
SCOPES = 'https://www.googleapis.com/auth/drive'
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> 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 = (
('hello.txt', None),
('hello.txt', 'application/vnd.google-apps.document'),
)
<b>for</b> filename, mimeType <b>in</b> FILES:
metadata = {'name': filename}
<b>if</b> mimeType:
metadata['mimeType'] = mimeType
res = DRIVE.files().create(body=metadata, media_body=filename).execute()
<b>if</b> res:
print('Uploaded "%s" (%s)' % (filename, res['mimeType']))
<b>if</b> res:
MIMETYPE = 'application/pdf'
data = DRIVE.files().export(fileId=res['id'], mimeType=MIMETYPE).execute()
<b>if</b> data:
fn = '%s.pdf' % os.path.splitext(filename)[0]
<b>with</b> open(fn, 'wb') <b>as</b> fh:
fh.write(data)
print('Downloaded "%s" (%s)' % (fn, MIMETYPE))</span></pre>
<pre><span style="font-size: small;">)</span>
</pre>
Just as in <a href="http://goo.gl/A3kb6o">the previous post</a>(s), you can now customize this code for your own needs, for a mobile frontend, sysadmin script, or a server-side backend, perhaps accessing other Google APIs. Hope we accomplished our goal by pointing out some of the shortcomings that are in v2 and how they were improved in v3! All of the content in this and the previous post are spelled out visually in <a href="http://goo.gl/EySSQV">this video</a> that I created for you.wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com5tag:blogger.com,1999:blog-6940043312015460811.post-46350101876050771742015-12-14T11:30:00.000-08:002018-03-11T21:38:38.478-07:00Google Drive: Uploading & Downloading files with Python<b>UPDATE</b>: Since this post was published, the Google Drive team <a href="http://goo.gl/cR151b">released a newer version</a> of their API. After reading this one, go to <a href="http://goo.gl/nxuR9w">the next post</a> to learn about migrating your app from v2 to v3 as well as link to <a href="http://goo.gl/EySSQV">my video</a> which walks through the code samples in both posts.<br />
<br />
<h2>
Introduction</h2>
So far in this series of blogposts covering authorized Google APIs, we've used Python to access Google <a href="http://goo.gl/cdm3kZ">Drive</a>, <a href="http://goo.gl/OfCbOz">Gmail</a>, and Google <a href="http://wescpy.blogspot.com/2015/09/creating-events-in-google-calendar.html">Calendar</a>. Today, we're revisiting Google Drive<!-- and introducing their new API. While v2 is technically still around, v3 removes unnecessary duplicity & complexity, uses better & more logical naming, and can allow your app to perform better (in both time and space).--> with a small snippet that uploads plain text files to Drive, with & without conversion to a Google Apps format (Google Docs), then exports & downloads the converted one as PDF®. <!--We present two similar scripts, one written in v2 while the other is in v3 so developers can get an idea of the changes and improvements that v3 represents.--><br />
<br />
<a href="http://goo.gl/57Gufk">Earlier posts</a> demonstrated the structure and "how-to" use Google APIs in general, so more recent posts, including this one, focus on solutions and apps, and use of specific APIs. Once you review the earlier material, you're ready to start with authorization scopes then see how to use the API itself.<br />
<ul></ul>
<h2>
Google Drive API Scopes</h2>
Google Drive features <a href="https://developers.google.com/drive/web/scopes" target="_blank">numerous API scopes of authorization</a>. As usual, we always recommend you use the most restrictive scope possible that allows your app to do its work. You'll request fewer permissions from your users (which makes them happier), and it also makes your app more secure, possibly preventing modifying, destroying, or corrupting data, or perhaps inadvertently going over quotas. Since we need to upload/create files in Google Drive, the minimum scope we need is:<br />
<ul>
<li><code>'https://www.googleapis.com/auth/drive'</code> — Read/write access to Drive</li>
</ul>
<h2>
Using the Google Drive API</h2>
Let's get going with our example today that uploads and downloads a simple plain text file to Drive. The file will be uploaded twice, once as-is, and the second time, converted to a Google Docs document. The last part of the script will request an export of the (uploaded) Google Doc as PDF and download <i>that</i> from Drive.<br />
<br />
Since we've fully covered the authorization boilerplate fully in earlier posts and videos, we're going to skip that here and jump right to the action, creating of a service endpoint to Drive. The API name is (of course) <code>'drive'</code>, and the current version of the API is 2, so use the string <code>'v2'</code> in this call to the <code>apiclient.discovey.build()</code> function:<!--, as mentioned above, we're going to demo both API versions 2 & 3. Version 3 does not deprecate v2, so existing v2 users don't have to port unless they want to take advantage of new v3 features. so here's the first call to <code>apiclient.discovery.build()</code> you'll use:--><br />
<br />
<code>DRIVE = build('drive', 'v2', http=creds.authorize(Http()))</code>
<br />
<br />
Let's also create a <code>FILES</code> array object (tuple, list, etc.) which holds 2-tuples of the files to upload. These pairs are made up of a filename and a flag indicating whether or not you wish the file to be converted to a Google Apps format:<br />
<pre>FILES = (
('hello.txt', False),
('hello.txt', True),
)</pre>
Since we're uploading a plain text file, a conversion to Apps format means Google Docs. (You can imagine that if it was a CSV file, the target format would be Google Sheets instead.) With the setup complete, let's move on to the code that performs the file uploads.<br />
<br />
We'll loop through <code>FILES</code>, cycling through each file-convert flag pair and call the <code>files.insert()</code> method to perform the upload. The four parameters needed are: 1) the conversion flag, 2) the file <code>metadata</code>, which is only the filename (see below), 3) the <code>media_body</code>, which is also the filename but has a different purpose — it specifies where the file content will come from, meaning the file will be opened and its data transferred to the API, and 4), a set of <code>fields</code> you want returned.<br />
<pre><span style="font-size: small;"><b>for</b> filename, convert <b>in</b> FILES:
metadata = {'title': filename}
res = DRIVE.files().insert(convert=convert, body=metadata,
media_body=filename, fields='mimeType,exportLinks').execute()
<b>if</b> res:
print('Uploaded "%s" (%s)' % (filename, res['mimeType']))
</span></pre>
It's important to give the <code>fields()</code> parameter because if you don't, more than 30(!) are returned by default from the API. There's no need to waste all that network traffic if all you need are just a couple. In our case, we only want the <code>mimeType</code>, to confirm what the file was saved as, and <code>exportLinks</code>, which we'll explore in a moment. If files are uploaded successfully, the <code>print()</code> lets the user know, and then we move on to the final section of the script.<br />
<div>
<br /></div>
Before we dig into the last bit of code, it's important to realize that the <code>res</code> variable still contains the result from the second upload, the one where the file is converted to Google Docs. This is important because this is where we need to extract the download link for the format you want (<code>res['exportLinks'][MIMETYPE]</code>). The way to download the file is to make an authorized HTTP GET call, passing in that link. In our case, it's the PDF version. If the download is successful, the <code>data</code> variable will have the payload to write to disk. If all's good, let the user know:<br />
<pre><span style="font-size: small;"><b>if</b> res:
MIMETYPE = 'application/pdf'
res, data = DRIVE._http.request(res['exportLinks'][MIMETYPE])
<b>if </b>data:
fn = '%s.pdf' % os.path.splitext(filename)[0]
<b>with</b> open(fn, 'wb') <b>as</b> fh:
fh.write(data)
print('Downloaded "%s" (%s)' % (fn, MIMETYPE))
</span></pre>
Final note: this code sample is slightly different from previous posts in two big ways: 1) now that the Google APIs Client Library runs on Python 3, I'll try to produce only code samples for this blog that run unmodified under both 2.x and 3.x interpreters — the primary one-line difference being the import of the <code>print()</code> function, and 2) we're going to incorporate the use of the <code>run_flow()</code> function from <code>oauth2client.tools</code> and only fallback to the deprecated <code>run()</code> function if necessary — more info on this change available in <a href="http://goo.gl/ughnW7">this earlier post</a>.<br />
<br />
If you run the script, grant the script access to your Google Drive (via the OAuth2 prompt that pops up in the browser), and then you should get output that looks like this:
<br />
<pre>$ python drive_updown3.py # or python3
Uploaded "hello.txt" (text/plain)
Uploaded "hello.txt" (application/vnd.google-apps.document)
Downloaded "hello.pdf" (application/pdf)
</pre>
<br />
<h2>
Conclusion</h2>
Below is the entire script for your convenience which runs on both Python 2 <b>and</b> Python 3 (unmodified!):<br />
<pre><span style="font-size: small;">#!/usr/bin/env python
<b>from</b> __future__ <b>import</b> print_function
<b>import</b> os
<b>from</b> apiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
SCOPES = 'https://www.googleapis.com/auth/drive'
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> creds.invalid:
flow = client.flow_from_clientsecrets('client_secret.json', SCOPES)
creds = tools.run_flow(flow, store)
DRIVE = discovery.build('drive', 'v2', http=creds.authorize(Http()))
FILES = (
('hello.txt', False),
('hello.txt', True),
)
<b>for</b> filename, convert <b>in</b> FILES:
metadata = {'title': filename}
res = DRIVE.files().insert(convert=convert, body=metadata,
media_body=filename, fields='mimeType,exportLinks').execute()
<b>if</b> res:
print('Uploaded "%s" (%s)' % (filename, res['mimeType']))
<b>if</b> res:
MIMETYPE = 'application/pdf'
res, data = DRIVE._http.request(res['exportLinks'][MIMETYPE])
<b>if</b> data:
fn = '%s.pdf' % os.path.splitext(filename)[0]
<b>with</b> open(fn, 'wb') <b>as</b> fh:
fh.write(data)
print('Downloaded "%s" (%s)' % (fn, MIMETYPE))</span>
</pre>
You can now customize this code for your own needs, for a mobile frontend, sysadmin script, or a server-side backend, perhaps accessing other Google APIs. If you want to see<i> another</i> example of using the Drive API, check out <a href="http://goo.gl/cdm3kZ">this earlier post</a> listing the files in Google Drive and its accompanying video as well as <a href="https://developers.google.com/drive/web/quickstart/python">a similar example </a>in the official docs or its equivalent in Java (server-side, Android), iOS (Objective-C, Swift), C#/.NET, PHP, Ruby, JavaScript (client-side, Node.js, Google Apps Script), or Go. That's it... hope you find these code samples useful in helping you get started with the Drive API!<br />
<br />
<b>UPDATE</b>: Since this post was published, the Google Drive team <a href="http://goo.gl/cR151b">released a newer version</a> of their API. Go to <a href="http://goo.gl/nxuR9w">the next post</a> to learn about migrating your app from v2 to v3 as well as link to <a href="http://goo.gl/EySSQV">my video</a> which walks through the code samples in both posts.<br />
<br />
<b>EXTRA CREDIT</b>: Feel free to experiment and try something else to test your skills and challenge yourself as there's a lot more to Drive than just uploading and downloading files. Experiment with creating <a href="https://developers.google.com/drive/web/folder">folders</a> and manipulate files there, work with a folder of photos and organize them using the <a href="http://googleappsdeveloper.blogspot.com/2013/03/even-more-image-metadata-for-google.html">image metadata</a> available to you, implement a search engine for your Drive files, etc. There are so many things you can do! <!--[if gte mso 9]><xml>
<w:LatentStyles DefLockedState="false" LatentStyleCount="156">
</w:LatentStyles>
</xml><![endif]--><!--[if gte mso 10]>
<style>
/* Style Definitions */
table.MsoNormalTable
{mso-style-name:"Table Normal";
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-parent:"";
mso-padding-alt:0in 5.4pt 0in 5.4pt;
mso-para-margin:0in;
mso-para-margin-bottom:.0001pt;
mso-pagination:widow-orphan;
font-size:10.0pt;
font-family:"Times New Roman";
mso-ansi-language:#0400;
mso-fareast-language:#0400;
mso-bidi-language:#0400;}
</style>
<![endif]-->wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com15tag:blogger.com,1999:blog-6940043312015460811.post-23780539709086701422015-09-09T10:00:00.000-07:002017-07-08T02:18:10.898-07:00Creating events in Google Calendar from Python<b>NOTE:</b> The code covered in this blogpost is also available in a video walkthrough <a href="http://goo.gl/9qKHVA" target="_blank">here</a>.
<br />
<b><br /></b>
<b>UPDATE (Jan 2016):</b> Tweaked the code to support <code>oauth2client.tools.run_flow()</code> which deprecates <code>oauth2client.tools.run()</code>. You can read more about that change and migration steps <a href="http://goo.gl/ughnW7" target="_blank">here</a>.<br />
<div>
<br /></div>
<h2>
Introduction</h2>
So far in this series of blogposts covering authorized Google APIs, we've used Python code to access Google Drive and Gmail. Today, we're going to demonstrate the Google Calendar API. While Google Calendar, and calendaring in general, have been around for a long time and are fairly stable, it's somewhat of a mystery as to why so few developers create good calendar integrations, whether it be with Google Calendar, or other systems. We'll try to show it isn't necessarily difficult and hopefully motivate some of you out there to add a calendaring feature in your next mobile or web app.<br />
<br />
Earlier posts (<a href="http://goo.gl/57Gufk">link 1</a>, <a href="http://goo.gl/cdm3kZ">link 2</a>) demonstrated the structure and "how-to" use Google APIs in general, so more recent posts, including this one, focus on solutions and apps, and use of specific APIs. Once you review the earlier material, you're ready to start with authorization scopes then see how to use the API itself.<br />
<ul></ul>
<h2>
Google Calendar API Scopes</h2>
Below are the <a href="http://developers.google.com/google-apps/calendar/auth" target="_blank">Google Calendar API scopes of authorization</a>. There are only a pair (at the time of this writing): <b>read-only</b> and <b>read/write</b>. As usual, use the most restrictive scope you possibly can yet still allowing your app to do its work. This makes your app more secure and may prevent inadvertently going over any quotas, or accessing, destroying, or corrupting data. Also, users are less hesitant to install your app if it asks only for more restricted access to their calendars. However, it's likely that in order to really use the API to its fullest, you will probably have to ask for read-write so that you can add, update, or delete events in their calendars.<br />
<ul>
<li><code>'https://www.googleapis.com/auth/calendar.readonly'</code> — Read-only access to calendar</li>
<li><code>'https://www.googleapis.com/auth/calendar'</code> — Read/write access to calendar</li>
</ul>
<h2>
Using the Google Calendar API</h2>
We're going to create a sample Python script that inserts a new event into your Google Calendar. Since this requires modifying your calendar, you need the read/write scope above. The API name is <code>'calendar'</code> which is currently on version 3, so here's the call to <code>apiclient.discovery.build()</code> you'll use:
<br />
<pre>GCAL = discovery.build('calendar', 'v3',
http=creds.authorize(Http()))</pre>
Note that all lines of code above that is predominantly boilerplate (that was explained in earlier posts and videos). Anyway, we've got an established service endpoint with <code>build()</code>, we need to come up with the data to create a calendar event with, at the very least, an event name plus start and end times.<br />
<br />
<table border="4"><tbody>
<tr><td><b>Timezone or offset required</b><br />
<br />
The API requires either a timezone or a <a href="https://en.wikipedia.org/wiki/List_of_UTC_time_offsets" target="_blank">GMT offset</a>, the number of hours your timezone is away from <a href="https://en.wikipedia.org/wiki/Coordinated_Universal_Time" target="_blank">Coordinated Universal Time</a> (UTC, more commonly known as GMT). The format is +/-HH:MM away from UTC. For example, <a href="http://www.timeanddate.com/time/zones/pdt" target="_blank">Pacific Daylight Time</a> (PDT, also known as Mountain Standard Time, or MST), is "-07:00," or seven hours behind UTC while Nepal Standard Time (NST [or NPT to avoid confusion with Newfoundland Standard Time]), is "+05:45," or five hours and forty-five minutes ahead of UTC. Also, the offset must be in <a href="https://tools.ietf.org/html/rfc3339">RFC 3339</a> format, which implements the specifications of <a href="http://www.iso.org/iso/home/standards/iso8601.htm">ISO 8601</a> for the Internet. Timestamps look like the following in the required format: "YYYY-MM-DDTHH:MM:SS±HH:MM". For example, September 15, 2015 at 7 PM PDT is represented by this string: "2015-09-15T19:00:00-07:00".<br/><br/>If you wish to avoid offsets and would rather use timezone names instead, see the next post in this series (link at bottom).</td></tr>
</tbody></table>
<br />
The script in this post uses the PDT timezone, so we set the <code>GMT_OFF</code> variable to "-07:00". The <code>EVENT</code> body will hold the event name, and start and end times suffixed with the GMT offset:<br />
<pre><span style="font-size: small;">GMT_OFF = '-07:00' # PDT/MST/GMT-7
EVENT = {
'summary': 'Dinner with friends',
'start': {'dateTime': '2015-09-15T19:00:00%s' % GMT_OFF},
'end': {'dateTime': '2015-09-15T22:00:00%s' % GMT_OFF},
}</span></pre>
Use the <code>insert()</code> method of the <code>events()</code> service to add the event. As expected, one required parameter is the ID of the calendar to insert the event into. A special value of <code>'primary'</code> has been set aside for the currently authenticated user. The other required parameter is the event body. In our request, we also ask the Calendar API to send email notifications to the guests, and that's done by passing in the <code>sendNotifications</code> flag with a <code>True</code> value. Our call to the API looks like this:<br />
<pre>e = GCAL.events().insert(calendarId='primary',
sendNotifications=True, body=EVENT).execute()</pre>
The one remaining thing is to confirm that the calendar event was created successfully. We do that by checking the return value — it should be an <a href="https://developers.google.com/google-apps/calendar/v3/reference/events">Event object</a> with all the details we passed in a moment ago:
<br />
<pre>print('''*** %r event added:
Start: %s
End: %s''' % (e['summary'].encode('utf-8'),
e['start']['dateTime'], e['end']['dateTime']))
</pre>
Now, if you really want some proof the event was created, one of the fields that's created is a link to the calendar event. We don't use it in the code, but you can... just use <code>e['htmlLink']</code>.<br />
<br />
Regardless, that's pretty much the entire script save for the OAuth2 code that we're so familiar with from previous posts. The script is posted below in its entirety, and if you run it, depending on the date/times you use, you'll see something like this:
<br />
<pre>$ python gcal_insert.py
*** 'Dinner with friends' event added:
Start: 2015-09-15T19:00:00-07:00
End: 2015-09-15T22:00:00-07:00</pre>
It also works with Python 3 with one slight nit/difference being the "b" prefix on from the event name due to converting from Unicode to <code>bytes</code>:<br />
<pre>*** b'Dinner with friends' event added:</pre>
<h2>
Conclusion</h2>
There can be much more to adding a calendar event, such as events that repeat with a recurrence rule, the ability to add attachments for an event, such as a party invitation or a PDF of the show tickets. For more on what you can do when creating events, take a look at the <a href="https://developers.google.com/google-apps/calendar/v3/reference/events/insert" target="_blank">docs for <code>events().insert()</code></a> as well as the <a href="https://developers.google.com/google-apps/calendar/create-events#add_an_event">corresponding developer guide</a>. All of the docs for the Google Calendar API can be found <a href="http://developers.google.com/google-apps/calendar">here</a>. Also be sure to check out the <a href="http://youtu.be/tNo9IoZMelI?index=97&list=PLOU2XLYxmsIJDPXCTt5TLDu67271PruEk" target="_blank">companion video for this code sample</a>. That's it!<br />
<br />
Below is the entire script for your convenience which runs on both Python 2 <b>and</b> Python 3 (unmodified!):<br />
<pre><span style="font-size: small;"><b>from</b> __future__ <b>import</b> print_function
<b>from</b> apiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
SCOPES = 'https://www.googleapis.com/auth/calendar'
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> creds.invalid:
flow = client.flow_from_clientsecrets('client_secret.json', SCOPES)
creds = tools.run_flow(flow, store)
GCAL = discovery.build('calendar', 'v3', http=creds.authorize(Http()))
GMT_OFF = '-07:00' # PDT/MST/GMT-7
EVENT = {
'summary': 'Dinner with friends',
'start': {'dateTime': '2015-09-15T19:00:00%s' % GMT_OFF},
'end': {'dateTime': '2015-09-15T22:00:00%s' % GMT_OFF},
'attendees': [
{'email': 'friend1@example.com'},
{'email': 'friend2@example.com'},
],
}
e = GCAL.events().insert(calendarId='primary',
sendNotifications=True, body=EVENT).execute()
print('''*** %r event added:
Start: %s
End: %s''' % (e['summary'].encode('utf-8'),
e['start']['dateTime'], e['end']['dateTime']))
</span></pre>
You can now customize this code for your own needs, for a mobile frontend, a server-side backend, or to access other Google APIs. If you want to see <i>another</i> example of using the Calendar API (listing the next 10 events in your calendar), check out <a href="https://developers.google.com/google-apps/calendar/quickstart/python" target="_blank">the Python Quickstart example</a> or its equivalent in Java (server-side, Android), iOS (Objective-C, Swift), C#/.NET, PHP, Ruby, JavaScript (client-side, Node.js), or Go. That's it... hope you find these code samples useful in helping you get started with the Calendar API!<br />
<br />
<h2>
Code challenge</h2>
To test your skills and challenge yourself, try creating recurring events (such as when you expect to receive your paycheck), events with attachments, or perhaps editing existing events. <b>UPDATE (Jul 2017):</b> If you're ready for the next step, we cover the first and last of those choices in our <a href="http://goo.gl/mBJPvN">follow-up post</a>.
<!--[if gte mso 9]><xml>
<w:LatentStyles DefLockedState="false" LatentStyleCount="156">
</w:LatentStyles>
</xml><![endif]--><!--[if gte mso 10]>
<style>
/* Style Definitions */
table.MsoNormalTable
{mso-style-name:"Table Normal";
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-parent:"";
mso-padding-alt:0in 5.4pt 0in 5.4pt;
mso-para-margin:0in;
mso-para-margin-bottom:.0001pt;
mso-pagination:widow-orphan;
font-size:10.0pt;
font-family:"Times New Roman";
mso-ansi-language:#0400;
mso-fareast-language:#0400;
mso-bidi-language:#0400;}
</style>
<![endif]-->wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com0tag:blogger.com,1999:blog-6940043312015460811.post-76314620374765292502015-08-06T11:08:00.001-07:002016-09-19T12:04:33.519-07:00Accessing Gmail from Python (plus BONUS)<b>NOTE:</b> The code covered in this blogpost is also available in a video walkthrough <a href="http://youtu.be/L6hQCgxgzLI?list=PLOU2XLYxmsILOIxBRPPhgYbuSslr50KVq&index=11" target="_blank">here</a>.
<br />
<b><br /></b>
<b>UPDATE (Aug 2016):</b> The code has been modernized to use <code>oauth2client.tools.run_flow()</code> instead of the deprecated <code>oauth2client.tools.run()</code>. You can read more about that change <a href="http://wescpy.blogspot.com/2015/04/google-apis-migrating-from-toolsrun-to.html">here</a>.<br />
<br />
<h2>
Introduction</h2>
The last several posts have illustrated how to connect to public/simple and authorized Google APIs. Today, we're going to demonstrate accessing the Gmail (another authorized) API. Yes, you read that correctly... "API." In the old days, you access mail services with standard Internet protocols such as IMAP/POP and SMTP. However, while they <i>are</i> standards, they haven't kept up with modern day email usage and developers' needs that go along with it. In comes the Gmail API which provides <a href="https://en.wikipedia.org/wiki/Create,_read,_update_and_delete" target="_blank">CRUD</a> access to email threads and drafts along with messages, search queries, management of labels (like folders), and domain administration features that are an extra concern for enterprise developers.<br />
<br />
Earlier posts demonstrate the structure and "how-to" use Google APIs in general, so the most recent posts, including this one, focus on solutions and apps, and use of specific APIs. Once you review the earlier material, you're ready to start with Gmail scopes then see how to use the API itself.<br />
<ul></ul>
<h2>
Gmail API Scopes</h2>
<span style="font-family: inherit;">Below are the <a href="http://developers.google.com/gmail/api/auth/scopes" target="_blank">Gmail API scopes of authorization</a>. We're listing them in most-to-least restrictive order because that's the order you should consider using them in </span>—<span style="font-family: inherit;"> use the most restrictive scope you possibly can yet still allowing your app to do its work. This makes your app more secure and may prevent </span>inadvertently going over any quotas, or accessing, destroying, or corrupting data. Also, users are less hesitant to install your app if it asks only for more restricted access to their inboxes.<br />
<ul>
<li><code>'https://www.googleapis.com/auth/gmail.readonly'</code> — Read-only access to all resources + metadata</li>
<li><code>'https://www.googleapis.com/auth/gmail.send'</code> — Send messages only (no inbox read nor modify)</li>
<li><code>'https://www.googleapis.com/auth/gmail.labels'</code> — Create, read, update, and delete labels only</li>
<li><code>'https://www.googleapis.com/auth/gmail.insert'</code> — Insert and import messages only</li>
<li><code>'https://www.googleapis.com/auth/gmail.compose'</code> — Create, read, update, delete, and send email drafts and messages</li>
<li><code>'https://www.googleapis.com/auth/gmail.modify'</code> — All read/write operations except for immediate & permanent deletion of threads & messages</li>
<li><code>'https://mail.google.com/'</code> — All read/write operations (use with caution)</li>
</ul>
<h2>
Using the Gmail API</h2>
We're going to create a sample Python script that goes through your Gmail threads and looks for those which have more than 2 messages, for example, if you're seeking particularly chatty threads on mailing lists you're subscribed to. Since we're only peeking at inbox content, the only scope we'll request is <span style="font-family: "courier new" , "courier" , monospace;">'gmail.readonly'</span>, the most restrictive scope. The API string is <span style="font-family: "courier new" , "courier" , monospace;">'gmail'</span> which is currently on version 1, so here's the call to <span style="font-family: "courier new" , "courier" , monospace;">apiclient.discovery.build()</span> you'll use:
<br />
<br />
<code>GMAIL = discovery.build('gmail', 'v1', http=creds.authorize(Http()))</code>
<br />
<br />
Note that all lines of code above that is predominantly boilerplate (that was explained in earlier posts). Anyway, once you have an established service endpoint with <span style="font-family: "courier new" , "courier" , monospace;">build()</span>, you can use the <span style="font-family: "courier new" , "courier" , monospace;">list()</span> method of the <span style="font-family: "courier new" , "courier" , monospace;">threads</span> service to request the file data. The one required parameter is the user's Gmail address. A special value of <span style="font-family: "courier new" , "courier" , monospace;">'me'</span> has been set aside for the currently authenticated user.<br />
<pre><span style="font-size: small;">threads = GMAIL.users().threads().list(userId='me').execute().get('threads', [])</span></pre>
If all goes well, the (JSON) response payload will (not be empty or missing and) contain a sequence of threads that we can loop over. For each thread, we need to fetch more info, so we issue a second API call for that. Specifically, we care about the number of messages in a thread:
<br />
<pre><span style="font-size: small;"><b>for</b> thread <b>in</b> threads:
tdata = GMAIL.users().threads().get(userId='me', id=thread['id']).execute()
nmsgs = len(tdata['messages'])
</span></pre>
We're seeking only all threads more than 2 (that means at least 3) messages, discarding the rest. If a thread meets that criteria, scan the first message and cycle through the email headers looking for the "Subject" line to display to users, skipping the remaining headers as soon as we find one:
<br />
<pre><span style="font-size: small;"> <b>if</b> nmsgs > 2:
msg = tdata['messages'][0]['payload']
subject = ''
<b>for</b> header <b>in</b> msg['headers']:
if header['name'] == 'Subject':
subject = header['value']
<b>break</b>
<b>if </b>subject:
print('%s (%d msgs)' % (subject, nmsgs))
</span></pre>
If you're on many mailing lists, this may give you more messages than desired, so feel free to up the threshold from 2 to 50, 100, or whatever makes sense for you. (In that case, you should use a variable.) Regardless, that's pretty much the entire script save for the OAuth2 code that we're so familiar with from previous posts. The script is posted below in its entirety, and if you run it, you'll see an interesting collection of threads... YMMV depending on what messages are in your inbox:
<br />
<pre><span style="font-size: small;">$ python3 gmail_threads.py
[Tutor] About Python Module to Process Bytes (3 msgs)
Core Python book review update (30 msgs)
[Tutor] scratching my head (16 msgs)
[Tutor] for loop for long numbers (10 msgs)
[Tutor] How to show the listbox from sqlite and make it searchable? (4 msgs)
[Tutor] find pickle and retrieve saved data (3 msgs)
</span></pre>
<h2>
BONUS: Python 3!</h2>
As of Mar 2015 (formally in Apr 2015 when the docs were updated), support for Python 3 was added to Google APIs Client Library (3.3+)! This update was a long time coming (<a href="https://github.com/google/google-api-python-client/issues/3" target="_blank">relevant GitHub thread</a>), and allows Python 3 developers to write code that accesses Google APIs. If you're already running 3.x, you can use its <span style="font-family: "courier new" , "courier" , monospace;">pip</span> command (<span style="font-family: "courier new" , "courier" , monospace;">pip3</span>) to install the Client Library:<br />
<br />
<code>$ pip3 install -U google-api-python-client</code><br />
<br />
Because of this, unlike previous blogposts, we're deliberately going to avoid use of the <span style="font-family: "courier new" , "courier" , monospace;"><b>print</b></span> statement and switch to the <span style="font-family: "courier new" , "courier" , monospace;">print()</span> function instead. If you're still running Python 2, be sure to add the following import so that the code will <b>also</b> run in your 2.x interpreter:<br />
<br />
<code><b>from</b> __future__ <b>import</b> print_function</code><br />
<br />
<h2>
Conclusion</h2>
To find out more about the input parameters as well as all the fields that are in the response, take a look at the <a href="https://developers.google.com/gmail/api/v1/reference/users/threads/list" target="_blank">docs for <code>threads().list()</code></a>. For more information on what other operations you can execute with the Gmail API, take a look at <a href="http://developers.google.com/gmail/api" target="_blank">the reference docs</a> and check out the <a href="http://youtu.be/L6hQCgxgzLI?list=PLOU2XLYxmsILOIxBRPPhgYbuSslr50KVq&index=11" target="_blank">companion video for this code sample</a>. That's it!<br />
<br />
Below is the entire script for your convenience which runs on both Python 2 <b>and</b> Python 3 (unmodified!):<br />
<pre><span style="font-size: small;"><b>from</b> __future__ <b>import</b> print_function
<b>from</b> apiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
SCOPES = 'https://www.googleapis.com/auth/gmail.readonly'
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> creds.invalid:
flow = client.flow_from_clientsecrets('client_secret.json', SCOPES)
creds = tools.run_flow(flow, store)
GMAIL = discovery.build('gmail', 'v1', http=creds.authorize(Http()))
threads = GMAIL.users().threads().list(userId='me').execute().get('threads', [])
<b>for</b> thread <b>in</b> threads:
tdata = GMAIL.users().threads().get(userId='me', id=thread['id']).execute()
nmsgs = len(tdata['messages'])
<b>if</b> nmsgs > 2:
msg = tdata['messages'][0]['payload']
subject = ''
<b>for</b> header <b>in</b> msg['headers']:
<b>if</b> header['name'] == 'Subject':
subject = header['value']
<b>break</b>
<b>if</b> subject:
print('%s (%d msgs)' % (subject, nmsgs))</span>
</pre>
You can now customize this code for your own needs, for a mobile frontend, a server-side backend, or to access other Google APIs. If you want to see <i>another</i> example of using the Gmail API (displaying all your inbox labels), check out <a href="http://developers.google.com/gmail/api/quickstart/python" target="_blank">the Python Quickstart example in the official docs</a> or its equivalent in Java (server-side, Android), iOS (Objective-C, Swift), C#/.NET, PHP, Ruby, JavaScript (client-side, Node.js), or Go. That's it... hope you find these code samples useful in helping you get started with the Gmail API!<br />
<br />
<b>EXTRA CREDIT</b>: To test your skills and challenge yourself, try writing code that allows users to perform a search across their email, or perhaps creating an email draft, adding attachments, then sending them! Note that to prevent spam, there are strict <a href="http://gmail.com/intl/en/mail/help/program_policies.html" target="_blank">Program Policies</a> that you must abide with... any abuse could rate limit your account or get it shut down. Check out those rules plus other Gmail terms of use <a href="http://google.com/mail/help/terms_of_use.html" target="_blank">here</a>.<!--[if gte mso 9]><xml>
<w:LatentStyles DefLockedState="false" LatentStyleCount="156">
</w:LatentStyles>
</xml><![endif]--><!--[if gte mso 10]>
<style>
/* Style Definitions */
table.MsoNormalTable
{mso-style-name:"Table Normal";
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-parent:"";
mso-padding-alt:0in 5.4pt 0in 5.4pt;
mso-para-margin:0in;
mso-para-margin-bottom:.0001pt;
mso-pagination:widow-orphan;
font-size:10.0pt;
font-family:"Times New Roman";
mso-ansi-language:#0400;
mso-fareast-language:#0400;
mso-bidi-language:#0400;}
</style>
<![endif]-->wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com0tag:blogger.com,1999:blog-6940043312015460811.post-89483371347309084712015-04-07T00:26:00.002-07:002016-08-27T11:23:59.020-07:00Google APIs: migrating from tools.run() to tools.run_flow()<i>Got </i><i><span style="font-family: "courier new" , "courier" , monospace;">AttributeError</span></i><i>? As in: </i><span class="typ" style="background-color: #eff0f1; border: 0px; color: #2b91af; font-family: "consolas" , "menlo" , "monaco" , "lucida console" , "liberation mono" , "dejavu sans mono" , "bitstream vera sans mono" , "courier new" , monospace , sans-serif; font-size: 13px; margin: 0px; padding: 0px; white-space: inherit;">AttributeError</span><span class="pun" style="background-color: #eff0f1; border: 0px; color: #303336; font-family: "consolas" , "menlo" , "monaco" , "lucida console" , "liberation mono" , "dejavu sans mono" , "bitstream vera sans mono" , "courier new" , monospace , sans-serif; font-size: 13px; margin: 0px; padding: 0px; white-space: inherit;">:</span><span class="pln" style="background-color: #eff0f1; border: 0px; color: #303336; font-family: "consolas" , "menlo" , "monaco" , "lucida console" , "liberation mono" , "dejavu sans mono" , "bitstream vera sans mono" , "courier new" , monospace , sans-serif; font-size: 13px; margin: 0px; padding: 0px; white-space: inherit;"> </span><span class="str" style="background-color: #eff0f1; border: 0px; color: #7d2727; font-family: "consolas" , "menlo" , "monaco" , "lucida console" , "liberation mono" , "dejavu sans mono" , "bitstream vera sans mono" , "courier new" , monospace , sans-serif; font-size: 13px; margin: 0px; padding: 0px; white-space: inherit;">'module'</span><span class="pln" style="background-color: #eff0f1; border: 0px; color: #303336; font-family: "consolas" , "menlo" , "monaco" , "lucida console" , "liberation mono" , "dejavu sans mono" , "bitstream vera sans mono" , "courier new" , monospace , sans-serif; font-size: 13px; margin: 0px; padding: 0px; white-space: inherit;"> object has no attribute </span><span class="str" style="background-color: #eff0f1; border: 0px; color: #7d2727; font-family: "consolas" , "menlo" , "monaco" , "lucida console" , "liberation mono" , "dejavu sans mono" , "bitstream vera sans mono" , "courier new" , monospace , sans-serif; font-size: 13px; margin: 0px; padding: 0px; white-space: inherit;">'run'</span><i>? Rename </i><i><span style="font-family: "courier new" , "courier" , monospace;">run()</span> to <span style="font-family: "courier new" , "courier" , monospace;">run_flow()</span></i><i>, and you'll be good-to-go. <b>TL;DR: </b>This mini-tutorial slash migration guide slash PSA (public service announcement) is aimed at Python developers using the <a href="http://developers.google.com/api-client-library/python" target="_blank">Google APIs Client Library</a> (to access Google APIs from their applications) currently calling <span style="font-family: "courier new" , "courier" , monospace;">oauth2client.tools.run()</span> and likely getting an exception (see Jan 2016 update below), and need to <span style="font-family: "courier new" , "courier" , monospace;">oauth2client.tools.run_flow()</span>, its replacement. </i><br />
<b><br /></b><b>UPDATE (Aug 2016)</b>: The <span style="font-family: "courier new" , "courier" , monospace;">flags</span> parameter in <span style="font-family: "courier new" , "courier" , monospace;">run_flow()</span> function <a href="https://github.com/google/oauth2client/commit/98df500a29157ccaa3f6bdf4517d57a5be8e85f1">became optional in Feb 2016</a>, so tweaked the blogpost to reflect that.<br />
<b><br /></b>
<b>UPDATE (Jun 2016)</b>: Revised the code and cleaned up the dialog so there are no longer any instances of using <span style="font-family: "courier new" , "courier" , monospace;">run()</span> function, significantly shortening this post.<br />
<b><br /></b>
<b>UPDATE (Jan 2016)</b>: The <span style="font-family: "courier new" , "courier" , monospace;">tools.run()</span> function itself <a href="https://github.com/google/oauth2client/commit/05ae3426f271515bab4dc6a210428300286438e8#diff-57fd91e392e1a9696a2348f92e8c87a1">was forcibly <b>removed</b></a> (without a fallback) in Aug 2015, so if you're using any release on or after that, any such calls from your code will throw an exception (<code>AttributeError: 'module' object has no attribute 'run'</code>). To fix this problem, continue reading.<br />
<br />
<h2>
Prelude</h2>
We're going to continue our look at accessing Google APIs from Python. In addition to the previous pair of posts (<a href="http://goo.gl/57Gufk">http://goo.gl/57Gufk</a> and <a href="http://goo.gl/cdm3kZ">http://goo.gl/cdm3kZ</a>), as part of my day job, I've been working on <a href="http://goo.gl/kFMUa6">corresponding video content</a>, some of which are tied specifically to posts on this blog.<br />
<br />
In this follow-up, we're going to specifically address the sidebar in <a href="http://goo.gl/cdm3kZ" target="_blank">the previous post</a>, where we bookmarked an item for future discussion where the future is <b>now</b>: in the <span style="font-family: "courier new" , "courier" , monospace;"><a href="http://github.com/google/oauth2client" target="_blank">oauth2client</a></span> package, <span style="font-family: "courier new" , "courier" , monospace;"><a href="https://github.com/google/oauth2client/blob/master/oauth2client/old_run.py#L50" target="_blank">tools.run()</a></span> has been deprecated by <span style="font-family: "courier new" , "courier" , monospace;"><a href="https://github.com/google/oauth2client/blob/master/oauth2client/tools.py#L111" target="_blank">tools.run_flow()</a></span>. Note you need at least Python 2.7 or 3.3 to use the <a href="http://developers.google.com/api-client-library/python">Google APIs Client Library</a>. (If you didn't even know Python 3 was supported at all, then you need to see <a href="http://goo.gl/OfCbOz">this post</a> and <a href="http://qr.ae/RbbrHI">this Quora Q&A</a>.)<!--As explained, use of <span style="font-family: "courier new" , "courier" , monospace;">tools.run()</span> is "easier," meaning less code on-screen (or in a blogpost) hence why I've been using it for code samples. But truth be told that it <i>is</i> outdated. Another problem is that <span style="font-family: "courier new" , "courier" , monospace;">tools.run()</span> requires users to install another package (<span style="font-family: "courier new" , "courier" , monospace;"><a href="http://pypi.python.org/pypi/python-gflags" target="_blank">python-gflags</a></span>), typically with a command like: "<span style="font-family: "courier new" , "courier" , monospace;">pip install -U python-gflags</span>".<br />
<br />
Let's look at <span style="font-family: "courier new" , "courier" , monospace;">tools.run_flow()</span> so that you can see the better alternative and can code accordingly, even if the code samples in the videos or blogposts use t<span style="font-family: "courier new" , "courier" , monospace;">ools.run()</span>. Yes, <span style="font-family: "courier new" , "courier" , monospace;">tools.run_flow()</span> does requires a recent version of Python.--><br />
<br />
<h2>
Replacing <span style="font-family: "courier new" , "courier" , monospace;">tools.run()</span> with <span style="font-family: "courier new" , "courier" , monospace;">tools.run_flow()</span></h2>
Now let's convert the authorized access to Google APIs code from using <span style="font-family: "courier new" , "courier" , monospace;">tools.run()</span> to <span style="font-family: "courier new" , "courier" , monospace;">tools.run_flow()</span>. Here is the old snippet I'm talking about that needs upgrading:<br />
<pre><span style="font-size: small;"><b>from</b> apiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
SCOPES = # one or more scopes (str or iterable)
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> creds.invalid:
flow = client.flow_from_clientsecrets('client_secret.json', SCOPES)
creds = tools.run(flow, store)
SERVICE = discovery.build(API, VERSION, http=creds.authorize(Http()))
</span></pre>
If you're using the latest Client Library (<a href="https://github.com/google/oauth2client/commit/98df500a29157ccaa3f6bdf4517d57a5be8e85f1">as of Feb 2016</a>), all you need to do is change the <span style="font-family: monospace;">tools.run()</span> call to <span style="font-family: monospace;">tools.run_flow()</span>, as italicized below. Everything else stays <i>exactly</i> the same:<br />
<pre><span style="font-size: small;"><b>from</b> apiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
SCOPES = # one or more scopes (str or iterable)
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> creds.invalid:
flow = client.flow_from_clientsecrets('client_secret.json', SCOPES)
creds = tools.run<i>_flow</i>(flow, store)</span></pre>
If you don't have the latest Client Library, then your update involves the extra steps of adding lines that <code><b>import</b> argparse</code> and using it to get the <code>flags</code> argument needed by <code>tools.run_flow()</code> plus the actual change from <code>tools.run()</code>; all updates italicized below:<br />
<pre><span style="font-size: small;"><i><b>import</b> argparse</i>
<b>from</b> apiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
SCOPES = # one or more scopes (str or iterable)
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> creds.invalid:
<i> flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args()</i>
flow = client.flow_from_clientsecrets('client_id.json', SCOPES)
creds = tools.run<i>_flow</i>(flow, store<i>, flags</i>)
SERVICE = discovery.build(API, VERSION, http=creds.authorize(Http()))
</span></pre>
<br />
<table border="4"><tbody>
<tr><td><b>Command-line argument processing, or "Why argparse?"</b><br />
<br />
Python has had several modules in the Standard Library that allow developers to process command-line arguments. The original one was <span style="font-family: "courier new" , "courier" , monospace;"><a href="http://docs.python.org/library/getopt" target="_blank">getopt</a></span> which mirrored the <span style="font-family: "courier new" , "courier" , monospace;">getopt()</span> function from C. In Python 2.3, <span style="font-family: "courier new" , "courier" , monospace;"><a href="http://docs.python.org/library/optparse" target="_blank">optparse</a></span> was introduced, featuring more powerful processing capabilities. However, it was deprecated in 2.7 in favor of a similar module, <span style="font-family: "courier new" , "courier" , monospace;"><a href="http://docs.python.org/library/argparse" target="_blank">argparse</a></span>. (To find out more about their similarities, differences and rationale behind developing <span style="font-family: "courier new" , "courier" , monospace;">argparse</span> , see <a href="http://python.org/dev/peps/pep-0389" target="_blank">PEP 389</a> and <a href="https://argparse.googlecode.com/svn/trunk/doc/argparse-vs-optparse.html" target="_blank">this <span style="font-family: "courier new" , "courier" , monospace;">argparse</span> docs page</a>.) For the purposes of using Google APIs, you're all set if using Python 2.7 as it's included in the Standard Library. Otherwise Python 2.3-2.6 users can install it with: "<span style="font-family: "courier new" , "courier" , monospace;">pip install -U argparse</span>". </td></tr>
</tbody></table>
<br />
Irregardless of whether you need <code>argparse</code>, once you migrate to either snippet with <code>tools.run_flow()</code>, your application should go back to working the way it had before.wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com0tag:blogger.com,1999:blog-6940043312015460811.post-82504004323654546522014-11-06T17:07:00.000-08:002020-03-30T15:10:30.681-07:00Authorized Google API access from Python (part 2 of 2)<span style="font-size: large;">Listing your files with the Google Drive API</span><br />
<b><br /></b>
<b>NOTE:</b> You can also watch a video walkthrough of the common code covered in this blogpost <a href="http://youtu.be/h-gBeC9Y9cE?list=PLOU2XLYxmsILOIxBRPPhgYbuSslr50KVq&index=3">here</a>.
<br />
<b><br /></b>
<b>UPDATE (Mar 2020):</b> You can build this application line-by-line with our <a href="https://g.co/codelabs/gsuite-apis-intro" target="_blank">codelab</a> (self-paced, hands-on tutorial) introducing developers to G Suite APIs. The deprecated auth library comment from the previous update below is spelled out in more detail in the green sidebar towards the bottom of step/module 5 (Install the Google APIs Client Library for Python). Also, the code sample is now maintained in a <a href="http://github.com/googlecodelabs/gsuite-apis-intro" target="_blank">GitHub repo</a> which includes a port to the newer auth libraries so you have both versions to refer to.<br />
<br />
<b>UPDATE (Apr 2019):</b> In order to have a closer relationship between the GCP and G Suite worlds of Google Cloud, all G Suite Python code samples have been updated, replacing some of the older G Suite API client libraries with their equivalents from GCP. NOTE: <a href="https://developers.google.com/drive/api/v3/quickstart/python" target="_blank">using the newer libraries</a> requires more initial code/effort from the developer thus will seem "less Pythonic." However, we will leave the code sample here with the original client libraries (deprecated but not shutdown yet) to be consistent with the video.<br />
<b><br /></b><b>UPDATE (Aug 2016):</b> The code has been modernized to use <code>oauth2client.tools.run_flow()</code> instead of the deprecated <code>oauth2client.tools.run_flow()</code>. You can read more about that change <a href="http://wescpy.blogspot.com/2015/04/google-apis-migrating-from-toolsrun-to.html">here</a>.<br />
<b><br /></b>
<b>UPDATE (Jun 2016):</b> Updated to Python 2.7 & 3.3+ and Drive API v3.<br />
<br />
<h2>
Introduction</h2>
In this final installment of a (currently) two-part series introducing Python developers to building on Google APIs, we'll extend from the simple API example from the <a href="http://wescpy.blogspot.com/2014/09/simple-google-api-access-from-python.html" target="_blank">first post</a> (part 1) just over a month ago. Those first snippets showed some skeleton code and a short real working sample that demonstrate accessing a public (Google) API with an API key (that queried public Google+ posts). An API key however, does <b>not</b> grant applications access to <i>authorized</i> data.<br />
<br />
Authorized data, including user information such as personal files on Google Drive and YouTube playlists, require additional security steps before access is granted. Sharing of and hardcoding credentials such as usernames and passwords is not only insecure, it's also a thing of the past. A more modern approach leverages token exchange, authenticated API calls, and standards such as <a href="http://oauth.net/" target="_blank">OAuth2</a>.<br />
<br />
In this post, we'll demonstrate how to use Python to access authorized Google APIs using OAuth2, specifically listing the files (and folders) in your Google Drive. In order to better understand the example, we strongly recommend you check out the OAuth2 guides (<a href="http://developers.google.com/accounts/docs/OAuth2" target="_blank">general OAuth2 info</a>, <a href="http://developers.google.com/api-client-library/python/guide/aaa_oauth" target="_blank">OAuth2 as it relates to Python and its client library</a>) in the documentation to get started.<br />
<br />
The docs describe the OAuth2 flow: making a request for authorized access, having the user grant access to your app, and obtaining a(n access) token with which to sign and make authorized API calls with. The steps you need to take to get started begin nearly the same way as for simple API access. The process diverges when you arrive on the Credentials page when following the steps below.<br />
<div>
<br /></div>
<h2>
Google API access</h2>
In order to Google API authorized access, follow these instructions (the first three of which are roughly the same for simple API access):<br />
<ul>
<li>Go to the <a href="http://console.developers.google.com/" target="_blank">Google Developers Console</a> and login.</li>
<ul>
<li>Use your Gmail or Google credentials; create an account if needed</li>
</ul>
<li>Click "Create a Project" from pulldown under your username (at top)</li>
<ul>
<li>Enter a Project Name (mutable, human-friendly string only used in the console)</li>
<li>Enter a Project ID (immutable, must be unique and not already taken)</li>
</ul>
<li>Once project has been created, enable APIs you wish to use</li>
<ul>
<li>You can toggle on any API(s) that support(s) simple or authorized API access.</li>
<li>For the code example below, we use the <a href="http://developers.google.com/drive" target="_blank">Google Drive API</a>.</li>
<li>Other ideas: <a href="http://developers.google.com/youtube" target="_blank">YouTube Data API</a>, <a href="http://developers.google.com/sheets" target="_blank">Google Sheets API</a>, etc.</li>
<li>Find more APIs (and version#s which you need) at the <a href="http://developers.google.com/oauthplayground" target="_blank">OAuth Playground</a>.</li>
</ul>
<li>Select "Credentials" in left-nav</li>
<ul>
<li>Click "Create credentials" and select OAuth client ID</li>
<li>In the new dialog, select your application type — we're building a command-line script which is an "Installed application"</li>
<li>In the bottom part of that same dialog, specify the <i>type</i> of installed application; choose "Other" (cmd-line scripts are not web nor mobile)</li>
<li>Click "Create Client ID" to generate your credentials</li>
</ul>
<li>Finally, click "Download JSON" to save the new credentials to your computer... perhaps choose a shorter name like "<span style="font-family: "courier new" , "courier" , monospace;">client_secret.json</span>" or "<span style="font-family: "courier new" , "courier" , monospace;">client_id.json</span>"</li>
<ul>
</ul>
</ul>
<b>NOTEs:</b> Instructions from the previous blogpost were to get an <i>API key</i>. This time, in the steps above, we're creating <b>and</b> downloading <i>OAuth2 credentials</i>. You can also watch a video walkthrough of this app setup process of getting simple or authorized access credentials in the "DevConsole" <a href="http://youtu.be/DYAwYxVs2TI?index=2&list=PLOU2XLYxmsILOIxBRPPhgYbuSslr50KVq">here</a>.
<br />
<ul></ul>
<h2>
Accessing Google APIs from Python</h2>
In order to access authorized Google APIs from Python, you still need the <a href="http://developers.google.com/api-client-library/python" target="_blank">Google APIs Client Library for Python</a>, so in this case, <i>do</i> follow those installation instructions from <a href="http://wescpy.blogspot.com/2014/09/simple-google-api-access-from-python.html" target="_blank">part 1</a>.<br />
<br />
We will again use <span style="font-family: "courier new" , "courier" , monospace;">googleapiclient.discovery.build()</span>, which is required to create a service endpoint for interacting with an API, authorized or otherwise. However, for authorized data access, we need additional resources, namely the <span style="font-family: "courier new" , "courier" , monospace;">httplib2</span> and <span style="font-family: "courier new" , "courier" , monospace;">oauth2client</span> packages. Here are the first five lines of the <i>new</i> boilerplate code for authorized access:<br />
<br />
<pre><b>from</b> __future__ <b>import</b> print_function
</pre>
<pre><b>from</b> googleapiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
SCOPES = # one or more scopes (strings)
</pre>
<span style="font-family: "courier new" , "courier" , monospace;">SCOPES</span><span style="font-family: inherit;"> is a critical variable: it represents the set of scopes of authorization an app wants to obtain (then access) on behalf of user(s). What's does a scope look like?</span><br />
<span style="font-family: inherit;"><br /></span>
<span style="font-family: inherit;">Each scope is a single character string, </span>specifically<span style="font-family: inherit;"> a URL. Here are some examples:</span><br />
<ul>
<li><span style="font-family: "courier new" , "courier" , monospace;">'https://www.googleapis.com/auth/plus.me'</span><span style="font-family: inherit;"> — access your personal Google+ settings</span></li>
<li><span style="font-family: "courier new" , "courier" , monospace;">'https://www.googleapis.com/auth/drive.metadata.readonly'</span><span style="font-family: inherit;"> — read-only access your Google Drive file or folder metadata</span></li>
<li><span style="font-family: "courier new" , "courier" , monospace;">'https://www.googleapis.com/auth/youtube'</span><span style="font-family: inherit;"> — access your YouTube playlists and other personal information</span></li>
</ul>
You can request one or more scopes, given as a <b>single space-delimited string</b> of scopes or an <b>iterable</b> (list, generator expression, etc.) <b>of strings</b>. <span style="font-family: inherit;">If you were writing an app that accesses both your YouTube playlists as well as your Google+ profile information, your </span><span style="font-family: "courier new" , "courier" , monospace;">SCOPES</span><span style="font-family: inherit;"> variable could be either of the following:</span><br />
<code><span style="font-size: x-small;">SCOPES = 'https://www.googleapis.com/auth/plus.me https://www.googleapis.com/auth/youtube'</span></code><br />
<span style="font-family: inherit;"><br /></span><span style="font-family: inherit;">That is space-delimited and made tiny by me so it doesn't wrap in a regular-sized browser window; or it could be an easier-to-read, non-tiny, and non-wrapped tuple:</span><br />
<span style="font-family: inherit;"><br /></span>
<span style="font-family: "courier new" , "courier" , monospace;">SCOPES = (</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'https://www.googleapis.com/auth/plus.me',</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'https://www.googleapis.com/auth/youtube',</span><br />
<span style="font-family: "courier new" , "courier" , monospace;">)</span><br />
<span style="font-family: inherit;"><br /></span>
<span style="font-family: inherit;">Our example command-line script will just list the files on your Google Drive, so we only need the read-only Drive metadata scope, meaning our </span><span style="font-family: "courier new" , "courier" , monospace;">SCOPES</span><span style="font-family: inherit;"> variable will be just this:</span><br />
<code>SCOPES = 'https://www.googleapis.com/auth/drive.metadata.readonly'</code><br />
<span style="font-family: inherit;">The next section of boilerplate represents the security code:</span><br />
<pre>store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> creds.invalid:
flow = client.flow_from_clientsecrets('client_secret.json', SCOPES)
creds = tools.run_flow(flow, store)
</pre>
<span style="font-family: inherit;">Once the user has authorized access to their personal data by your app, a special "access token" is given to your app. This precious resource must be stored somewhere local for the app to use. In our case, we'll store it in a file called "</span><span style="font-family: "courier new" , "courier" , monospace;">storage.json</span><span style="font-family: inherit;">". The lines setting the </span><span style="font-family: "courier new" , "courier" , monospace;">store</span><span style="font-family: inherit;"> and </span><span style="font-family: "courier new" , "courier" , monospace;">creds</span><span style="font-family: inherit;"> variables are attempting to get a valid access token with which to make an authorized API call.</span><br />
<span style="font-family: inherit;"><br /></span>
If the credentials are missing or invalid, such as being expired, the authorization flow (using the client secret you downloaded along with a set of requested scopes) must be created (by <span style="font-family: "courier new" , "courier" , monospace;">client.flow_from_clientsecrets()</span>) and executed (by <span style="font-family: "courier new" , "courier" , monospace;">tools.run_flow()</span>) to ensure possession of valid credentials. <span style="font-family: inherit;">The </span><span style="font-family: "courier new" , "courier" , monospace;">client_secret.json</span><span style="font-family: inherit;"> file is the credentials file you saved when you clicked "Download JSON" from the DevConsole after you've created your OAuth2 client ID.</span><br />
<br />
If you don't have credentials at all, the user much explicitly grant permission — I'm sure you've all seen the OAuth2 dialog describing the type of access an app is requesting (remember those scopes?). Once the user clicks "Accept" to grant permission, a valid access token is returned and saved into the storage file (because you passed a handle to it when you called <span style="font-family: "courier new" , "courier" , monospace;">tools.run_flow()</span>).<br />
<br />
<table border="4"><tbody>
<tr><td><b>Note: <code>tools.run()</code> deprecated by <code>tools.run_flow()</code></b><br />
You may have seen usage of the older <code>tools.run()</code> function, but it has been deprecated by <code>tools.run_flow()</code>. We explain this in more detail in <a href="http://wescpy.blogspot.com/2015/04/migrating-from-toolsrun-to-toolsrunflow.html" target="_blank">another blogpost</a> specifically geared towards migration.</td></tr>
</tbody></table>
<br />
Once the user grants access and valid credentials are saved, you can create one or more endpoints to the secure service(s) desired with <span style="font-family: "courier new" , "courier" , monospace;">googleapiclient.discovery.build()</span>, just like with simple API access. Its call will look slightly different, mainly that you need to sign your HTTP requests with your credentials rather than passing an API key:<br />
<div>
<br /></div>
<span style="font-family: "courier new" , "courier" , monospace;">DRIVE = discovery.build(<i>API</i>, <i>VERSION</i>, http=creds.authorize(Http()))</span><br />
<br />
In our example, we're going to list your files and folders in your Google Drive, so for <span style="font-family: "courier new" , "courier" , monospace;">API</span>, use the string <span style="font-family: "courier new" , "courier" , monospace;">'drive'</span>. The API is currently on version 3 so use <span style="font-family: "courier new" , "courier" , monospace;">'v3'</span> for <span style="font-family: "courier new" , "courier" , monospace;">VERSION</span>:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;">DRIVE = discovery.build('drive', 'v3', http=creds.authorize(Http()))</span><br />
<div>
<br /></div>
If you want to get comfortable with OAuth2, what it's flow is and how it works, we recommend that you experiment at the <a href="http://developers.google.com/oauthplayground" target="_blank">OAuth Playground</a>. There you can choose from any number of APIs to access and experience first-hand how your app must be authorized to access personal data.<br />
<br />
Going back to our working example, once you have an established service endpoint, you can use the <span style="font-family: "courier new" , "courier" , monospace;">list()</span> method of the <span style="font-family: "courier new" , "courier" , monospace;">files</span> service to request the file data:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;">files = DRIVE.files().list().execute().get('files', [])</span><br />
<br />
If there's any data to read, the response dict will contain an iterable of files that we can loop over (or default to an empty list so the loop doesn't fail), displaying file names and types:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;"><b>for</b> f <b>in</b> files:</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> print(f['name'], f['mimeType'])</span><br />
<br />
<h2>
Conclusion</h2>
To find out more about the input parameters as well as all the fields that are in the response, take a look at the <a href="http://developers.google.com/drive/v3/reference/files/list" target="_blank">docs for <span style="font-family: "courier new" , "courier" , monospace;">files().list()</span></a>. For more information on what other operations you can execute with the Google Drive API, take a look at <a href="http://developers.google.com/drive/v2/reference" target="_blank">the reference docs</a> and check out the <a href="http://youtu.be/Z5G0luBohCg?list=PLOU2XLYxmsILOIxBRPPhgYbuSslr50KVq&index=4">companion video for this code sample</a>. Don't forget the <a href="http://g.co/codelabs/gsuite-apis-intro" target="_blank">codelab</a> and this sample's <a href="http://github.com/googlecodelabs/gsuite-apis-intro" target="_blank">GitHub repo</a>. That's it!<br />
<br />
Below is the entire script for your convenience:
<br />
<pre>'''
drive_list.py -- Google Drive API demo; maintained at:
http://github.com/googlecodelabs/gsuite-apis-intro
'''
<b>from</b> __future__ <b>import</b> print_function
<b>from</b> googleapiclient <b>import</b> discovery
<b>from</b> httplib2 <b>import</b> Http
<b>from</b> oauth2client <b>import</b> file, client, tools
SCOPES = 'https://www.googleapis.com/auth/drive.readonly.metadata'
store = file.Storage('storage.json')
creds = store.get()
<b>if not</b> creds <b>or</b> 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', [])
<b>for</b> f <b>in</b> files:
print(f['name'], f['mimeType'])
</pre>
When you run it, you should see pretty much what you'd expect, a list of file or folder names followed by their MIMEtypes — I named my script <span style="font-family: "courier new" , "courier" , monospace;">drive_list.py</span>:<br />
<pre><span style="font-size: small;">$ python3 drive_list.py
Google Maps demo application/vnd.google-apps.spreadsheet
Overview of Google APIs - Sep 2014 application/vnd.google-apps.presentation
tiresResearch.xls application/vnd.google-apps.spreadsheet
6451_Core_Python_Schedule.doc application/vnd.google-apps.document
out1.txt application/vnd.google-apps.document
tiresResearch.xls application/vnd.ms-excel
6451_Core_Python_Schedule.doc application/msword
out1.txt text/plain
Maps and Sheets demo application/vnd.google-apps.spreadsheet
ProtoRPC Getting Started Guide application/vnd.google-apps.document
gtaskqueue-1.0.2_public.tar.gz application/x-gzip
Pull Queues application/vnd.google-apps.folder
gtaskqueue-1.0.1_public.tar.gz application/x-gzip
appengine-java-sdk.zip application/zip
taskqueue.py text/x-python-script
Google Apps Security Whitepaper 06/10/2010.pdf application/pdf
</span></pre>
Obviously your output will be different, depending on what files are in your Google Drive. But that's it... hope this is useful. You can now customize this code for your own needs and/or to access other Google APIs. Thanks for reading!<br />
<br />
<b>EXTRA CREDIT</b>: To test your skills, add functionality to this code that also displays the last modified timestamp, the file (byte)size, and perhaps shave the MIMEtype a bit as it's slightly harder to read in its entirety... perhaps take just the final path element? One last challenge: in the output above, we have both Microsoft Office documents as well as their auto-converted versions for Google Apps... perhaps only show the filename once and have a double-entry for the filetypes!
<!--[if gte mso 9]><xml>
<w:LatentStyles DefLockedState="false" LatentStyleCount="156">
</w:LatentStyles>
</xml><![endif]--><!--[if gte mso 10]>
<style>
/* Style Definitions */
table.MsoNormalTable
{mso-style-name:"Table Normal";
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-parent:"";
mso-padding-alt:0in 5.4pt 0in 5.4pt;
mso-para-margin:0in;
mso-para-margin-bottom:.0001pt;
mso-pagination:widow-orphan;
font-size:10.0pt;
font-family:"Times New Roman";
mso-ansi-language:#0400;
mso-fareast-language:#0400;
mso-bidi-language:#0400;}
</style>
<![endif]-->wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com3tag:blogger.com,1999:blog-6940043312015460811.post-633952906523579962014-09-20T00:35:00.005-07:002016-08-26T12:44:23.444-07:00Simple Google API access from Python (part 1 of 2)<b>NOTE:</b> You can also watch a video walkthrough of the common code covered in this blogpost <a href="http://youtu.be/h-gBeC9Y9cE?list=PLOU2XLYxmsILOIxBRPPhgYbuSslr50KVq&index=3">here</a>.<br />
<b><br /></b><b>UPDATE (Aug 2016):</b> The code has been modernized to recognize that the Client Library is available for Python 2 or 3.<br />
<br />
<h2>
Introduction</h2>
Back in 2012 when I published <a href="http://amzn.com/0132678209" target="_blank"><i>Core Python Applications Programming</i>, 3rd ed.</a>, I<br />
<a href="http://wescpy.blogspot.com/2012/04/integrating-google-apis-and.html" target="_blank">posted</a> about how I integrated Google technologies into the book. The only problem is that I presented very specific code for <a href="http://developers.google.com/appengine" target="_blank">Google App Engine</a> and <a href="http://plus.google.com/" target="_blank">Google+</a> only. I didn't show a generic way how, using pretty much the same boilerplate Python snippet, you can access any number of Google APIs; so here we are.<br />
<br />
In this multi-part series, I'll break down the code that allows <b>you</b> to leverage Google APIs to the most basic level (even for Python), so you can customize as necessary for your app, whether it's running as a command-line tool or something server-side in the cloud backending Web or mobile clients. If you've got the book and played around with our <a href="http://developers.google.com/+" target="_blank">Google+ API</a> example, you'll find this code familiar, if not identical — I'll go into more detail here, highlighting the common code for generic API access and <i>then</i> bring in the G+-relevant code later.<br />
<br />
We'll start in this first post by demonstrating how to access public or <i>unauthorized</i> data from Google APIs. (The next post will illustrate how to access <i>authorized</i> data from Google APIs.) Regardless of which you use, the corresponding boilerplate code stands alone. In fact, it's probably best if you saved these generic snippets in a library module so you can (re)use the same bits for any number of apps which access any number of <a href="https://developers.google.com/api-client-library/python/apis" target="_blank">modern Google APIs</a>.<br />
<br />
<h2>
Google API access</h2>
In order to access Google APIs, follow these instructions:<br />
<ul>
<li>Go to the <a href="http://console.developers.google.com/" target="_blank">Google Developers Console</a> and login.</li>
<ul>
<li>Use your Gmail or Google credentials; create an account if needed</li>
</ul>
<li>Click "Create Project" button</li>
<ul>
<li>Enter a Project Name (mutable, human-friendly string only used in the console)</li>
<li>Enter a Project ID (immutable, must be unique and not already taken)</li>
</ul>
<li>Once project has been created, click "Enable an API" button</li>
<ul>
<li>You can toggle on any API(s) that support(s) simple API access (not authorized).</li>
<li>For the code example below, we use the Google+ API.</li>
<li>Other ideas: YouTube Data API, Google Maps API, etc.</li>
<li>Find more APIs (and version#s which you need) at the <a href="http://developers.google.com/oauthplayground" target="_blank">OAuth Playground</a>.</li>
</ul>
<li>Select "Credentials" in left-nav under "APIs & auth"</li>
<ul>
<li>Go to bottom half and click "Create new Key" button</li>
<li>Grab long "API KEY" cryptic string and save to Python script</li>
</ul>
</ul>
<ul></ul>
<b>NOTE:</b> You can also watch a video walkthrough of this app setup process in the "DevConsole" <a href="http://youtu.be/DYAwYxVs2TI?index=2&list=PLOU2XLYxmsILOIxBRPPhgYbuSslr50KVq">here</a>.
<br />
<h2>
Accessing Google APIs from Python</h2>
Now that you're set up, everything else is done on the Python side. To talk to a Google API, you need the <a href="http://developers.google.com/api-client-library/python" target="_blank">Google APIs Client Library for Python</a>, specifically the <span style="font-family: "courier new" , "courier" , monospace;">apiclient.discovery.build()</span> function. Download and install the library in your usual way, for example:<br />
<div>
<br /></div>
<span style="font-family: "courier new" , "courier" , monospace;">$ pip install -U google-api-python-client # or pip3 for 3.x</span><br />
<blockquote class="tr_bq">
<b>NOTE</b>: If you're building a Python App Engine app, you'll need something else, the <a href="https://developers.google.com/api-client-library/python/guide/google_app_engine" target="_blank">Google APIs Client Library for Python on Google App Engine</a>. It's similar but has extra goodies (specifically decorators — brief generic intro to those in <a href="http://wescpy.blogspot.com/2014/07/introduction-to-python-decorators.html" target="_blank">my previous post</a>) just for cloud developers that must be installed elsewhere. As App Engine developers know, libraries must be in the same location on the filesystem as your source code.</blockquote>
Once everything is installed, make sure that you can import <span style="font-family: "courier new" , "courier" , monospace;">apiclient.discovery</span>:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;">$ python</span><br />
<span style="font-family: "courier new" , "courier" , monospace;">Python 2.7.6 (default, Apr 9 2014, 11:48:52)</span><br />
<span style="font-family: "courier new" , "courier" , monospace;">[GCC 4.2.1 Compatible Apple LLVM 5.1 (clang-503.0.38)] on darwin</span><br />
<span style="font-family: "courier new" , "courier" , monospace;">Type "help", "copyright", "credits" or "license" for more information.</span><br />
<span style="font-family: "courier new" , "courier" , monospace;">>>> <b>import</b> apiclient.discovery</span><br />
<span style="font-family: "courier new" , "courier" , monospace;">>>></span><br />
<br />
In <span style="font-family: "courier new" , "courier" , monospace;">discovery.py</span> is the <span style="font-family: "courier new" , "courier" , monospace;">build()</span> function, which is what we need to create a service endpoint for interacting with an API. Now craft the following lines of code in your command-line tool, using the shorthand <span style="font-family: "courier new" , "courier" , monospace;"><b>from-import</b></span> statement instead:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;"><b>from</b> apiclient <b>import</b> </span><span style="font-family: "courier new" , "courier" , monospace;">discovery</span><br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;">API_KEY = # copied from project credentials page</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"><i>SERVICE</i> = </span><span style="font-family: "courier new" , "courier" , monospace;">discovery.</span><span style="font-family: "courier new" , "courier" , monospace;">build(</span><i style="font-family: 'courier new', courier, monospace;">API, VERSION</i><span style="font-family: "courier new" , "courier" , monospace;">, developerKey=API_KEY)</span><br />
<br />
Take the API key you copied from the credentials page and assign to the <span style="font-family: "courier new" , "courier" , monospace;">API_KEY</span> variable as a string. <i>Obviously</i>, embedding an API key in source code isn't something you'd so in practice as it's <b>not secure</b> whatsoever — stick it in a database, key broker, encrypt, or at least have it in a separate byte code (<span style="font-family: "courier new" , "courier" , monospace;">.pyc</span>/<span style="font-family: "courier new" , "courier" , monospace;">.pyo</span>) file that you import — but we'll allow it now solely for illustrative purposes of a simple command-line script.<br />
<br />
In our short example we're going to do a simple search for "python" in public Google+ posts, so for the <span style="font-family: "courier new" , "courier" , monospace;">API</span> variable, use the string <span style="font-family: "courier new" , "courier" , monospace;">'plus'</span>. The API version is currently on version 1 (at the time of this writing), so use <span style="font-family: "courier new" , "courier" , monospace;">'v1'</span> for <span style="font-family: "courier new" , "courier" , monospace;">VERSION</span>. (Each API will use a different name and version string... again, you can find those in the <a href="http://developers.google.com/oauthplayground" target="_blank">OAuth Playground</a> or in the docs for the specific API you want to use.) Here's the call once we've filled in those variables:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;">GPLUS = </span><span style="font-family: "courier new" , "courier" , monospace;">discovery.</span><span style="font-family: "courier new" , "courier" , monospace;">build('plus', 'v1', developerKey=API_KEY)</span><br />
<br />
We need a template for the results that come back. There are many fields in a Google+ post, so we're only going to pick three to display... the user name, post timestamp, and a snippet of the post itself:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;">TMPL = '''</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> User: %s</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> Date: %s</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> Post: %s</span><br />
<span style="font-family: "courier new" , "courier" , monospace;">'''</span><br />
<br />
Now for the code. Google+ posts are activities (known as "notes;" there are other activities as well). One of the methods you have access to is <span style="font-family: "courier new" , "courier" , monospace;">search()</span>, which lets you <b>query</b> public activities; so that's what we're going to use. Add the following call using the <span style="font-family: "courier new" , "courier" , monospace;">GPLUS</span> service endpoint you already created using the verbs we just described and execute it:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace; font-size: x-small;">items = GPLUS.activities().search(query='python').execute().get('items', [])</span><br />
<br />
If all goes well, the (JSON) response payload will contain a set of <span style="font-family: "courier new" , "courier" , monospace;">'items'</span> (else we assign an empty list for the <span style="font-family: "courier new" , "courier" , monospace;"><b>for</b></span> loop). From there, we'll loop through each matching post, do some minor string manipulation to replace all whitespace characters (including NEWLINEs [ <span style="font-family: "courier new" , "courier" , monospace;">\n</span> ]) with spaces, and display if not blank:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;"><b>for</b> data <b>in</b> items:</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> post = ' '.join(data['title'].strip().split())</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> <b>if</b> post:</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> print(TMPL % (</span><span style="font-family: "courier new" , "courier" , monospace;">data['actor']['displayName'],</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> data['published'], post))</span><br />
<br />
<br />
<h2>
Conclusion</h2>
To find out more about the input parameters as well as all the fields that are in the response, take a look at <a href="http://developers.google.com/+/api/latest/activities/search" target="_blank">the docs</a>. Below is the entire script missing only the <span style="font-family: "courier new" , "courier" , monospace;">API_KEY</span> which you'll have to fill in yourself.<br />
<br />
<pre><b>from</b> __future__ <b>import</b> print_function
<b>from</b> apiclient <b>import</b> discovery
TMPL = '''
User: %s
Date: %s
Post: %s
'''
API_KEY = # copied from project credentials page
GPLUS = discovery.build('plus', 'v1', developerKey=API_KEY)
items = GPLUS.activities().search(query='python').execute().get('items', [])
<b>for</b> data <b>in</b> items:
post = ' '.join(data['title'].strip().split())
<b>if</b> post:
print(TMPL % (data['actor']['displayName'],
data['published'], post))
</pre>
<div>
<br />
When you run it, you should see pretty much what you'd expect, a few posts on Python, some on Monty Python, and of course, some on the snake — I called my script <span style="font-family: "courier new" , "courier" , monospace;">plus_search.py</span>:<br />
<br />
<pre>$ python plus_search.py # or python3
User: Jeff Ward
Date: 2014-09-20T18:08:23.058Z
Post: How to make python accessible in the command window.
User: Fayland Lam
Date: 2014-09-20T16:40:11.512Z
Post: Data Engineer http://findmjob.com/job/AB7ZKitA5BGYyW1oAlQ0Fw/Data-Engineer.html #python #hadoop #jobs...
User: Willy's Emporium LTD
Date: 2014-09-20T16:19:33.851Z
Post: MONTY PYTHON QUOTES MUG Take a swig to wash down all that albatross and crunchy frog. Featuring 20 ...
User: Doddy Pal
Date: 2014-09-20T15:49:54.405Z
Post: Classic Monty Python!!!
User: Sebastian Huskins
Date: 2014-09-20T15:33:00.707Z
Post: Made a small python script to get shellcode out of an executable. I found a nice commandlinefu.com oneline...
</pre>
<div>
<br /></div>
<b>EXTRA CREDIT</b>: To test your skills, check the docs and add a fourth line to each output which is the URL/link to that specific post, so that you (and your users) can open a browser to it if of interest.<br />
<br />
If you want to build on from here, check out the <i>larger</i> app using the Google+ API featured in Chapter 15 of the book — it adds some brains to this basic code where the Google+ posts are sorted by popularity using a "chatter" score. That just about wraps it up this post. Once you're good to go, then you're ready to learn how to perform <a href="http://goo.gl/cdm3kZ"><b>authorized Google API access</b></a> in part 2 of this two-part series!<br />
<div>
<br /></div>
</div>
<!--[if gte mso 9]><xml>
<w:LatentStyles DefLockedState="false" LatentStyleCount="156">
</w:LatentStyles>
</xml><![endif]--><!--[if gte mso 10]>
<style>
/* Style Definitions */
table.MsoNormalTable
{mso-style-name:"Table Normal";
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-parent:"";
mso-padding-alt:0in 5.4pt 0in 5.4pt;
mso-para-margin:0in;
mso-para-margin-bottom:.0001pt;
mso-pagination:widow-orphan;
font-size:10.0pt;
font-family:"Times New Roman";
mso-ansi-language:#0400;
mso-fareast-language:#0400;
mso-bidi-language:#0400;}
</style>
<![endif]-->wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com0tag:blogger.com,1999:blog-6940043312015460811.post-61726917871243438172014-07-26T21:28:00.002-07:002023-02-11T18:14:18.511-08:00Introduction to Python decorators<!--[if gte mso 9]><xml>
<w:WordDocument>
<w:View>Normal</w:View>
<w:Zoom>0</w:Zoom>
<w:PunctuationKerning/>
<w:ValidateAgainstSchemas/>
<w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid>
<w:IgnoreMixedContent>false</w:IgnoreMixedContent>
<w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText>
<w:Compatibility>
<w:BreakWrappedTables/>
<w:SnapToGridInCell/>
<w:WrapTextWithPunct/>
<w:UseAsianBreakRules/>
<w:DontGrowAutofit/>
</w:Compatibility>
<w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel>
</w:WordDocument>
</xml><![endif]-->
In this post, we're going to give you a user-friendly introduction to Python decorators. (The code works on both Python 2 [2.6 or 2.7 only] <i>and </i>3 so don't be concerned with your version.) Before jumping into the topic du jour, consider the usefulness of the
<span style="font-family: "Courier New",Courier,monospace;">map()</span> function. You've got a list with some data and want to apply some
function [like <span style="font-family: "Courier New",Courier,monospace;">times2()</span> below] to all its elements and get a new list with the modified data:<br />
<br />
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"><b>def </b>times2(x):</span></span><br />
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"> <b>return </b>x * 2</span></span><br />
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"></span><br /></span>
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;">>>> list(map(times2, [0, 1, 2, 3, 4]))</span></span><br />
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;">
</span></span><span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;">[0, 2, 4, 6, 8]</span></span><br />
<br />
Yeah yeah, I know that you can do the same thing with a <i>list comprehension
</i>or <i>generator expression</i>, but my point was about an
independent piece of logic [like <span style="font-family: "Courier New",Courier,monospace;">times2()</span>] and mapping that function across a data set (<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;">[0, 1, 2, 3, 4]</span></span>) to generate a new data set (<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;">[0, 2, 4, 6, 8]</span></span>). However, since mapping functions like <span style="font-family: "Courier New",Courier,monospace;">times2()</span>aren't tied to any particular chunk of data, you can reuse them elsewhere with
other unrelated (or related) data.<br />
<br />
Along similar lines, consider function calls. You have independent functions and methods in classes. Now, think about "mapped" execution across
functions. What are things that you can do with functions that don't have much
to do with the behavior of the functions themselves? How about <i>logging
</i>function calls, <i>timing</i> them, or some other introspective, <i><a href="http://en.wikipedia.org/wiki/Cross-cutting_concern">cross-cutting</a></i> behavior. Sure you can implement that behavior in each of the functions that you care
about such information, however since they're so generic, it would be nice to
only write that logging code just once.<br />
<br />
<div class="MsoNormal">
Introduced in 2.4, decorators modularize
cross-cutting behavior so that developers <b style="mso-bidi-font-weight: normal;">don't</b>
have to implement near duplicates of the same piece of code for each function.
Rather, Python gives them the ability to put that logic in one place and use
decorators with its at-sign ("@") syntax to "map" that
behavior to any function (or method). This compartmentalization of cross-cutting
functionality gives Python an <i><a href="http://en.wikipedia.org/wiki/Aspect-oriented_programming">aspect-oriented programming</a></i> flavor.</div>
<div class="MsoNormal">
<br /></div>
<div class="MsoNormal">
How do you do this in Python? Let's take a look at a simple
example, the logging of function calls. Create a decorator function that takes
a function object as its sole argument, and implement the cross-cutting
functionality. In <span style="font-family: "Courier New",Courier,monospace;">logged()</span> below, we're just going to log function calls by
making a call to the <span style="font-family: "Courier New",Courier,monospace;">print()</span> function each time a logged function is called.</div>
<div class="MsoNormal">
<br /></div>
<div class="MsoNormal">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"><b>def </b>logged(_func):</span></span></div>
<div class="MsoNormal">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"> <b>def </b>_wrapped():</span></span><br />
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"> print('Function %r
called at: %s' % (</span></span></div>
<div class="MsoNormal">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"> _func.__name__, ctime()))</span></span></div>
<div class="MsoNormal">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"> <b>return </b>_func()</span></span><br />
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"> <b>return </b>_wrapped </span></span></div>
<div class="MsoNormal">
<br /></div>
<div class="MsoNormal">
In <span style="font-family: "Courier New",Courier,monospace;">logged()</span>, we use the function's name (given by
<span style="font-family: "Courier New",Courier,monospace;">func.__name__</span>) plus a timestamp from <span style="font-family: "Courier New",Courier,monospace;">time.ctime()</span> to build our output string.
Make sure you get the right imports, <span style="font-family: "Courier New",Courier,monospace;">time.ctime()</span> for sure, and if using Python
2, the <span style="font-family: "Courier New",Courier,monospace;">print()</span> function:</div>
<div class="MsoNormal">
<br /></div>
<div class="MsoNormal">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"><b>from </b>__future__ <b>import </b>print_function # 2-3 compatibility<br />
<b>from </b>time <b>import </b>ctime</span></span></div>
<div class="MsoNormal">
<br /></div>
<div class="MsoNormal">
Now that we have our <span style="font-family: "Courier New",Courier,monospace;">logged()</span> decorator, how do we use it? On
the line above the function which you want to apply the decorator to, place an
at-sign in front of the decorator name. That's followed immediately on the next
line with the normal function declaration. Here's what it looks like, applied
to a boring generic <span style="font-family: "Courier New",Courier,monospace;">foo()</span> function which just <span style="font-family: "Courier New",Courier,monospace;">print()</span>s it's been called.</div>
<div class="MsoNormal">
<br /></div>
<div class="MsoNormal">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;">@logged</span></span></div>
<div class="MsoNormal">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"><b>def </b>foo():</span></span></div>
<div class="MsoNormal">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"> print('foo()
called')</span></span></div>
<div class="MsoNormal">
<br /></div>
<div class="MsoNormal">
When you call <span style="font-family: "Courier New",Courier,monospace;">foo()</span>, you can see that the decorator <span style="font-family: "Courier New",Courier,monospace;">logged()</span>
is called first, which then calls <span style="font-family: "Courier New",Courier,monospace;">foo()</span> on your behalf:</div>
<div class="MsoNormal">
<br /></div>
<div class="MsoNormal">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;">$ log_func.py</span></span></div>
<div class="MsoNormal">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;">Function 'foo' called at: Sun Jul 27 04:09:37 2014</span></span></div>
<div class="MsoNormal">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;">foo() called</span></span></div>
<div class="MsoNormal">
<br /></div>
<div class="MsoNormal">
<span style="mso-spacerun: yes;">If you take a closer look at </span><span style="mso-spacerun: yes;"><span style="font-family: "Courier New",Courier,monospace;">logged()</span> above</span>, the way the decorator works is that the decorated function is
"wrapped" so that it is passed as <span style="font-family: "Courier New",Courier,monospace;">func</span> to the decorator then the
newly-wrapped function <span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;">_wrapped()</span></span>is (re)assigned as <span style="font-family: "Courier New",Courier,monospace;">foo()</span>. That's why it now behaves the
way it does when you call it.</div>
<div class="MsoNormal">
<br />
The entire script:<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">
#!/usr/bin/env python<br />
'log_func.py -- demo of decorators'<br />
<br />
<b>from </b>__future__ <b>import </b>print_function</span><span style="font-family: 'Courier New', Courier, monospace;"> # 2-3 compatibility</span><span style="font-family: "Courier New",Courier,monospace;"><br />
<b>from </b>time <b>import </b>ctime<br />
<br />
<b>def </b>logged(_func):<br />
<b>def </b>_wrapped():<br />
print('Function %r called at: %s' % (<br />
_func.__name__, ctime()))<br />
<b>return </b>_func()<br />
<b>return </b>_wrapped<br /><br />
@logged<br />
<b>def </b>foo():<br />
print('foo() called')<br /><br />foo()</span><br />
<br /></div>
<div class="MsoNormal">
That was just a simple example to give you an idea of what decorators are. If you dig a little deeper, you'll discover one caveat is that the wrapping isn't perfect. For example, the attributes of <span style="font-family: 'Courier New', Courier, monospace;">foo()</span> are lost, i.e., its name and docstring. If you ask for either, you'll get <span style="font-family: 'Courier New', Courier, monospace;">_wrapped()</span>'s info instead:<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">>>> print("My name:", foo.__name__) # should be 'foo'!</span><br />
<span style="font-family: Courier New, Courier, monospace;">My name: _wrapped</span><br />
<span style="font-family: Courier New, Courier, monospace;">>>> print("Docstring:", foo.__doc__) # _wrapped's docstring!</span><br />
<span style="font-family: Courier New, Courier, monospace;">Docstring: None</span><br />
<div>
<br /></div>
In reality, the "@" syntax is just a shortcut. Here's what you really did, which should explain this behavior:<br />
<br />
<div class="MsoNormal">
<b style="font-family: "Courier New", Courier, monospace;">def </b><span style="font-family: 'Courier New', Courier, monospace;">foo():</span></div>
<span style="font-family: 'Courier New', Courier, monospace;"> print('foo() called')</span><br />
<div class="MsoNormal" style="-webkit-text-stroke-width: 0px; color: black; font-family: Times; font-size: medium; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px;">
<div style="margin: 0px;">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"><br /></span></span></div>
<div style="margin: 0px;">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;">foo = logged(foo) # returns _wrapped (and its attributes)</span></span></div>
</div>
<br />
So as you can tell, it's not a complete wrap. A convenience function that ties up these loose ends is <span style="font-family: Courier New, Courier, monospace;"><a href="https://docs.python.org/2/library/functools.html#functools.wraps">functools.wraps()</a></span>. If you use it and run the same code, you will get <span style="font-family: 'Courier New', Courier, monospace;">foo()</span>'s info. However, if you're not going to use a function's attributes while it's wrapped, it's less important to do this.<br />
<br />
There's also support for additional
features, such calling decorated functions with parameters, applying more
complex decorators, applying multiple levels of decorators, and also class
decorators. You can find out more about (function and method) decorators in
Chapter 11 of <a href="http://amzn.com/dp/0132269937"><i>Core Python Programming</i></a> or live in <a href="http://goo.gl/pyJseQ">my upcoming course</a> which starts in just a few days near the San Francisco airport... there are still a few seats left!</div>
<!--[if gte mso 9]><xml>
<w:LatentStyles DefLockedState="false" LatentStyleCount="156">
</w:LatentStyles>
</xml><![endif]--><!--[if gte mso 10]>
<style>
/* Style Definitions */
table.MsoNormalTable
{mso-style-name:"Table Normal";
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-parent:"";
mso-padding-alt:0in 5.4pt 0in 5.4pt;
mso-para-margin:0in;
mso-para-margin-bottom:.0001pt;
mso-pagination:widow-orphan;
font-size:10.0pt;
font-family:"Times New Roman";
mso-ansi-language:#0400;
mso-fareast-language:#0400;
mso-bidi-language:#0400;}
</style>
<![endif]-->wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com0tag:blogger.com,1999:blog-6940043312015460811.post-8290492714080616112013-09-04T00:01:00.001-07:002015-04-17T16:21:38.920-07:00Learning ProgrammingTwo years ago, I wrote a post on "<a href="http://wescpy.blogspot.com/2011/09/learning-python.html">learning Python</a>" to launch this blog dedicated to Python. While useful, it doesn't address beginners' needs as much, so it's time for a revisit. Because Python is such a user-friendly language for beginners, I'm often asked whether Python is the "best first language" for those new to programming. While tempted to respond in the affirmative, my answer really is, "it depends." It depends on your experience, age, level of exposure, etc.<br />
<br />
Yes, there are indeed plenty of resources out there, such as courses from online learning brands such as Khan Academy, Udacity, Coursera, Codecademy, CodeSchool, and edX, but most certainly don't come <i>with </i>an instructor, instead relying on live or recorded videos and possibly supplemental study groups, or "cohort learning," as a colleague of mine has branded it. Whatever the mechanism, it's surely better than pure online tutorials or slaving away over a book, neither of which come with instructors either.<br />
<br />
Stepping back a bit, before jumping into hardcore C/C++, Java, PHP, Ruby, or Javascript lessons, for learning tools that are used in industry today, there are better stepping stones to get you there. You may be a kid or a professional who either doesn't code much or had done so long ago. You're say that type of user who is "insulted" by the move "left" or "right" commands for controlling a turtle, say, and desire something more complex. The good news is that there <i>are </i>tools out there, more which allow you to venture further without an instructor.<br />
<br />
One of them is <a href="http://scratch.mit.edu/">Scratch</a>, a "jigsaw puzzle"-like programming language created at MIT (or <a href="http://tynker.com/" target="_blank">Tynker</a> or <a href="http://developers.google.com/blockly" target="_blank">Blockly</a>, Scratch-like derivatives). Yes, you <i>will </i>do left, right, up, down, etc., but you'll also get to play audio, video, repeat commands, draw graphics, and make sounds. This tool is great for teaching the young learner, who don't need any of the advanced features but which are available for when they're ready to take the next step. It can be used to teach children the <i>concepts</i> of programming without all the syntax that text-based programming languages feature which may make learning those concepts a burden.<br />
<br />
If you wish to proceed, go to the website to get started. They've got videos there as well as projects you can copy. As you can see, you snap together puzzle pieces that teach you coding. Better yet, to get started even <i>more</i> quickly, clone one (or more) of the projects, and "tweak" the code a bit to "do your own thing." In time, you may even develop your own fun applications or real games. Another similar graphical learning tool to consider is <a href="http://alice.org/">Alice</a> from the University of Virginia and now Carnegie-Mellon University.<br />
<br />
Once you're comfortable with that type of working environment, there's a similar tool from MIT called <a href="http://appinventor.mit.edu/">App Inventor</a>. Leverage your Scratch skills and start building applications that run on Android devices! There's an emulator, so you don't really <i>need</i> an Android device, but it's certainly more rewarding when you can use an app that you built running on a tablet or phone! (Try a family friend who may have an old device they don't use any more.)<br />
<br />
Once you're to move beyond block-like languages, there are 2 good choices (or better yet, do both!). One of which the de facto language of the web: Javascript. Unfortunately, there are so many online tutorials out there, I wouldn't know which to suggest, so looking forward to your comments below. The ones which are the most effective however, have you learning then coding directly into the browser and seeing results immediately, requiring you to write successful Javascript before allowing you to move on.<br />
<br />
The thing about Javascript is that code typically only runs within the browser, to control web pages (i.e., "DOM manipulation") and actions you can take on a single page -- it can also be AJAX code that makes an external call to update a page without requiring a page load. Nevertheless, browser-only execution can be somewhat limiting, so there are now 2 additional ways you can use it.<br />
<br />
One is to write "server-side" applications via <a href="http://nodejs.org/">Node.js</a>. This type of Javascript allows you to write code that executes on the remote machine serving your web pages (generally) after you've entered information in a form and clicked submit. For every web page that users see and interact with, there's also got to be code on the server side that does all the work! This code will also end up returning the final HTML that users see in their browsers once the form has been submitted and results returned.<br />
<br />
Another place you can use Javascript is in Google's cloud. The tool there is called <a href="http://developers.google.com/apps-script">Google Apps Script</a>. Using Apps Script, you can create applications that interact with various Google Apps, automate repetitive tasks, or write glue code that lets you connect and share data between different Google services. Try some of their tutorials to get started!<br />
<br />
The other option besides Javascript is Python. No doubt you already know what it is since you're here. Python's syntax is extremely approachable for beginners and is widely considered "executable pseudocode." That's right, a programming language that doesn't require you have a Computer Science degree to make good use of it! It's also one of those rare languages that can be used by adults in the professional world as well as by kids learning how to code. Sure there are many online learning systems out there, a sampling of which are here:<br />
<ul>
<li><a href="http://codecademy.com/tracks/python">Codecademy</a></li>
<li><a href="http://learnpython.org/">LearnPython.org</a></li>
<li><a href="http://pyschools.com/">PySchools</a></li>
<li><a href="http://pythontutor.com/">Online Python Tutor</a></li>
<li><a href="http://learnstreet.com/lessons/study/python">LearnStreet </a>(login optional)</li>
<li><a href="http://codingbat.com/python">CodingBat </a>(login optional)</li>
<li><a href="http://singpath.com/">SingPath </a>(login required)</li>
</ul>
See if you like any of them or have your new coder friends try them out. However, I think kids (and even adults) learn programming best when they get to write cool games (leveraging the amazing <a href="http://pygame.org/">PyGame </a>library). There are several books written just for kids, including "Hello World" which was actually written by an engineer and his son! Along with that book there are two more you should consider:<br />
<ul>
<li><a href="http://helloworldbookblog.com/">Hello World! Computer Programming for Kids and Other Beginners</a> (Sande & Sande)</li>
<li><a href="http://inventwithpython.com/">Invent your Own Computer Games with Python</a> (Sweigart)</li>
<li><a href="http://nostarch.com/pythonforkids">Python for Kids: A Playful Introduction to Programming</a> (Briggs)</li>
</ul>
Two of the three books above are in the beginners list I created over a year ago along with two other Python reading lists in <a href="http://goo.gl/i4u0R">this post</a>. (The third book should be added to the list as well.) Those of you who are already programmers probably know which one I would recommend. <span style="font-family: Courier New, Courier, monospace;">:-)</span> Seriously though, those reading lists show that I can toot other horns too. <span style="font-family: Courier New, Courier, monospace;">:P</span><br />
<br />
Here are other online projects and learning resources, including book websites, that you can also try (many are for kids):<br />
<ul>
<li><a href="http://openbookproject.net/thinkcs/python/english3e">How to Think Like a Computer Scientist</a> (Downey, Elkner, Meyers, Wentworth) </li>
<li><a href="http://alan-g.me.uk/">Learning to Program</a> (Gauld)</li>
<li><a href="http://livewires.org.uk/python">LiveWires Python course</a></li>
<li><a href="http://swaroopch.com/notes/python">A Byte of Python</a> (Swaroop)</li>
<li><a href="http://hetland.org/writing/instant-hacking.html">Instant Hacking: Learning to Program with Python</a> (Hetland)</li>
<li><a href="http://briggs.net.nz/snake-wrangling-for-kids.html">Snake Wrangling for Kids</a> (Briggs)</li>
<li><a href="http://handysoftware.com/cpif">Computer Programming is Fun!</a> (Handy)</li>
<li><a href="http://gvr.sf.net/">Guido van Robot</a></li>
<li><a href="http://rur-ple.sf.net/">RUR-PLE</a></li>
</ul>
In conjunction with a good learning system, book, or project-based learning above, you <i>should</i> also try out one of many free online courses to validate things you've picked up but to also build other knowledge you <i>haven't</i> learned yet. There are a pair from Coursera and one from Udacity:<br />
<ul>
<li><a href="http://coursera.org/course/interactivepython">An Introduction to Interactive Programming in Python</a> (Rice University)</li>
<li><a href="http://coursera.org/course/programming1">Learn to Program: The Fundamentals</a> (University of Toronto)</li>
<li><a href="http://udacity.com/course/cs101">Introduction to Computer Science</a> (University of Virginia)</li>
</ul>
For existing programmers who are still questioning why Python, check out Udacity's <a href="http://blog.udacity.com/2012/05/learning-to-program-why-python.html">motivational blogpost</a>.<br />
<br />
That's it! Hopefully I've given you enough resources you can pass along to friends and family members who are intrigued by your passion for computer programming and wish to see what all the excitement is all about. A young man I met on vacation this summer motivated this post... good luck Mitchell! I hope to see the rest of <i>you </i>on the road as well, perhaps at a developers' conference or sitting in one of my upcoming Python courses!<br />
<div>
<br /></div>
wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com2tag:blogger.com,1999:blog-6940043312015460811.post-23424813731212349092012-05-29T14:14:00.012-07:002012-06-07T22:58:34.423-07:00Tuples aren't what you think they're for<div style="font-family: Georgia, serif; font-size: 100%; font-style: normal; font-variant: normal; font-weight: normal; line-height: normal; ">While I'm happy that the number of Python users continues to grow at a rapid pace and that there are many tutorials added each day to support all the newbies, there are a few things that make me cringe when I see them.</div><div style="font-family: Georgia, serif; font-size: 100%; font-style: normal; font-variant: normal; font-weight: normal; line-height: normal; "><br /></div><div style="font-size: 100%; font-style: normal; font-variant: normal; font-weight: normal; line-height: normal; "><span style="font-family: Georgia, serif; ">One example of this is seeing a Python college textbook (you can tell by its retail price) produced by a big-name publisher (one of the largest in the world which shall remain unnamed) that instructs users (of Python 2), to get user command-line input using the </span><code>input()</code><span style="font-family: Georgia, serif; "> function! Clearly, this is a major faux pas, as most Python users know that it's a security risk and that </span><code>raw_input()</code><span> should always be used instead (and the main reason why </span><code>raw_input()</code><span> replaces and is renamed as </span><code>input()</code><span> in Python 3).</span></div><div style="font-family: Georgia, serif; font-size: 100%; font-style: normal; font-variant: normal; font-weight: normal; line-height: normal; "><br /></div><div style="font-family: Georgia, serif; font-size: 100%; font-style: normal; font-variant: normal; font-weight: normal; line-height: normal; ">Another example is this <a href="http://pythoncentral.org/lists-and-tuples">recent article on lists and tuples</a>. While I find the content useful in teaching new Python developers various useful ways of using slicing, I disagree with the premise that tuples...</div><div style="font-family: Georgia, serif; font-size: 100%; font-style: normal; font-variant: normal; font-weight: normal; line-height: normal; "><ol><li><span style="font-size: 100%; ">along with lists are two of Python's most popular data structures</span></li><li><span style="font-size: 100%; ">are mostly immutable but there are workarounds, and</span></li><li><span style="font-size: 100%; ">should be used for application data manipulation</span></li></ol></div><div style="font-family: Georgia, serif; font-size: 100%; font-style: normal; font-variant: normal; font-weight: normal; line-height: normal; "><br /></div><div style="font-style: normal; font-family: Georgia, serif; font-size: 100%; font-variant: normal; font-weight: normal; line-height: normal; "><span style="font-style: normal; ">I would says lists and </span><i>dictionaries</i> are the two most popular Python data structures; tuples shouldn't even be in that group. In fact, I would even argue that tuples shouldn't be used to manipulate application data at all, as that wasn't what they were generally created for. (If this was the case, then why not have lists with a read-only flag?)</div><div style="font-style: normal; font-family: Georgia, serif; font-size: 100%; font-variant: normal; font-weight: normal; line-height: normal; "><br /></div><div style="font-style: normal; font-family: Georgia, serif; font-size: 100%; font-variant: normal; font-weight: normal; line-height: normal; ">The main reason why tuples exist is to get data to and from function calls. [UPDATE: two other strong use cases: 1) "constructed" dictionary keys (i would've turned such N-tuples into a delimited string) and from that use comes 2) a data structure with positional semantics, aka indices with implied meaning... both of these view such tuples as an individual entity (made up of multiple components), again, not a data structure for manipulating objects. Named tuples is an related alternative. See the debate in the commentary below.]</div><div style="font-style: normal; font-family: Georgia, serif; font-size: 100%; font-variant: normal; font-weight: normal; line-height: normal; "><br /></div><div style="font-style: normal; font-family: Georgia, serif; font-size: 100%; font-variant: normal; font-weight: normal; line-height: normal; ">Calling a foreign API or 3rd-party function and want to pass in a data structure you know can't be altered? Check. Calling any function where you want to pass in only one data structure (instead of separate variables)? Use "*" and you're good to go. Previously worked with a programming language that only allowed you to return a single value? Tuples are that <i>one</i> object (think of it as a single shopping bag for all your groceries).</div><div style="font-style: normal; font-family: Georgia, serif; font-size: 100%; font-variant: normal; font-weight: normal; line-height: normal; "><br /></div><div style="font-style: normal; font-family: Georgia, serif; font-size: 100%; font-variant: normal; line-height: normal; "><span style="font-weight: normal; ">All of the manipulations in the post on getting around the immutability are superfluous and not adhering to the best practice of </span><i>not</i> using tuples as a data structure. I mean, this is not a strict rule. If you're needing a data structure where you're not going to make any modifications and desire slightly better performance, sure a tuple can be used in such cases. This is why in Python 2.6, for the first time "evar," tuples were given methods!</div><div style="font-style: normal; font-family: Georgia, serif; font-size: 100%; font-variant: normal; line-height: normal; "><br /></div><div style="font-style: normal; font-size: 100%; font-variant: normal; line-height: normal; "><span style="font-family: Georgia, serif; ">There was never any need for tuples to have methods because they were immutable. "Just use lists," is what we would all say. However, lists had a pair of read-only methods (</span><code>count()</code><span> and </span><code>index()</code><span>) that led to inefficiencies (and poor practices) where developers used tuples for the reason we just outlined but needed to either get a count on how many times an object appeared in that sequence or wanted to find the index of the first appearance of an object. They would have to convert that tuple to a list, just to call those methods. Starting in 2.6, tuples now have those (and only those) methods to avoid this extra nonsense.</span></div><div style="font-style: normal; font-size: 100%; font-variant: normal; line-height: normal; "><span><br /></span></div><div style="font-size: 100%; font-variant: normal; line-height: normal; "><span>So yes, you can use tuples as user-land data structures in such cases, but that's really it. For manipulation, use lists instead. As stated at the top, I'm generally all for more intro posts and tutorials out there. However, there may be some that don't always impart the best practices out there. Readers should always be alert and question whether there are more "Pythonic" ways of doing things. In this case, tuples should <i>not</i> be one of the "[two] of the most commonly used built-in data types in Python...."</span></div>wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com18tag:blogger.com,1999:blog-6940043312015460811.post-25726347911879824142012-04-06T14:19:00.019-07:002012-04-07T10:03:37.523-07:00Integrating Google APIs and Technologies<span id="internal-source-marker_0.7833635245915502" style="text-align: -webkit-auto; font-size: medium; "><span style="font-weight: normal; font-style: normal; font-size: 15px; font-family: Arial; vertical-align: baseline; white-space: pre-wrap; "><span style="font-weight: normal; ">In 1997, long before my tenure at </span><a href="http://google.com/">Google</a><span style="font-weight: normal; ">, I became a member of the </span><a href="http://python.org/">Python</a><span style="font-weight: normal; "> community in helping to create </span></span><a href="http://mail.yahoo.com/" style="font-family: Times; font-weight: normal; font-style: normal; "><span style="font-size: 15px; font-family: Arial; color: rgb(17, 85, 204); font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">Yahoo!Mail</span></a><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">, one of the most popular web-based email systems in the world. There were only two Python books on the market back then, and neither addressed my developer’s need to learn Python quickly and competently, so I had to resort to the online docs. This absence, and consequently my development of class materials for a Python course, inspired me to write Prentice Hall’s bestselling </span><span style="font-size: 15px; font-family: Arial; font-weight: normal; font-style: italic; vertical-align: baseline; white-space: pre-wrap; ">Core Python Programming</span><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> over a decade ago. Since then, I’ve used Python to work on all kinds of interesting applications, from web-based e-mail to geolocalized product search engines, social media games, antispam/antivirus e-mail appliances, and most interestingly, software for doctors to help them analyze and assess patients with spinal fractures. (Ask me about osteoporosis!)</span><br /><span style="font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "></span><br /><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">Today at Google, my work involves advocating our tools and APIs to the global developer community. Now that I've been part of the Google family for the past 2.5 years, I thought it would be fun to integrate some of our technologies into the book. With the just-published 3rd edition, readers will find revised but also brand new material they can use to build real applications with. Some of the Google technologies I've integrated into </span><a href="http://corepython.com/" style="font-family: Times; font-weight: normal; font-style: normal; "><span style="font-size: 15px; font-family: Arial; color: rgb(17, 85, 204); font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">Co</span><span style="font-size: 15px; font-family: Arial; color: rgb(17, 85, 204); font-weight: normal; font-style: italic; vertical-align: baseline; white-space: pre-wrap; ">re Python Applications Programming</span></a><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> include accessing your </span><a href="http://mail.google.com/" style="font-family: Times; font-weight: normal; font-style: normal; "><span style="font-size: 15px; font-family: Arial; color: rgb(17, 85, 204); font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">Gmail</span></a><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">, parsing </span><a href="http://news.google.com/" style="font-family: Times; font-weight: normal; font-style: normal; "><span style="font-size: 15px; font-family: Arial; color: rgb(17, 85, 204); font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">Google News</span></a><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> XML feeds, and a complete chapter on cloud computing with </span><a href="http://code.google.com/appengine" style="font-family: Times; font-weight: normal; font-style: normal; "><span style="font-size: 15px; font-family: Arial; color: rgb(17, 85, 204); font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">Google App Engine</span></a><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">. It’s also the first published book to feature code that utilizes the </span><a href="http://developers.google.com/+/api" style="font-family: Times; font-weight: normal; font-style: normal; "><span style="font-size: 15px; font-family: Arial; color: rgb(17, 85, 204); font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">Google+ API</span></a><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">. While the book contains a longer example using that API, I want to show you how easy it is to connect to </span><a href="http://developers.google.com/+" style="font-family: Times; font-weight: normal; font-style: normal; "><span style="font-size: 15px; font-family: Arial; color: rgb(17, 85, 204); font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">Google+</span></a><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> using Python right now!</span><br /><span style="font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "></span><br /><span style="font-weight: normal; font-style: normal; font-size: 15px; font-family: Arial; vertical-align: baseline; white-space: pre-wrap; "><span style="font-weight: normal; ">The bulk of the work in connecting to Google+ (and </span><a href="http://code.google.com/more">other Google APIs</a><span style="font-weight: normal; ">) is done by my fellow colleagues who maintain the </span></span><a href="http://code.google.com/p/google-api-python-client" style="font-family: Times; font-weight: normal; font-style: normal; "><span style="font-size: 15px; font-family: Arial; color: rgb(17, 85, 204); font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">Google APIs Client Library for Python</span></a><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">, easily downloaded with </span><span style="font-style: normal; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "><a href="http://pip-installer.org/">pip</a></span><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> or </span><span style="font-style: normal; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "><a href="http://pypi.python.org/pypi/setuptools">easy_install</a></span><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> as "google-api-python-client." With this library, the most difficult step to connect with your API of choice has basically been reduced to a single line... see the fourth line of this short Python 2.x example:</span><br /><span style="font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "></span><br /><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "># plus.py (by Wesley Chun under </span><a href="http://creativecommons.org/licenses/by-sa/3.0/" style="font-family: Times; font-weight: normal; font-style: normal; "><span style="font-size: 15px; font-family: 'Courier New'; color: rgb(17, 85, 204); font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">CC-SA3.0 license</span></a><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">)</span><br /><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; vertical-align: baseline; white-space: pre-wrap; "><b>from</b></span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> apiclient </span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; vertical-align: baseline; white-space: pre-wrap; "><b>import</b></span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> discovery</span><br /><span style="font-size: 13px; font-family: Verdana; color: rgb(0, 0, 136); background-color: rgb(255, 255, 255); font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "></span><br /><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">API_KEY = </span><span style="font-size: 15px; font-family: 'Courier New'; font-weight: normal; font-style: italic; vertical-align: baseline; white-space: pre-wrap; ">YOUR_KEY_FROM_CONSOLE_API_ACCESS_PAGE</span><br /><span style="font-family: Times; font-weight: normal; "><span><i><span style="font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">service = discovery.build("plus", "v1", developerKey=API_KEY)</span><br /></i></span></span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">feed = service.activities().search(query='android').execute()</span><br /><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; vertical-align: baseline; white-space: pre-wrap; "><b>for</b></span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> record </span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; vertical-align: baseline; white-space: pre-wrap; "><b>in</b></span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> feed['items']:</span><br /><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> post = ' '.join(record['title'].strip().split())</span><br /><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> </span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; vertical-align: baseline; white-space: pre-wrap; "><b>if</b></span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> post:</span><br /><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> </span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; vertical-align: baseline; white-space: pre-wrap; "><b>print</b></span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> '\nFrom:', record['actor']['displayName']</span><br /><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> </span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; vertical-align: baseline; white-space: pre-wrap; "><b>print</b></span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> 'Post:', post</span><br /><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> </span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; vertical-align: baseline; white-space: pre-wrap; "><b>print</b></span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> 'Date:', record['published']</span><br /><span style="font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "></span><br /><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">In that one line of code (</span><span style="font-family: Times; "><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "><i>italicized</i></span></span><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> above), we use the Google APIs Client Library's </span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "><a href="http://google-api-python-client.googlecode.com/hg/docs/apiclient.discovery.html#-build">apiclient.discovery.build()</a></span><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> method, passing in: a) the desired API (</span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">"plus"</span><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> for Google+), b) the version (currently </span><span style="font-style: normal; font-size: 15px; font-family: 'Courier New'; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">"v1"</span><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">), and c) the API key you obtained from </span><a href="http://code.google.com/apis/console" style="font-family: Times; font-weight: normal; font-style: normal; "><span style="font-size: 15px; font-family: Arial; color: rgb(17, 85, 204); font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">your project's development console</span></a><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> in the "Simple API Access" section. This key gives your project access to APIs that do not need to access user data. Once we have a handle to the service, we can execute generic queries on the available data stream.</span><br /><span style="font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "></span><br /><span style="font-weight: normal; font-style: normal; font-size: 15px; font-family: Arial; vertical-align: baseline; white-space: pre-wrap; "><span style="font-weight: normal; ">In this code snippet, we're simply querying for the latest (public) Google+ posts that are related to </span><a href="http://android.com/">Android</a><span style="font-weight: normal; "> and displaying them on the command-line (code can be easily repurposed into any mobile or web application). Naturally, you need to go through the </span><a href="http://oauth.net/" style="font-weight: normal; ">OAuth</a><span style="font-weight: normal; "> flow if you do want access to authenticated data. Give it a try!</span></span><br /><span style="font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "></span><br /><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">If you like the code, dig into </span><span style="font-size: 15px; font-family: Arial; font-weight: normal; font-style: italic; vertical-align: baseline; white-space: pre-wrap; ">Core Python Applications Programming </span><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">for a longer, more detailed example. Both scripts can be downloaded at </span><span style="font-style: normal; font-size: 15px; font-family: Arial; color: rgb(17, 85, 204); font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "><a href="http://corepython.com/" style="font-weight: normal; font-style: normal; ">the website</a> </span><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">(the code is part of Chapter 15), and you can get involved in the conversation on </span><a href="http://plus.ly/corepython" style="font-family: Times; font-weight: normal; font-style: normal; "><span style="font-size: 15px; font-family: Arial; color: rgb(17, 85, 204); font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">the Google+ page</span></a><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">. I'm open to all feedback, suggestions, and fixes. You can find me at </span><a href="http://plus.ly/wescpy" style="font-family: Times; font-weight: normal; font-style: normal; "><span style="font-size: 15px; font-family: Arial; color: rgb(17, 85, 204); font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">+wescpy</span></a><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; "> or </span><a href="http://twitter.com/wescpy" style="font-family: Times; font-weight: normal; font-style: normal; "><span style="font-size: 15px; font-family: Arial; color: rgb(17, 85, 204); font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">@wescpy</span></a><span style="font-style: normal; font-size: 15px; font-family: Arial; font-weight: normal; vertical-align: baseline; white-space: pre-wrap; ">. Looking forward to meeting you at an upcoming Google or Python event or in <a href="http://cyberwebconsulting.com/">one of my public courses</a>!</span></span>wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com1tag:blogger.com,1999:blog-6940043312015460811.post-24439975339390411032012-03-09T00:02:00.026-08:002012-03-21T09:45:27.456-07:00A new PyCon... and a new book (and a new article)!!I'm excited about this year's <a href="http://us.pycon.org/" style="font-style: normal; ">PyCon</a> conference happening this time in the heart of <a href="http://en.wikipedia.org/wiki/Silicon_Valley" style="font-style: normal; ">Silicon Valley</a>. There are many firsts, so let's just list a few here (let me know if I'm missing any)!<div style="font-style: normal; "><br /></div><div>This is the first PyCon...<div style="font-style: normal; "><ul><li><span style="font-size: 100%; ">ever held in Silicon Valley (although </span><span style="font-size: 100%; ">older Python workshops have been hosted here)</span></li><li><span style="font-size: 100%; ">that had a cap; yes, we "ended" registration at 1500 people</span></li><li><span>where we ran out of swag bags; 1800 were ordered... POOF, gone by mid-Saturday</span></li><li><span>to have sold out! (even though we capped at 1500; didn't stop it from going over 2000)</span></li><li><span style="font-size: 100%; ">with an attendance near or exceeding 2200 (2257)</span></li><li><span style="font-size: 100%; ">that had to stop accepting sponsorships... at 136!!!</span></li><li><span style="font-size: 100%; ">to feature a </span><a href="https://us.pycon.org/2012/5k/" style="font-size: 100%; ">physical race</a><span style="font-size: 100%; "> (not to be confused with a </span><a href="http://en.wikipedia.org/wiki/Race_condition" style="font-size: 100%; ">race condition</a><span style="font-size: 100%; ">)</span></li></ul></div><div>Another exciting announcement is that my first 3rd edition <i>Core Python</i> book will be published and debuting at the conference!! It's called <a href="http://corepython.com/cpp3ev2" style="font-style: normal; ">Core Python Applications Programming</a> and based on the second part of the original <a href="http://corepython.com/cpp2e" style="font-style: normal; ">Core Python Programming</a> book. All of the books' individual home pages are now unified at <a href="http://corepython.com/" style="font-style: normal; ">corepython.com</a>. The books also have a <a href="http://plus.ly/corepython" style="font-style: normal; ">shared Google+ page</a> for you to encircle! They're literally "hot off the presses" as they were overnighted by the printer to the publisher's hotel and brought by hand to the conference! (Amazon's not shipping them for another 10 days after that!)</div><div style="font-style: normal; "><br /></div><div style="font-style: normal; ">The new book features upgrades and new stuff added to existing chapters as well as brand new chapters on <a href="http://djangoproject.com/">Django</a>, <a href="http://code.google.com/appengine">Google App Engine</a>, and text processing with <a href="http://docs.python.org/library/csv">CSV</a>, <a href="http://docs.python.org/library/json">JSON</a>, and <a href="http://docs.python.org/library/markup">XML</a>. There is even new material on <a href="http://twitter.com/">Twitter</a> and <a href="http://plus.google.com/">Google+</a> in case you're feeling more social than when the previous edition was published. Those of you asking for that PowerPoint slideshow generator for the past N years, or perhaps an intro to NoSQL/<a href="http://mongodb.org/">MongoDB</a>? Yep, they're in there too! Finally, I've added not only Python 3 equivalents to many of the code samples, but I also cover some best practices when porting from 2.x to 3.x.</div><div style="font-style: normal; "><br /></div><div style="font-style: normal; ">With all of the updates and new material, I'm hoping that this will be one of the most popular places for intermediate Python programmers to go once they've gotten comfortable with the langauge but want to apply their skills to a variety of topics in Python development today. While the coverage doesn't necessarily go particularly deep, the goal is to give programmers a kickstart with a comprehensive introduction.</div><div style="font-style: normal; "><br /></div><div style="font-style: normal; ">To help kickoff the new book, I got to thinking about Python books in general, especially the numerous times that people have either asked me or asked in some online forum: "What's a good Python book?" Unlike Python, there's not one right answer for this question, so as part of this exploration, I came up with 3 different book lists for diverse audiences of readers out there. You can find that article at <a href="http://www.informit.com/articles/article.aspx?p=1849069">InformIT</a>.</div><div style="font-style: normal; "><br /></div><div style="font-style: normal; ">In the meantime, it's back to the drawing board for me as I prepare to work on the 3rd edition of the main part of Core Python. If you've got ideas or suggestions on updating part 1 or wish to participate in the review process, please contact me now! (<a href="http://twitter.com/wescpy">@wescpy</a>/<a href="http://plus.ly/wescpy">+wescpy</a>)</div><div style="font-style: normal; "><br /></div><div style="font-style: normal; ">ps. For those interested in brushing up on your Python skills, I'll be offering my popular Intro+Intermediate course this summer near the San Francisco airport. Go to <a href="http://cyberwebconsulting.com/">cyberwebconsulting.com</a> for more information!</div></div>wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com2tag:blogger.com,1999:blog-6940043312015460811.post-74143960022771049602011-12-06T01:00:00.000-08:002011-12-12T00:58:07.276-08:00Writing 2.x & 3.x Compatible Code<div><div><div><div style="font-family: 'Times New Roman'; text-align: -webkit-auto; background-color: rgb(255, 255, 255); font-size: medium; "><p class="HC" style="text-align: left; text-indent: 0pt; margin-top: 24pt; margin-bottom: 10pt; margin-right: 48pt; margin-left: 0pt; font-size: 18pt; font-weight: bold; text-decoration: none; vertical-align: baseline; font-family: GillSans; "><span class="Apple-style-span" style="font-family: 'New Caledonia'; font-size: 15px; font-weight: normal; "></span></p><blockquote></blockquote><blockquote></blockquote><blockquote></blockquote></div><div style="text-align: -webkit-auto; background-color: rgb(255, 255, 255); "><span class="Apple-style-span" style="font-family: 'New Caledonia'; font-size: 15px; ">While we're at the crossroads transitioning from Python 2 to 3, you may wonder whether it is possible to write code runs without modification under both Python 2 and 3. It seems like a reasonable request, but how would you get started? What breaks the most Python 2 code when executed by a 3.x interpreter?</span></div><div style="text-align: -webkit-auto; background-color: rgb(255, 255, 255); "><p style="font-family: 'Times New Roman'; font-size: medium; "></p></div><div style="text-align: -webkit-auto; background-color: rgb(255, 255, 255); "><h1 class="HD" style="text-align: left; text-indent: 0pt; margin-top: 13pt; margin-bottom: 9pt; margin-right: 0pt; margin-left: 12pt; font-size: 16pt; font-style: italic; text-decoration: none; vertical-align: baseline; font-family: GillSans; "><a name="0_pgfId-1006112"></a>print vs. print()</h1><p class="FT" style="text-align: justify; text-indent: 0pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1005829"></a>If you think like me, you'd say the print statement. That's as good a place to start as any, so let's give it a shot. The tricky part is that in 2.x, it's a statement, thus a keyword or reserved word while in 3.x, it's just a BIF. In other words, because language syntax is involved, you cannot use if statements, and no, Python still doesn't have #ifdef macros!</p><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1005978"></a>Let's try just putting parentheses around the arguments to print:</p><p class="CDT1" style="text-align: left; text-indent: -36pt; margin-top: 5.5pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005981"></a>>>> print('Hello World!')</p><div style="font-size: medium; font-family: 'Times New Roman'; "><h6 class="CDTX" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 5.5pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><span class="Apple-style-span" style="font-weight: normal;"><a name="0_pgfId-1005979"></a>Hello World!</span></h6><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1005985"></a>Cool! That works under both Python 2 and Python 3! Are we done? Sorry.</p><p class="CDT1" style="text-align: left; text-indent: -36pt; margin-top: 5.5pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005988"></a>>>> <b>print</b>(10, 20) # Python 2</p></div><div style="font-size: medium; font-family: 'Times New Roman'; "><h6 class="CDTX" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 5.5pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><span class="Apple-style-span" style="font-weight: normal;"><a name="0_pgfId-1005986"></a>(10, 20)</span></h6><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1005998"></a>You're not going to be as lucky this time as the former is a tuple while in Python 3, you're passing in multiple arguments to print():</p><p class="CDT1" style="text-align: left; text-indent: -36pt; margin-top: 5.5pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005994"></a>>>> print(10, 20) # Python 3</p></div><div style="font-size: medium; font-family: 'Times New Roman'; "><h6 class="CDTX" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 5.5pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><span class="Apple-style-span" style="font-weight: normal;"><a name="0_pgfId-1005992"></a>10 20</span></h6><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1006100"></a>If you think a bit more, perhaps we can check if print is a keyword. You may recall there is a keyword module which contains a list of keywords. Since print won't be a keyword in 3.x, you may think that it can be as simple as this:</p></div><div><blockquote style="font-size: medium; "><p style="font-family: 'Times New Roman'; "></p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "></p></blockquote><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005772"></a>>>> <b>import </b>keyword</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; ">>>> 'print' <b>in </b>keyword.kwlist</p></div><div style="font-size: medium; font-family: 'Times New Roman'; "><h6 class="CDTX" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 5.5pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005773"></a>False</h6><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "></p></div><div style="font-size: medium; font-family: 'Times New Roman'; "><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1005779"></a>As a smart programmer, you'd probably try it in 2.x expecting a True response. Although you would be correct, you'd still fail for a different reason:</p><p class="CDT1" style="text-align: left; text-indent: -36pt; margin-top: 5.5pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005714"></a>>>> <b>import </b>keyword</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005786"></a>>>> <b>if </b>'print' <b>in </b>keyword.kwlist:</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005787"></a>... <b>from </b>__future__ <b>import </b>print_function</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005788"></a>...</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005789"></a>File "<stdin>", line 2</stdin></p></div><div style="font-size: medium; font-family: 'Times New Roman'; "><h6 class="CDTX" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 5.5pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><span class="Apple-style-span" style="font-weight: normal;"><a name="0_pgfId-1005790"></a>SyntaxError: from __future__ imports must occur at the beginning of the file</span></h6><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1005727"></a>One solution which works requires you to use a function that has similar capabilities as print. One of them is sys.stdout.write() while another is distutils.log.warn(). For whatever reason, we decided to use the latter in many of this book's chapters. I suppose sys.stderr.write() will also work, if unbuffered output is your thing.</p><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1005887"></a>The "Hello World!" example would then look like this:</p><p class="CDT1" style="text-align: left; text-indent: -36pt; margin-top: 5.5pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005888"></a># Python 2.x</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005889"></a><b>print </b>'Hello World!'</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005890"></a># Python 3.x</p></div><div style="font-size: medium; font-family: 'Times New Roman'; "><h6 class="CDTX" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 5.5pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><span class="Apple-style-span" style="font-weight: normal;"><a name="0_pgfId-1005891"></a>print('Hello World!')</span></h6><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1005892"></a>The following line would work in both versions:</p><p class="CDT1" style="text-align: left; text-indent: -36pt; margin-top: 5.5pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005899"></a># Python 2.x & 3.x compatible</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005893"></a><b>from </b>distutils.log <b>import </b>warn <b>as </b>printf</p></div><div style="font-size: medium; font-family: 'Times New Roman'; "><h6 class="CDTX" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 5.5pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><span class="Apple-style-span" style="font-weight: normal;"><a name="0_pgfId-1005894"></a>printf('Hello World!')</span></h6><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1005895"></a>That reminds me of why we didn't use sys.stdout.write()... we would need to add a NEWLINE character at the end of the string to match the behavior:</p><p class="CDT1" style="text-align: left; text-indent: -36pt; margin-top: 5.5pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005896"></a># Python 2.x & 3.x compatible</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1005897"></a><b>import </b>sys</p></div><div style="font-size: medium; font-family: 'Times New Roman'; "><h6 class="CDTX" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 5.5pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><span class="Apple-style-span" style="font-weight: normal;"><a name="0_pgfId-1005898"></a>sys.stdout.write('Hello World!\n')</span></h6><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1005824"></a>The one real problem isn't this little minor annoyance, but that these functions are no true proxy for print or print() for that matter... they only work when you've come up with a single string representing your output. Anything more complex requires you to put in more effort.</p></div></div><div style="font-family: 'Times New Roman'; text-align: -webkit-auto; background-color: rgb(255, 255, 255); font-size: medium; "><h1 class="HD" style="text-align: left; text-indent: 0pt; margin-top: 13pt; margin-bottom: 9pt; margin-right: 0pt; margin-left: 12pt; font-size: 16pt; font-style: italic; text-decoration: none; vertical-align: baseline; font-family: GillSans; "><a name="0_pgfId-1005947"></a>Import your way to a solution</h1><p class="FT" style="text-align: justify; text-indent: 0pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1005948"></a>In other situations, life is a bit easier, and you can just import the correct solution. In the code below, we want to import the urlopen() function. In Python 2, it lives in urllib and urllib2 (we'll use the latter), and in Python 3, it's been integrated into urllib.request. Your solution which works for both 2.x and 3.x is neat and simple in this case:</p><p class="CDT1" style="text-align: left; text-indent: -36pt; margin-top: 5.5pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006127"></a><b>try</b>:</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006128"></a><b> from </b>urllib2 <b>import </b>urlopen</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006129"></a><b>except </b>ImportError:</p><div><h6 class="CDTX" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 5.5pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><span class="Apple-style-span" style="font-weight: normal;"><a name="0_pgfId-1006130"></a></span> from <span class="Apple-style-span" style="font-weight: normal;">urllib.request </span>import <span class="Apple-style-span" style="font-weight: normal;">urlopen</span></h6><p class="FT" style="text-align: justify; text-indent: 0pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1006125"></a>For memory conservation, perhaps you're interested in the iterator (Python 3) version of a well-known built-in like zip(). In Python 2, the iterator version is itertools.izip(). This function is renamed as and replaces zip() in Python 3, and if you insist on this iterator version, your import statement is also fairly straightforward:</p><p class="CDT1" style="text-align: left; text-indent: -36pt; margin-top: 5.5pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006162"></a><b>try</b>:</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006163"></a><b> from </b>itertools <b>import </b>izip <b>as </b>zip</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006164"></a><b>except </b>ImportError:</p></div><div><h6 class="CDTX" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 5.5pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006204"></a> pass</h6><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1006205"></a>One example which isn't as elegant looking is the StringIO class. In Python 2, the pure Python version is in the StringIO module, meaning you access it via StringIO.StringIO. There is also a C version for speed, and that's located at cStringIO.StringIO. Depending on your Python installation, you may prefer cStringIO first and fallback to StringIO if cStringIO is not available.</p><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1006212"></a>In Python 3, Unicode is the default string type, but if you're doing any kind of networking, it's likely you'll have to manipulate ASCII/bytes strings instead, so instead of StringIO, you'd want io.BytesIO. In order to get what you want, the import is slightly uglier:</p><p class="CDT1" style="text-align: left; text-indent: -36pt; margin-top: 5.5pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006215"></a><b>try</b>:</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006216"></a><b> from </b>io <b>import </b>BytesIO <b>as </b>StringIO</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006217"></a><b>except </b>ImportError:</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006218"></a><b> try</b>:</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006219"></a><b> from </b>cStringIO <b>import </b>StringIO</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006220"></a><b> except </b>ImportError:</p></div><div><h6 class="CDTX" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 5.5pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006221"></a> from <span class="Apple-style-span" style="font-weight: normal;">StringIO </span>import <span class="Apple-style-span" style="font-weight: normal;">StringIO</span></h6></div></div><div style="font-family: 'Times New Roman'; text-align: -webkit-auto; background-color: rgb(255, 255, 255); font-size: medium; "><h1 class="HD" style="text-align: left; text-indent: 0pt; margin-top: 13pt; margin-bottom: 9pt; margin-right: 0pt; margin-left: 12pt; font-size: 16pt; font-style: italic; text-decoration: none; vertical-align: baseline; font-family: GillSans; "><a name="0_pgfId-1006213"></a>Putting it all together</h1><p class="FT" style="text-align: justify; text-indent: 0pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1006459"></a>If you're lucky, these are all the changes you have to make, and the rest of your code is simpler than the setup at the beginning. If you install the imports above of distutils.log.warn() [as printf()], url*.urlopen(), *.StringIO, and a normal import of xml.etree.ElementTree (2.5 and newer), you can write a very short parser to display the top headline stories from the Google News service with just these roughly eight lines of code:</p><p class="CDT1" style="text-align: left; text-indent: -36pt; margin-top: 5.5pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006365"></a>g = urlopen('http://news.google.com/news?topic=h&output=rss')</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006366"></a>f = StringIO(g.read())</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006367"></a>g.close()</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006368"></a>tree = xml.etree.ElementTree.parse(f)</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006369"></a>f.close()</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006371"></a><b>for </b>elmt <b>in </b>tree.getiterator():</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006372"></a><b> if </b>elmt.tag == 'title' <b>and not</b> \</p><p class="CDT" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 6pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><a name="0_pgfId-1006423"></a> elmt.text.startswith('Top Stories'):</p><div><h6 class="CDTX" style="text-align: left; text-indent: -36pt; margin-top: 0pt; margin-bottom: 5.5pt; margin-right: 0pt; margin-left: 60pt; font-size: 9.5pt; text-decoration: none; vertical-align: baseline; font-family: Courier; "><span class="Apple-style-span" style="font-weight: normal;"><a name="0_pgfId-1006373"></a> printf('- %s' % elmt.text)</span></h6><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1006362"></a>This script runs exactly the same under 2.x and 3.x with no changes to the code whatsoever. Of course, if you're using 2.4 and older, you need to download ElementTree separately.</p><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1006451"></a>The code snippets in this subsection come from the "Text Processing" chapter of the book, so take a look at the goognewsrss.py file to see the full version in action.</p><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1006206"></a>Some will feel that these changes really start to mess up the elegance of your Python source. After all, readbility counts! If you prefer to keep your code cleaner yet still write code that runs under both versions without changes, take a look at the six package.</p><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1006336"></a>six is a compatibility library who's primary role is to provide an interface to keep your application code the same while hiding the complexities described in this appendix subsection from the developer. To find out more about six, read this: http://packages.python.org/six</p><p class="IT" style="text-align: justify; text-indent: 12pt; margin-top: 0pt; margin-bottom: 0pt; margin-right: 0pt; margin-left: 0pt; font-size: 11pt; text-decoration: none; vertical-align: baseline; font-family: 'New Caledonia'; "><a name="0_pgfId-1006452"></a>Regardless whether you use a library like six or choose to roll your own, we hoped to show in this short narrative that it is possible to write code that runs under both 2.x & 3.x. The bottom line is that you may have to sacrifice some of the elegance and simplicity of Python, trading it off for true 2 to 3 portability. I'm sure we'll be revisiting this issue for the next few years until the whole world has completed the transition to the next generation.</p></div></div></div></div></div>wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com0tag:blogger.com,1999:blog-6940043312015460811.post-83040171485197413352011-09-29T12:05:00.000-07:002011-10-04T22:54:08.058-07:00Learning Python<span class="Apple-style-span"><b>Finally here!!</b> Welcome to my new blog dedicated specifically to Python programming. My <a href="http://wesc.livejournal.com/">old blog at LiveJournal</a> is getting a little long in the tooth, plus their spam filtering was lacking... something. However, I'll leave that blogsite up for personal posts, but I'm moving here for Python stuff. In fact, I think I'll "refactor" some of those old posts here from time-to-time.</span><div><span class="Apple-style-span"><br /></span></div><div><span class="Apple-style-span">As you can see, I've named this blog (currently) the same name as my book. This was not done intentionally as some marketing effort to promte the book as I do want to focus on having readers/users understand the core elements of the language. This in turn makes for better Python programmers, thus lowers the stress level in the world a little. Now let's really start the contents of this post, and that means going back to the beginning and learning Python:</span></div><div><span class="Apple-style-span"><br /></span></div><div><span class="Apple-style-span" style="line-height: 16px; background-color: rgb(255, 255, 255); "><span class="Apple-style-span">If you don't know Python but already code, try the <a href="http://code.google.com/edu/languages/google-python-class/">Google Python course</a> first. It is basically the internal 2-day training course scrubbed and externalized for all of you. It jumps in fairly quickly without a lot of explanation. If you learn best in this style, you'll be okay. There are also 7 videos available on the site so you can follow the lectures from both days.</span></span></div><div><span class="Apple-style-span" style="line-height: 16px; background-color: rgb(255, 255, 255); "><span class="Apple-style-span"><br /></span></span></div><div><span class="Apple-style-span" style="line-height: 16px; background-color: rgb(255, 255, 255); "><span class="Apple-style-span">If you're really new to programming, consider taking a beginner course in programming. Sure Python is a great first language to learn coding with, but not all such courses feature it. If you have no time for courses, do the <a href="http://docs.python.org/tutorial" style="color: rgb(0, 102, 204); text-decoration: underline; cursor: pointer; ">online Python tutorial</a> as well as the <a href="http://singpath.com/" style="color: rgb(0, 102, 204); text-decoration: underline; cursor: pointer; ">SingPath</a> exercises. As far as books go, online-wise you can try <i style="color: rgb(0, 102, 204); text-decoration: underline; cursor: pointer; "><a href="http://learnpythonthehardway.org/" style="color: rgb(0, 102, 204); text-decoration: underline; cursor: pointer; ">Learn Python the Hard Way</a></i> (book+lessons) or <a href="http://diveintopython.org/" style="color: rgb(0, 102, 204); text-decoration: underline; cursor: pointer; "><i>Dive Into Python</i></a> (book-only).</span></span></div><div><span class="Apple-style-span" style="line-height: 16px; background-color: rgb(255, 255, 255); "><span class="Apple-style-span"><br /></span></span></div><div><span class="Apple-style-span" style="line-height: 16px; background-color: rgb(255, 255, 255); "><span class="Apple-style-span">Since <i>Dive Into Python</i> is written by a <a href="http://diveintomark.org/">co-worker of mine</a>, you can buy a dead-tree version if you wish to support them, or <a href="http://corepython.com/" style="color: rgb(0, 102, 204); text-decoration: underline; cursor: pointer; "><i>Core Python Programming</i></a>, to help out someone else you know. :-) The primary difference between these books is that one is a quick dive while the other is a deep dive, as so <a href="http://amazon.com/product-reviews/0132269937?sortBy=bySubmissionDateDescending#RKG44D8GQYLNL" style="color: rgb(0, 102, 204); text-decoration: underline; cursor: pointer; ">well described in this Amazon review.</a> Here's <a href="http://t.co/ip67ljJO">another more recent review</a> although it does not shed as much positive light on my colleague's tome. </span></span><span class="Apple-style-span" style="line-height: 16px; background-color: rgb(255, 255, 255); ">As far as references go, you can support yet <span class="Apple-style-span"><span class="Apple-style-span" style="cursor: pointer;"><u><a href="http://aleax.it/">a third Googler</a></u></span></span> by buying <i>Python in a Nutshell</i>, or a non-colleague who wrote <a href="http://dabeaz.com/per.html" style="color: rgb(0, 102, 204); text-decoration: underline; cursor: pointer; "><i>Python Essential Reference</i></a>.</span></div><div><span class="Apple-style-span" style="line-height: 16px; background-color: rgb(255, 255, 255); "><br /></span></div><div><span class="Apple-style-span" style="line-height: 16px; background-color: rgb(255, 255, 255); ">If you have children or wish to teach kids how to program, <a href="http://cp4k.blogspot.com/"><i>Hello World! Computer Programming for Kids and Other Beginners</i></a> is a friendly and well-received book written by an engineer and his (then) 8-year old son. It's good to have a kid's perspective as kids (generally) respect other kids who are/were in their same shoes.</span></div><div><span class="Apple-style-span" style="line-height: 16px; background-color: rgb(255, 255, 255); "><br /></span></div><div><span class="Apple-style-span" style="line-height: 16px; background-color: rgb(255, 255, 255); ">The cool thing is that the sky is the limit once you've learned some Python. You can take off in any different direction like <a href="http://code.google.com/appengine">Google App Engine</a>, <a href="http://pylonsproject.org/">Pyramid</a>, or <a href="http://djangoproject.com/">Django</a> for web or mobile development, <a href="http://scipy.org/">SciPy/NumPy</a> for scientific development, <a href="http://sqlalchemy.org/">SQLAlchemy</a>/<a href="http://sqlobject.org/">SQLObject</a> for database ORMs, <a href="http://jython.org/">Jython</a> for Java development, <a href="http://pywin32.sf.net/">Win32</a> for PC development, <a href="http://pygame.org/">PyGame</a> for writing games, etc. Testing is something you should always keep in mind, so look into <a href="http://readthedocs.org/docs/nose">Nose</a> or <a href="http://pytest.org/">py.test</a>.</span></div><div><span class="Apple-style-span" style="line-height: 16px; background-color: rgb(255, 255, 255); "><br /></span></div><div><span class="Apple-style-span" style="line-height: 16px; background-color: rgb(255, 255, 255); ">For those of you who already know programming, but want to learn Python as quickly and as in-depth as possible in the shortest amount of time, join me near San Francisco for my <a href="http://sfbay.craigslist.org/sfc/cls/2495963854.html">upcoming 3-day Python training course</a> running mid-week October 18-20! You need to be proficient programming in another high-level programming language (such as C/C++, Java, PHP, Ruby, etc.). The fee covers for all lectures, labs (3 per day), and everyone gets a copy of my bestseller, <a href="http://corepython.com/"><i>Core Python Programming</i></a>. There is a significant discount to primary/secondary school teachers so ask about that if applicable!</span></div><div><span class="Apple-style-span" style="line-height: 16px;"><br /></span></div><div><span class="Apple-style-span" style="line-height: 16px; background-color: rgb(255, 255, 255); ">I hope this helps some of you get started! We always welcome new users to the <a href="http://python.org/community">Python community</a>!!</span></div>wescpyhttp://www.blogger.com/profile/08896306361304265422noreply@blogger.com0