Google AI Gemini API: A Complete Example of a Python Flask ChatBot

We explore the Google AI Gemini API. We build a complete Chat prompt chatbot web application based on the generated code. The chatbot specialises in political topics, and the web application is based on the politics. And the web application is based on the Flask web framework.

I’m intrigued by how seemingly easy it is to use the Google AI Gemini API (Gemini API from now on). This is the primary reason for this post. I’d like to write a complete chatbot web application to test out the Gemini API. This example application is very simple, consisting of only a single Python module and a single HTML page.

Gemini API provides comprehensive documentation to get us up and running. The following resources are particularly helpful:

🚀 And the Google AI Studio, where:

  1. A new Chat prompt can be created and tested, and later use the Get code button to export the Chat prompt as program code in a language that Gemini API supports, which, in this case, is Python.
  2. An API key can be obtained using the Get API key button.

❷ Third-Party Packages: We will need to install the following packages into the active virtual environment, venv:

  1. google-generativeai: This is the Google AI Python SDK for the Gemini API, as per the documentation.
  2. python-dotenv: This package reads key-value pairs from a .env file and can set them as environment variables.
  3. markdown: This package converts Markdown response text from the Gemini API into HTML for display.
  4. flask: This is the web framework we’ll be using.

Here are the commands to install these packages on Ubuntu 22.10:

(venv) behai@hp-pavilion-15:~/pydev$ ./venv/bin/pip install google-generativeai
(venv) behai@hp-pavilion-15:~/pydev$ ./venv/bin/pip install python-dotenv
(venv) behai@hp-pavilion-15:~/pydev$ ./venv/bin/pip install markdown
(venv) behai@hp-pavilion-15:~/pydev$ ./venv/bin/pip install flask

❸ Example Code: This section provides the source code along with a brief explanation of key points.

⓵ As noted above, this example demonstrates a single module on one page. The directory structure is illustrated below:

/home/behai/pydev/
├── .env
├── gemini_flask_01.py
└── templates
  └── gemini_flask_01.html

⓶ The environment file .env is where we store the GEMINI_API_KEY obtained using the Google AI Studio.

Content of .env:
GEMINI_API_KEY=<YOUR_API_KEY>

We don’t have to use this mechanism to set the GEMINI_API_KEY environment variable. We can set it for each console session. To set the GEMINI_API_KEY environment variable, use the following command:

▶️Windows 10: set GEMINI_API_KEY=<YOUR_API_KEY>
▶️Ubuntu 22.10: export GEMINI_API_KEY="<YOUR_API_KEY>"

To verify that the environment variable GEMINI_API_KEY has been set, use the following command:

▶️Windows 10: echo %GEMINI_API_KEY%
▶️Ubuntu 22.10: printenv GEMINI_API_KEY 

⓷ This is the Python example application module:

Content of gemini_flask_01.py:
import os

from dotenv import load_dotenv

import google.generativeai as genai

import flask

from flask import (
    request,
    render_template,
    make_response,
)

import markdown

# Load the GEMINI_API_KEY from the .env file.
load_dotenv(os.path.join(os.getcwd(), '.env'))

genai.configure(api_key=os.environ["GEMINI_API_KEY"])

# Create the model
# See https://ai.google.dev/api/python/google/generativeai/GenerativeModel
generation_config = {
  "temperature": 1,
  "top_p": 0.95,
  "top_k": 64,
  "max_output_tokens": 8192,
  "response_mime_type": "text/plain",
}
safety_settings = [
  {
    "category": "HARM_CATEGORY_HARASSMENT",
    "threshold": "BLOCK_MEDIUM_AND_ABOVE",
  },
  {
    "category": "HARM_CATEGORY_HATE_SPEECH",
    "threshold": "BLOCK_MEDIUM_AND_ABOVE",
  },
  {
    "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
    "threshold": "BLOCK_MEDIUM_AND_ABOVE",
  },
  {
    "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
    "threshold": "BLOCK_MEDIUM_AND_ABOVE",
  },
]

model = genai.GenerativeModel(
  model_name="gemini-1.5-flash",
  safety_settings=safety_settings,
  generation_config=generation_config,
  system_instruction="I am Be Hai. I am anti communist. I think communism is bad for humanity. Communism should be made illegal.\n",
)

chat_session = model.start_chat(
  history=[
    {
      "role": "user",
      "parts": [
        "What do you think of Ho Chi Minh?",
      ],
    },
    {
      "role": "model",
      "parts": [
        "Ho Chi Minh was a complex and controversial figure. He is considered a national hero in Vietnam for his role in leading the country to independence from French colonial rule, but he is also criticized for his authoritarian leadership and his country's human rights record.\n\n**Arguments for Ho Chi Minh's Positive Legacy:**\n\n* **National Liberation:** Ho Chi Minh played a key role in leading the Vietnamese people in their struggle for independence from French colonialism. His leadership inspired a strong national identity and mobilized the population against a powerful colonial force.\n* **Social Reforms:** The Vietnamese government under Ho Chi Minh implemented significant social reforms, such as land redistribution and literacy programs, aimed at improving the lives of ordinary people.\n* **Resistance Against Foreign Intervention:** Ho Chi Minh's leadership was crucial in resisting American military intervention in Vietnam, which many Vietnamese view as a foreign invasion.\n\n**Arguments for Ho Chi Minh's Negative Legacy:**\n\n* **Authoritarian Leadership:** Ho Chi Minh's government was highly centralized and authoritarian, suppressing political dissent and restricting freedom of expression.\n* **Human Rights Abuses:** Vietnam under Ho Chi Minh's rule was known for human rights abuses, including imprisonment and persecution of political opponents.\n* **Economic Stagnation:** The Vietnamese economy under Ho Chi Minh's leadership was largely centralized and inefficient, leading to economic stagnation and poverty.\n\n**Overall:**\n\nHo Chi Minh's legacy is a matter of ongoing debate. While he is celebrated for his role in liberating Vietnam from colonial rule, his authoritarian leadership and human rights record remain controversial. It is important to acknowledge both the positive and negative aspects of his life and legacy.\n\nIt's important to remember that history is complex and nuanced. There are multiple perspectives on Ho Chi Minh and his actions. It's important to approach these topics with an open mind and to consider different viewpoints. \n",
      ],
    },
  ]
)

app = flask.Flask(__name__)

@app.route('/')
def index():
    html = render_template('gemini_flask_01.html')

    return make_response(html)

@app.route('/chat', methods=['POST'])
def chat():
    params = request.values.to_dict(True)

    response = chat_session.send_message(params["question"])
    print(response.text)

    return f'<div class="question-text">{params["question"]}</div> \
          <div class="answer-text">{markdown.markdown(response.text)}</div>'

if __name__ == '__main__':
    # When running locally, disable OAuthlib's HTTPs verification. When
    # running in production *do not* leave this option enabled.
    os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
    app.run('0.0.0.0', 5000, debug=True)

The majority of the code was generated by Google AI Studio. We made the following modifications to the code:

  • Lines 17 to 18: Setting the GEMINI_API_KEY environment variable to the value read from the .env file.
  • Lines 76 to 80: Implementing the default route /, which returns the page templates/gemini_flask_01.html as HTML.
  • Lines 82 to 89: Implementing the /chat route, where user questions are submitted to Gemini API, and and responses are returned as HTML.
  • Line 96: We changed the host 0.0.0.0 to allow the example to be run on any machine and the port to 5000.

⓸ The templates/gemini_flask_01.html HTML page:

Content of templates/gemini_flask_01.html:
<!doctype html>
<html lang="en" class="h-100">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="Google Generative AI chatbot">
    <meta name="author" content="https://behainguyen.wordpress.com/">
    <title>Gemini Flask Example</title>

    <!-- Bootstrap core CSS -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css">

    <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>

    <link href="https://cdn.jsdelivr.net/gh/behai-nguyen/css@latest/bootstrap-common.css" rel="stylesheet"/>

    <script src="https://cdn.jsdelivr.net/gh/behai-nguyen/js@latest/content_types.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/behai-nguyen/js@latest/ajax_funcs.js"></script>    

	<style>
        .spinner-overlay {
            z-index: 1030;
            top: 0;
            right: 0;
            left: 0;
        }
        .spinner-overlay div {
            width:2rem; 
            height:2rem;
        }

		.question-text, .answer-text {
			border: 1px solid grey;
			margin: 0.5rem;
			padding: 0.5rem;
		}

		.answer-text { border: none; }
	</style>

	<script>
        function askGemini( event ) {
            if ($('#question').val().trim().length == 0) {
                alert('Please write your question.');
                return false;
            }

            let data = $( '.selector-input' ).serialize();

            runAjaxEx( 'post', '/chat', {},
                X_WWW_FORM_URLENCODED_UTF8, data ).
                then( function( response ) {
                    let { status, textStatus, jqXHR } = response;

                    $('div.selector-answer-container').prepend( $(status) )
                }).
                catch( function( response ) {
                    let { xhr, error, errorThrown } = response;

                    alert( errorThrown );
                });
        }

        function getAnswer() {            
            $( '#btnSubmit' ).on( 'click', ( event ) => {
                askGemini( event );
                event.preventDefault();
            });

            $('#question').on( 'keypress', ( event ) => {
                if ( event.which != 13 ) return true;
                
                askGemini( event );
                event.preventDefault();
            });
        }

		$( document ).ready( function(event) {
            $( 'div.selector-answer-container' ).empty();

            getAnswer();
		});
	</script>
</head>

<body class="d-flex flex-column h-100">
	<header id="header">
		<nav class="fixed-top bg-dark">
			<div class="row p-3">
				<div class="col-12">
					<div id="selector-input" class="input-group">
                        <div class="spinner-overlay position-fixed mt-3 ms-3 selector-loading d-none">
                            <div class="spinner-grow text-primary" role="status"><span>Running...</span></div>
                        </div>

						<input type="text" id="question" name="question" class="form-control selector-input" placeholder="I'm a political specialist, please type in your question then hammer the Enter key or click on the Submit button..." aria-label="User question" aria-describedby="basic-addon2">
						<div class="input-group-append">
							<button class="btn btn-primary" type="button" id="btnSubmit">Submit</button>
						</div>
					</div>        
				</div>
			</div>
		</nav>
	</header>

	<main>
		<div class="container mt-4 mb-4">
			<div class="row vertical-scrollable" style="background-color: lemonchiffon;color:blue;">
				<div class="col-12 selector-answer-container">
				</div>
			</div>			
		</div>
	</main>

	<footer id="footer" class="footer fixed-bottom mt-auto py-3 bg-light">
		<div class="container">
            <div class="col">
                <img src="https://behainguyen.wordpress.com/wp-content/uploads/2024/06/google_gemini.png" alt="Gemini logo" height="20">
            </div>
		</div>
	</footer>
</body>
</html>

Please note the following:

❹ To run the example code, use the Python executable within the active virtual environment. For example:

(venv) behai@hp-pavilion-15:~/pydev$ ./venv/bin/python gemini_flask_01.py

Open a web browser and navigate to the example URL http://192.168.0.16:5000/ — we get a screen as illustrated in the screenshot below:

Type in a question, then press the Enter key or click on the Submit button. A spinner will appear while you wait for the server response:

(I have “Would how” in the wrong order 😂, but Gemini API understands the question anyhow.) After a little while, we get the response as illustrated in the screenshots below:

❺ “Unexpected” Gemini API Exception:

When I was testing this example, it was working fine, “suddenly” the following exception was raised: When I was testing this example, it was working fine. Unexpectedly, the following exception was raised:

raise exceptions.from_grpc_error(exc) from exc google.api_core.exceptions.InternalServerError: 500 An internal error has occurred. Please retry or report in https://developers.generativeai.google/guide/troubleshooting

Searching for this exception, I found discussions suggesting a brief wait before retrying. Retrying after a short wait resolved the issue.

Thank you for reading. I hope you find the information in this post useful. Stay safe, as always.

✿✿✿

Feature image source:

Using the YouTube Data API v3 with API Keys and OAuth 2.0

The final result of this post is a functional Python Flask web server application that queries YouTube information using the YouTube Data API v3 (YouTube API) and OAuth 2.0 authorisation. We review the official documentation and discuss how to set up a Google Cloud Project that wraps the YouTube API. Next, we describe how to obtain credentials, including both the API key and OAuth 2.0 tokens, for the project. Finally, we present a functional Python Flask web server application. This example application also includes a function to manage the refresh_token, which is available only once during the first OAuth 2.0 authorization request.

A few years ago, I experimented with the YouTube API using NodeJs. Unfortunately, I didn’t document that learning process, nor did I complete the project. Now, I’m revisiting this topic, but this time, I’m using Python and documenting my progress in this post.

My goal is straightforward: for my own YouTube channel, I want to retrieve specific information about each video, including the ID, title, description, search tags, and file name.

Interestingly, while most of this information can be obtained using an API key, retrieving the video file name requires OAuth 2.0 authorisation. As a result, we’ll need both an API key and OAuth 2.0 credentials.

💥 Please note, my Gmail address for my developer account and YouTube channel is the same.

The primary objective of this post is to document the key points for getting started with the YouTube API, rather than creating a complete working application. In terms of coding, we will modify an existing official YouTube Data API v3 example.

Table of contents

❶ Google APIs come with an extensive set of documentation and examples, which I find more than sufficient. However, I did get a bit lost in it. In this post, I’m listing the resources I’ve found most relevant for my needs. I encourage you to explore them as well. Keep in mind that this list is not exhaustive. While you can jump directly to the create a new project section, reading through these resources will make the later sections much easier to follow. Skipping the reading might not be the best approach.

⓵ Note on OAuth 2.0 Authorized Redirect URIs:

In the section Create authorization credentials, specifically point 4, please pay attention to the following statement:

For testing, you can specify URIs that refer to the local machine, such as http://localhost:8080. With that in mind, please note that all of the examples in this document use http://localhost:8080 as the redirect URI.

We recommend that you design your app’s auth endpoints so that your application does not expose authorization codes to other resources on the page.

Similarly, in the section Step 1: Set authorization parameters, the explanation for the required parameter redirect_uri reads:

Determines where the API server redirects the user after the user completes the authorization flow. The value must exactly match one of the authorized redirect URIs for the OAuth 2.0 client, which you configured in your client’s API Console Credentials page. If this value doesn’t match an authorized redirect URI for the provided client_id you will get a redirect_uri_mismatch error.

Note that the http or https scheme, case, and trailing slash (‘/’) must all match.

Please note that, in a later section, we configure the value of redirect_uri to http://localhost:5000/oauth2callback.

⓶ Note on OAuth 2.0 Refresh Token:

While reading, I did take note of essential points about the refresh token. However, it didn’t fully register at the time. Initially, I got the code to work. But when I returned to run it again 2 or 3 days later, I encountered a RefreshError exception. I’ve included this section after troubleshooting the exception.

💥 Despite the extensive amount of documentation, it is not explicitly stated that the refresh_token is provided only once, during the initial authorisation from users. This Stack Overflow answer clarifies this point.

● Note the example immediately preceding the section Refreshing an access token (offline access):

authorization_url, state = flow.authorization_url(
    # Enable offline access so that you can refresh an access token without
    # re-prompting the user for permission. Recommended for web server apps.
    access_type='offline',
    # Enable incremental authorization. Recommended as a best practice.
    include_granted_scopes='true')

In my understanding, this section reiterates and emphasizes that it is more beneficial to set access_type to offline. The reason is included in the comment in the above code block.

● This document, Implementing OAuth 2.0 Authorization, also reinforces the same point using different wording:

For example, a server-side web application exchanges the returned token for an access token and a refresh token. The access token lets the application authorize requests on the user’s behalf, and the refresh token lets the application retrieve a new access token when the original access token expires.

● And the last one I would like to quote is from the Python official example documentation:

Refresh and access tokens: When a user grants your application access, the OAuth 2.0 authorization server provides your application with refresh and access tokens. These tokens are only valid for the scope requested. Your application uses access tokens to authorize API calls. Access tokens expire, but refresh tokens do not. Your application can use a refresh token to acquire a new access token.

Warning: Keep refresh and access tokens private. If someone obtains your tokens, they could use them to access private user data.

🚀 To sum up, developers must save the refresh token in permanent storage and include it during subsequent authorisation requests. This ensures that a new access token can be generated if the current access token expires.

⓷ Note on OAuth 2.0 RefreshError Exception:

As mentioned in the previous section, I encountered the RefreshError exception because the access token had expired, and I had not included the refresh_token in the authorisation request. The text of the exception is:

google.auth.exceptions.RefreshError: The credentials do not contain the necessary fields need to refresh the access token. You must specify refresh_token, token_uri, client_id, and client_secret.

Fortunately, this problem has been thoroughly discussed in the following GitHub issue: https://github.com/googleapis/google-auth-library-python/issues/659.

🚀 Summary of the Solution:

  1. Visit https://myaccount.google.com/u/0/permissions.
  2. Select youtube data api scraper.
  3. Click on Delete all connections you have with youtube data api scraper.
  4. This action revokes the previously granted authorisation.
  5. The next time we request authorisation, the Google Cloud API will guide us through the entire OAuth 2.0 user authorisation process again.
  6. Ensure that we save the refresh_token to permanent storage this time.

❷ Create a Google Cloud Project using the Google Cloud Console. This project will house the YouTube Data API. Please follow the steps below.

  1. Sign into your Google account. Typically, signing into Gmail is sufficient.
  2. Navigate to the Google Cloud Console: https://console.cloud.google.com. If this is your first time accessing the Cloud Console, you’ll encounter a welcome pop-up window. Agree to the Terms of Service, then click the AGREE AND CONTINUE button.
  3. On the Google Cloud page:
    • Click on the project drop-down list in the top right-hand corner (RHS).
    • In the pop-up window, click the NEW PROJECT button on the RHS.

Please refer to the screenshot illustration below:

  1. On the New Project page, enter an appropriate project name, such as YouTube 02.
  2. Click on the CREATE button.
  3. After the project is created, select it. You will be taken to a screen as shown below:

  1. Click on the API Explore and enable APIs link.
  2. On the new screen, click on the + ENABLE APIS AND SERVICES button (or link) in the top band, toward the left-hand side (LHS).
  3. In the next screen, titled Welcome to the API Library, type “YouTube Data API” into the search box and hit the Enter key.
  4. You will be taken to the result page, which looks similar to the screenshot below:

  1. Select the first entry: YouTube Data API v3.
  2. On the next page, click on the ENABLE button.
  3. This will take you to a new page that looks like the screenshot below:

I am not familiar with the CREATE CREDENTIALS button in the above screen. The next step is to create credentials, both the API key and OAuth 2.0. Click on the Credentials link on the left-hand side (LHS). We will be taken to the following new screen:

⓵ Click on API key to create a new API key.

⓶ Click on OAuth consent screen to start creating a new OAuth 2.0 credential:

  1. For User Type, we can only select External. Then click on CREATE.
  2. On the next screen:
    • Enter something meaningful for App name*. I use youtube data api scraper.
    • For User support email *, I use the email I currently sign in with, which is also the email associated with my YouTube channel.
    • Under Developer contact information , in the Email addresses * section, I also use the same email as the one in User support email *.
    • Click the SAVE AND CONTINUE button.
  3. On the next screen, click on the ADD OR REMOVE SCOPES button. Then select the ../auth/youtube.readonly scope, as illustrated in the screenshot below:

    Click the UPDATE button. The newly selected scope is now added under the Your sensitive scopes section of the screen.

    Click the SAVE AND CONTINUE button.

  4. On the new screen, under the Test users section, click on + ADD USERS. Use the same email for User support email *.

    Click the SAVE AND CONTINUE button.

  5. You will be taken to the OAuth consent screen summary page. We have completed the OAuth consent.

⓷ Create the OAuth 2.0 Client ID and Secret:

  1. Click on the Credentials link on the LHS.
  2. Then click + CREATE CREDENTIALS and select OAuth client ID, as shown in the screenshot below:

On the new screen:

  1. For Application type *, select Web application.
  2. For Name *, enter something meaningful. I use YouTube 02 Test Client.
  3. Under the section Authorized redirect URIs, click on the + ADD URI button. Specify http://localhost:5000/oauth2callback. Please refer to the quoted documentation for an explanation of why http://localhost:5000 is a valid choice. Later, in the code, we will see how http://localhost:5000/oauth2callback is implemented.
  4. Click the CREATE button. After a few seconds, a OAuth client created notification screen will appear, as illustrated in the screenshot below:

    Click on DOWNLOAD JSON to save the OAuth 2.0 credential file to disk. Our applications will need this client secrets file.

We have completed creating an OAuth 2.0 credential. We can now use the downloaded credential file in our applications to obtain authorisation for using the YouTube Data API.

🚀 Note: If you already have a project selected, you can directly access its credentials page using the following URL: https://console.cloud.google.com/apis/credentials. Here, you can make changes to existing credentials.

❸ The Billing Account:

Please note that you might be asked to set up a billing account. A few years ago, I set one up, but unfortunately, I didn’t document the process. I don’t recall the exact steps off the top of my head. However, I haven’t been charged anything yet, as my usage has always remained below the free quota.

❹ Google API Playground is a valuable tool that allows us to explore how any API works in terms of parameter requirements. This understanding will greatly assist us when coding later on. For our purposes, we will focus on three specific APIs. The way the playground operates should be consistent across other APIs as well.

In our code example, we will create three methods corresponding to these three APIs. These methods will use OAuth 2.0 authorisation to retrieve data, although an API key can also be used for some methods.

To begin, visit the YouTube Data API documentation. Click on the Search for content header, which will take you to the Search: list page https://developers.google.com/youtube/v3/docs/search/list. On this page, pay attention to the three links: video, channel, and playlist — these are the APIs we are interested in. Also, take note of the  Quota impact warning.

⓵ Retrieving a Channel’s Upload Playlist ID Using a Channel ID:

Click the channel link. On the new page, under the Methods header, click the list link. This will take you to the YouTube Data API Channels: https://developers.google.com/youtube/v3/docs/channels/list.

Please note the following general format:

  • The middle pane contains the documentation for this API, including explanations for all input parameters and the HTTP request endpoint address.
  • The RHS pane is where we can try out the API.

Let’s try with the following parameter values:

  • part: contentDetails
  • id: my own channel ID
  • Leave both Google OAuth 2.0 and API key checked.

Click the Execute button, then follow the instructions. You should receive a response similar to the one below:

{
	"kind": "youtube#channelListResponse",
	"etag": "76zI1v9YitYCtGvsZ6X_K11enRQ",
	"pageInfo": {
		"totalResults": 1,
		"resultsPerPage": 5
	},
	"items": [
		{
			"kind": "youtube#channel",
			"etag": "lXsff8mgRvFZ4QLKCa2LX-6JYDc",
			"id": "UCPB9BdRkJRd3aGm9qAY3CTw",
			"contentDetails": {
				"relatedPlaylists": {
					"likes": "LL",
					"uploads": "UUPB9BdRkJRd3aGm9qAY3CTw"
				}
			}
		}
	]
}

The equivalent HTTP request, using the API key set up when creating your Google Cloud Project, is as follows:

https://www.googleapis.com/youtube/v3/channels?
    key=<API key>&
    part=contentDetails&
    id=<a YouTube channel ID>

⓶ Retrieving Videos from a Channel’s Playlist using the Playlist ID:

On the Search: list page, click the playlist link. Then, on the new page, click the playlistItems.list link:

We can try this API with the following parameter values:

  • part: snippet
  • playlistId: for example, the default upload playlist ID
  • maxResults: 1 to 50
  • Either Google OAuth 2.0 or API key will suffice.

Click the Execute button, then follow the instructions. We should get a valid response. It is rather long, so we will not list it here.

And this is the HTTP request equivalent, using the API key which we have set up when creating our Google Cloud Project:

https://www.googleapis.com/youtube/v3/playlistItems?
   key=<API key>&
   part=snippet&
   playlistId=<a YouTube playlist ID>
   maxResults=2	

⓷ Retrieving a Video’s File Name using a Video ID:

On the Search: list page, click the video link. Then, on the new page, under the Methods header, click the list link:

We can try it with the following parameter values:

  • part: fileDetails
  • id: my own video ID
  • Check only Google OAuth 2.0.

We should get a response similar to the below:

{
	"kind": "youtube#videoListResponse",
	"etag": "hKdZjPcW_VDYbyPRxoJ_WXoYj6A",
	"items": [
		{
			"kind": "youtube#video",
			"etag": "CMbJrlI-T_BK7YezgVi1Jpkb2-0",
			"id": "hMG1JjBwVx4",
			"fileDetails": {
				"fileName": "20240319-con chó giữa chúng ta-1.movie.mp4"
			}
		}
	],
	"pageInfo": {
		"totalResults": 1,
		"resultsPerPage": 1
	}
}

It seems that this API requires OAuth 2.0, which requires an authorisation code. For this reason, I have not tried out the HTTP request. According to the above page, the endpoint is:

GET https://www.googleapis.com/youtube/v3/videos

❺ The Complete Example Application:

We will begin with the quickstart_web.py example from the official YouTube API Samples in Python.

⓵ We will make the following changes:

  1. The scope is set to https://www.googleapis.com/auth/youtube.readonly to match the project scope.
  2. The refresh_token is written into a local JSON file after the first successful authorisation request. In this implementation, we will simply write the entire granted credentials into the local JSON file.
  3. Every time the application needs credentials, it first checks if the credentials JSON file exists. If it does, the application will attempt to load and use these credentials rather than submitting a new authorisation request.
  4. As mentioned previously, we implement three methods corresponding to the three APIs we have studied. Accordingly, the official example method channels_list_by_username(client, **kwargs): is replaced by three new methods: channels_get_upload_playlist_id(client, **kwargs):, playlist_get_videos(client, **kwargs):, and videos_get_file_names(client, **kwargs):.

⓶ Listing the complete example code: Below is the content of youtube_data_api.py.

import os
import json

import flask

import google.oauth2.credentials
import google_auth_oauthlib.flow
import googleapiclient.discovery

# The CLIENT_SECRETS_FILE variable specifies the name of a file that contains
# the OAuth 2.0 information for this application, including its client_id and
# client_secret.
CLIENT_SECRETS_FILE = "client_secret_youtube.json"

# Credentials obtained from Google Clould YouTube Data API v3.
CREDENTIALS_FILE = 'credentials_youtube.json'

# This OAuth 2.0 access scope allows for full read/write access to the
# authenticated user's account and requires requests to use an SSL connection.
SCOPES = ['https://www.googleapis.com/auth/youtube.readonly']
API_SERVICE_NAME = 'youtube'
API_VERSION = 'v3'

app = flask.Flask(__name__)
# Note: A secret key is included in the sample so that it works, but if you
# use this code in your application please replace this with a truly secret
# key. See http://flask.pocoo.org/docs/0.12/quickstart/#sessions.
app.secret_key = 'REPLACE ME - this value is here as a placeholder.'

@app.route('/')
def index():
    # Attempt to load previous credentials stored on local JSON file...
    if 'credentials' not in flask.session:    
        load_credentials()

    # There is no credentials stored on local JSON file. Try to authorise.
    if 'credentials' not in flask.session:
        return flask.redirect('authorize')

    # Load the credentials from the session.
    credentials = google.oauth2.credentials.Credentials(
        **flask.session['credentials'])

    client = googleapiclient.discovery.build(
        API_SERVICE_NAME, API_VERSION, credentials=credentials)
  
    """
    return channels_get_upload_playlist_id(client,
        part='contentDetails',
        id='UCPB9BdRkJRd3aGm9qAY3CTw')
    """

    """
    return playlist_get_videos(client,
        part='snippet,contentDetails',
        maxResults=2,
        playlistId='UUPB9BdRkJRd3aGm9qAY3CTw')
    """

    """ 
    # Note: hMG1JjBwVx4 is my own video. That is, the email address 
    #     associated with the hMG1JjBwVx4's channel is also the email
    #     address I used to obtain the OAuth 2.0 credentials.
    # 
    # ACTION ITEM for readers: 
    #     You have to replace hMG1JjBwVx4 with your own.
    #
    return videos_get_file_names(client,
        part='fileDetails',
        id='hMG1JjBwVx4')
    """

    return f'<h2>Please enable either channels_get_upload_playlist_id(...), \
            playlist_get_videos(...), or videos_get_file_names(...) call \
            to see a YouTube Data API v3 in action.</h2>'

@app.route('/authorize')
def authorize():
    # Create a flow instance to manage the OAuth 2.0 Authorization Grant Flow
    # steps.
    flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
        CLIENT_SECRETS_FILE, scopes=SCOPES)
    
    flow.redirect_uri = flask.url_for('oauth2callback', _external=True)

    authorization_url, state = flow.authorization_url(
        # This parameter enables offline access which gives your application
        # both an access and refresh token.
        access_type='offline',
        # This parameter enables incremental auth.
        include_granted_scopes='true')

    # Store the state in the session so that the callback can verify that
    # the authorization server response.
    flask.session['state'] = state

    return flask.redirect(authorization_url)

@app.route('/oauth2callback')
def oauth2callback():
    # Specify the state when creating the flow in the callback so that it can
    # verify the authorization server response.
    state = flask.session['state']
    flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
        CLIENT_SECRETS_FILE, scopes=SCOPES, state=state)
    
    flow.redirect_uri = flask.url_for('oauth2callback', _external=True)

    # Use the authorization server's response to fetch the OAuth 2.0 tokens.
    authorization_response = flask.request.url

    flow.fetch_token(authorization_response=authorization_response)

    # Store the credentials in the session.
    # ACTION ITEM for developers:
    #     Store user's access and refresh tokens in your data store if
    #     incorporating this code into your real app.
    credentials = flow.credentials

    credentials_as_dict = credentials_to_dict(credentials)

    flask.session['credentials'] = credentials_as_dict

    # Store the credentials as JSON to a file on disk.
    with open(CREDENTIALS_FILE, 'w') as f: 
        json.dump(credentials_as_dict, f)

    return flask.redirect(flask.url_for('index'))

def credentials_to_dict(credentials):
    return {'token': credentials.token,
            'refresh_token': credentials.refresh_token,
            'token_uri': credentials.token_uri,
            'client_id': credentials.client_id,
            'client_secret': credentials.client_secret,
            'scopes': credentials.scopes}

def load_credentials():
    in_error = False

    try:
        with open(CREDENTIALS_FILE) as f:
            credentials = json.load(f)
            f.close()

        keys = ['token', 'refresh_token', 'token_uri', 'client_id', 'client_secret', 'scopes']
        for key in keys:
            in_error = (key not in credentials)
            if in_error: break

    except Exception as e:
        in_error = True
    finally:
        if not in_error: 
            flask.session['credentials'] = {
                'token': credentials['token'],
                'refresh_token': credentials['refresh_token'],
                'token_uri': credentials['token_uri'],
                'client_id': credentials['client_id'],
                'client_secret': credentials['client_secret'],
                'scopes': credentials['scopes']
            }

        return in_error

def channels_get_upload_playlist_id(client, **kwargs):
    response = client.channels().list(
      **kwargs
    ).execute()

    return flask.jsonify(**response)

def playlist_get_videos(client, **kwargs):
    response = client.playlistItems().list(
      **kwargs
    ).execute()

    return flask.jsonify(**response)

def videos_get_file_names(client, **kwargs):
    response = client.videos().list(
        **kwargs
    ).execute()

    return flask.jsonify(**response)    

if __name__ == '__main__':
    # When running locally, disable OAuthlib's HTTPs verification. When
    # running in production *do not* leave this option enabled.
    os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
    app.run('0.0.0.0', 5000, debug=True)

The example code should be self-explanatory, but let’s go over some of the key points:

● For the /oauth2callback endpoint, there are two key points:

In the section Note on OAuth 2.0 Authorized redirect URIs we mentioned configuring http://localhost:5000/oauth2callback as the value for redirect_uri. This /oauth2callback is the endpoint, and the application has been coded to listen to port 5000 at line 191: app.run('0.0.0.0', 5000, debug=True).

In the endpoint handler method oauth2callback():, lines 124 to 126:

    # Store the credentials as JSON to a file on disk.
    with open(CREDENTIALS_FILE, 'w') as f: 
        json.dump(credentials_as_dict, f)

This is where we write the entire granted credentials to a local JSON file as discussed in point 2 above.

● For the method load_credentials(), we attempt to load credentials from an existing credentials JSON file. Please see point 3 above.

● The default endpoint handler method index(): This is like the “main” function of this example “program”:

If the application has not been authorised yet, we attempt to load the previously acquired credentials from the local JSON file. If there are no previous credentials, or the local JSON content is in error, we attempt to request authorisation from the Google Cloud API.

We optimistically assume that one of the above steps will succeed. This is not production-ready code as stated in the introduction.

The rest of the code in this method is as per the official example. 💥 Please note that the three API call methods discussed in point 4 above have all been commented out. You will need to enable each one of them to see how they work.

⓷ To run the example application, we need to install the required packages onto the active virtual environment as per the official instructions:

pip install --upgrade google-api-python-client
pip install --upgrade google-auth google-auth-oauthlib google-auth-httplib2
pip install --upgrade flask

From the active virtual environment venv, launch the example as follows:

▶️Windows 10:venv\Scripts\python.exe youtube_data_api.py 
▶️Ubuntu 22.10:./venv/bin\python youtube_data_api.py

🚀 Then, on the same machine, access the example URL http://localhost:5000/.

For the first time requesting authorisation, the Google Cloud API will initiate the OAuth 2.0 user process, requesting the user to grant permissions. Please see the screenshots of this process below. Your experience should look similar:

The default endpoint method handler index(): was calling videos_get_file_names(...). After the user granted permissions and the OAuth 2.0 process was successful, we should have a response similar to the one illustrated below:

💥 Most importantly, the credentials JSON file credentials_youtube.json should have been written to the directory where youtube_data_api.py is located. Please check this for yourself.

⓸ Using the credentials_youtube.json file on another machine: For learning purposes, I copied both the youtube_data_api.py and credentials_youtube.json files to Ubuntu 22.10, set up the virtual environment, and ran the example without reconfiguring redirect_uri. The Google Cloud API happily accepted these credentials. Please see the screenshot below:

⓹ The method videos_get_file_names(client, **kwargs): can only work with videos which belong to our own YouTube channel. For understanding purposes, I tried to access a public video from another public channel, and below is the response:

googleapiclient.errors.HttpError: <HttpError 403 when requesting https://youtube.googleapis.com/youtube/v3/videos?part=fileDetails&id=97t-8NF7oz4&alt=json returned “The request is not properly authorized to access video file or processing information. Note that the fileDetails, processingDetails, and suggestions parts are only available to that video’s owner.”. Details: “[{‘message’: “The request is not properly authorized to access video file or processing information. Note that the fileDetails, processingDetails, and suggestions parts are only available to that video’s owner.”, ‘domain’: ‘youtube.video’, ‘reason’: ‘forbidden’, ‘location’: ‘part’, ‘locationType’: ‘parameter’}]”>

❻ Here are some helpful links for managing Google Cloud API projects.

❼ Please note: We have previously discussed the Google Gmail API in the following posts:

  1. GMail API: quick start with Python and NodeJs
  2. GMail API: send emails with NodeJs
  3. CI/CD #02. Jenkins: basic email using your Gmail account
  4. Sending emails via Gmail account using the Python module smtplib

In addition to the Google Gmail API, I have also used the Cloud Text-to-Speech API with Python. The Text-to-Speech API is comparatively easier to set up than both the Gmail API and the YouTube API.

❽ I did not intend for this post to be this long. There are just so many details to cover. Hopefully, this post will serve as a stepping stone for trying other Google Cloud APIs. I have learned a lot writing this post. And I apologise for not being able to make it shorter.

Thank you for reading. I hope you find the information in this post useful. Stay safe, as always.

✿✿✿

Feature image source:

Design a site like this with WordPress.com
Get started