Come utilizzare l'archivio BLOB di App Engine (Modulo 15)

1. Panoramica

La serie di codelab Serverless Migration Station (tutorial pratici autogestiti) e i video correlati mirano ad aiutare gli sviluppatori di Google Cloud serverless a modernizzare le loro applicazioni guidandoli attraverso una o più migrazioni, principalmente l'abbandono dei servizi legacy. In questo modo, le tue app sono più portabili e hai più opzioni e flessibilità, il che ti consente di integrarle e accedere a una gamma più ampia di prodotti Cloud ed eseguire più facilmente l'upgrade alle versioni più recenti dei linguaggi. Sebbene inizialmente si concentri sui primi utenti di Cloud, principalmente gli sviluppatori di App Engine (ambiente standard), questa serie è abbastanza ampia da includere altre piattaforme serverless come Cloud Functions e Cloud Run o altre, se applicabile.

Questo codelab del Modulo 15 spiega come aggiungere l'utilizzo di blobstore di App Engine all'app di esempio del Modulo 0. Successivamente, nel Modulo 16, potrai eseguire la migrazione di questo utilizzo a Cloud Storage.

Imparerai a utilizzare

  • Aggiungi l'utilizzo dell'API/libreria Blobstore di App Engine
  • Archivia i caricamenti degli utenti nel servizio blobstore
  • Preparati al passaggio successivo per eseguire la migrazione a Cloud Storage

Che cosa ti serve

Sondaggio

Come utilizzerai questo tutorial?

Leggerlo e basta Leggerlo e completare gli esercizi

Come valuteresti la tua esperienza con Python?

Principiante Intermedio Esperto

Come valuteresti la tua esperienza con l'utilizzo dei servizi Google Cloud?

Principiante Intermedio Esperto

2. Sfondo

Per eseguire la migrazione dall'API Blobstore di App Engine, aggiungi il suo utilizzo all'app App Engine ndb di riferimento esistente del Modulo 0. L'app di esempio mostra all'utente le dieci visite più recenti. Stiamo modificando l'app in modo che chieda all'utente finale di caricare un artefatto (un file) corrispondente alla sua "visita". Se l'utente non vuole farlo, è disponibile un'opzione "Salta". Indipendentemente dalla decisione dell'utente, la pagina successiva mostra lo stesso output dell'app del Modulo 0 (e di molti altri moduli di questa serie). Una volta implementata questa integrazione di App Engine blobstore, possiamo eseguirne la migrazione a Cloud Storage nel prossimo codelab (Modulo 16).

App Engine fornisce l'accesso ai sistemi di modelli Django e Jinja2. Una cosa che rende questo esempio diverso (oltre all'aggiunta dell'accesso all'archivio BLOB) è che passa dall'utilizzo di Django nel Modulo 0 a Jinja2 qui nel Modulo 15. Un passaggio chiave nella modernizzazione delle app App Engine è la migrazione dei framework web da webapp2 a Flask. Quest'ultimo utilizza Jinja2 come sistema di modelli predefinito, quindi iniziamo a muoverci in questa direzione implementando Jinja2 e rimanendo su webapp2 per l'accesso a Blobstore. Poiché Flask utilizza Jinja2 per impostazione predefinita, non sarà necessario apportare modifiche al modello nel Modulo 16.

3. Configurazione/preparazione

Prima di passare alla parte principale del tutorial, configura il progetto, ottieni il codice ed esegui il deployment dell'app di base per iniziare con il codice funzionante.

1. Configura il progetto

Se hai già eseguito il deployment dell'app del Modulo 0, ti consigliamo di riutilizzare lo stesso progetto (e lo stesso codice). In alternativa, puoi creare un nuovo progetto o riutilizzare un altro progetto esistente. Assicurati che il progetto abbia un account di fatturazione attivo e che App Engine sia abilitato.

2. Scarica l'app di esempio di base

Uno dei prerequisiti di questo codelab è avere un'app di esempio del Modulo 0 funzionante. Se non ce l'hai, puoi scaricarla dalla cartella "START" del Modulo 0 (link di seguito). Questo codelab ti guida attraverso ogni passaggio, per concludere con un codice simile a quello presente nella cartella "FINISH" del Modulo 15.

La directory dei file di INIZIO del Modulo 0 dovrebbe avere il seguente aspetto:

$ ls
README.md               index.html
app.yaml                main.py

3. (Riesegui il) deployment dell'app di base

I passaggi rimanenti della preparazione da eseguire ora:

  1. Acquisisci nuovamente familiarità con lo strumento a riga di comando gcloud
  2. Esegui di nuovo il deployment dell'app di esempio con gcloud app deploy
  3. Verifica che l'app venga eseguita su App Engine senza problemi

Una volta eseguiti correttamente questi passaggi e verificato che l'app web funziona (con un output simile a quello riportato di seguito), puoi aggiungere l'utilizzo della memorizzazione nella cache alla tua app.

a7a9d2b80d706a2b.png

4. Aggiorna i file di configurazione

app.yaml

Non sono previste modifiche sostanziali alla configurazione dell'applicazione, tuttavia, come accennato in precedenza, stiamo passando dai modelli Django (predefiniti) a Jinja2, quindi per cambiare, gli utenti devono specificare l'ultima versione di Jinja2 disponibile sui server App Engine, aggiungendola alla sezione delle librerie di terze parti integrate di app.yaml.

PRIMA:

runtime: python27
threadsafe: yes
api_version: 1

handlers:
- url: /.*
  script: main.app

Modifica il file app.yaml aggiungendo una nuova sezione libraries come mostrato di seguito:

AFTER:

runtime: python27
threadsafe: yes
api_version: 1

handlers:
- url: /.*
  script: main.app

libraries:
- name: jinja2
  version: latest

Non è necessario aggiornare altri file di configurazione, quindi passiamo ai file dell'applicazione.

5. Modifica i file dell'applicazione

Supporto di importazioni e Jinja2

Il primo insieme di modifiche per main.py include l'aggiunta dell'utilizzo dell'API Blobstore e la sostituzione dei modelli Django con Jinja2. Ecco cosa cambierà:

  1. Lo scopo del modulo os è creare un percorso del nome file per un template Django. Poiché stiamo passando a Jinja2, dove questo viene gestito, l'utilizzo di os e del renderer dei modelli Django, google.appengine.ext.webapp.template, non è più necessario, quindi vengono rimossi.
  2. Importa l'API Blobstore: google.appengine.ext.blobstore
  3. Importa i gestori di Blobstore presenti nel framework webapp originale, che non sono disponibili in webapp2: google.appengine.ext.webapp.blobstore_handlers
  4. Importa il supporto di Jinja2 dal pacchetto webapp2_extras

PRIMA:

import os
import webapp2
from google.appengine.ext import ndb
from google.appengine.ext.webapp import template

Implementa le modifiche nell'elenco riportato sopra sostituendo l'attuale sezione di importazione in main.py con lo snippet di codice riportato di seguito.

AFTER:

import webapp2
from webapp2_extras import jinja2
from google.appengine.ext import blobstore, ndb
from google.appengine.ext.webapp import blobstore_handlers

Dopo le importazioni, aggiungi un po' di codice boilerplate per supportare l'utilizzo di Jinja2 come definito nella documentazione di webapp2_extras. Il seguente snippet di codice racchiude la classe del gestore delle richieste webapp2 standard con la funzionalità Jinja2, quindi aggiungi questo blocco di codice a main.py subito dopo le importazioni:

class BaseHandler(webapp2.RequestHandler):
    'Derived request handler mixing-in Jinja2 support'
    @webapp2.cached_property
    def jinja2(self):
        return jinja2.get_jinja2(app=self.app)

    def render_response(self, _template, **context):
        self.response.write(self.jinja2.render_template(_template, **context))

Aggiungi il supporto di Blobstore

A differenza di altre migrazioni di questa serie in cui manteniamo la funzionalità o l'output dell'app di esempio identici (o quasi) senza (molte) modifiche alla UX, questo esempio si discosta in modo più radicale dalla norma. Anziché registrare immediatamente una nuova visita e poi visualizzare le dieci più recenti, stiamo aggiornando l'app per chiedere all'utente un file artefatto con cui registrare la visita. Gli utenti finali possono quindi caricare un file corrispondente o selezionare "Salta" per non caricare nulla. Una volta completato questo passaggio, viene visualizzata la pagina "Visite più recenti".

Questa modifica consente alla nostra app di utilizzare il servizio Blobstore per archiviare (ed eventualmente eseguire il rendering in un secondo momento) l'immagine o un altro tipo di file nella pagina delle visite più recenti.

Aggiorna il modello di dati e implementane l'utilizzo

Stiamo archiviando più dati, in particolare aggiornando il modello di dati per archiviare l'ID (chiamato "BlobKey") del file caricato in Blobstore e aggiungendo un riferimento per salvarlo in store_visit(). Poiché questi dati aggiuntivi vengono restituiti insieme a tutto il resto al momento della query, fetch_visits() rimane invariato.

Ecco il prima e il dopo con questi aggiornamenti che includono file_blob, un ndb.BlobKeyProperty:

PRIMA:

class Visit(ndb.Model):
    'Visit entity registers visitor IP address & timestamp'
    visitor   = ndb.StringProperty()
    timestamp = ndb.DateTimeProperty(auto_now_add=True)

def store_visit(remote_addr, user_agent):
    'create new Visit entity in Datastore'
    Visit(visitor='{}: {}'.format(remote_addr, user_agent)).put()

def fetch_visits(limit):
    'get most recent visits'
    return Visit.query().order(-Visit.timestamp).fetch(limit)

AFTER:

class Visit(ndb.Model):
    'Visit entity registers visitor IP address & timestamp'
    visitor   = ndb.StringProperty()
    timestamp = ndb.DateTimeProperty(auto_now_add=True)
    file_blob = ndb.BlobKeyProperty()

def store_visit(remote_addr, user_agent, upload_key):
    'create new Visit entity in Datastore'
    Visit(visitor='{}: {}'.format(remote_addr, user_agent),
            file_blob=upload_key).put()

def fetch_visits(limit):
    'get most recent visits'
    return Visit.query().order(-Visit.timestamp).fetch(limit)

Ecco una rappresentazione grafica delle modifiche apportate finora:

2270783776759f7f.png

Supporto dei caricamenti di file

La modifica più significativa alla funzionalità è il supporto dei caricamenti di file, sia che si tratti di chiedere all'utente un file, di supportare la funzionalità "Salta" o di eseguire il rendering di un file corrispondente a una visita. Tutto questo fa parte del quadro. Ecco le modifiche necessarie per supportare i caricamenti di file:

  1. La richiesta del gestore principale GET non recupera più le visite più recenti da visualizzare. ma chiede all'utente di caricarne uno.
  2. Quando un utente finale invia un file da caricare o salta questa procedura, un POST del modulo passa il controllo al nuovo UploadHandler, derivato da google.appengine.ext.webapp.blobstore_handlers.BlobstoreUploadHandler.
  3. Il metodo POST di UploadHandler esegue il caricamento, chiama store_visit() per registrare la visita e attiva un reindirizzamento HTTP 307 per rimandare l'utente a "/", dove…
  4. Il metodo POST del gestore principale esegue query (tramite fetch_visits()) e visualizza le visite più recenti. Se l'utente seleziona "Salta", non viene caricato alcun file, ma la visita viene comunque registrata, seguita dallo stesso reindirizzamento.
  5. La visualizzazione delle visite più recenti include un nuovo campo mostrato all'utente, ovvero un link ipertestuale "Visualizza" se è disponibile un file di caricamento o "Nessuno" in caso contrario. Queste modifiche vengono implementate nel modello HTML insieme all'aggiunta di un modulo di caricamento (disponibile a breve).
  6. Se un utente finale fa clic sul link "Visualizza" per una visita con un video caricato, viene effettuata una richiesta GET a un nuovo ViewBlobHandler, derivato da google.appengine.ext.webapp.blobstore_handlers.BlobstoreDownloadHandler , che esegue il rendering del file se si tratta di un'immagine (nel browser, se supportato), richiede il download in caso contrario o restituisce un errore HTTP 404 se non viene trovato.
  7. Oltre alla nuova coppia di classi di gestori e alla nuova coppia di route per inviare il traffico, il gestore principale ha bisogno di un nuovo metodo POST per ricevere il reindirizzamento 307 descritto sopra.

Prima di questi aggiornamenti, l'app del Modulo 0 presentava solo un gestore principale con un metodo GET e un singolo percorso:

PRIMA:

class MainHandler(webapp2.RequestHandler):
    'main application (GET) handler'
    def get(self):
        store_visit(self.request.remote_addr, self.request.user_agent)
        visits = fetch_visits(10)
        tmpl = os.path.join(os.path.dirname(__file__), 'index.html')
        self.response.out.write(template.render(tmpl, {'visits': visits}))

app = webapp2.WSGIApplication([
    ('/', MainHandler),
], debug=True)

Con l'implementazione di questi aggiornamenti, ora sono presenti tre gestori: 1) gestore di caricamento con un metodo POST, 2) un gestore di download "visualizza blob" con un metodo GET e 3) il gestore principale con i metodi GET e POST. Apporta queste modifiche in modo che il resto dell'app abbia l'aspetto seguente.

AFTER:

class UploadHandler(blobstore_handlers.BlobstoreUploadHandler):
    'Upload blob (POST) handler'
    def post(self):
        uploads = self.get_uploads()
        blob_id = uploads[0].key() if uploads else None
        store_visit(self.request.remote_addr, self.request.user_agent, blob_id)
        self.redirect('/', code=307)

class ViewBlobHandler(blobstore_handlers.BlobstoreDownloadHandler):
    'view uploaded blob (GET) handler'
    def get(self, blob_key):
        self.send_blob(blob_key) if blobstore.get(blob_key) else self.error(404)

class MainHandler(BaseHandler):
    'main application (GET/POST) handler'
    def get(self):
        self.render_response('index.html',
                upload_url=blobstore.create_upload_url('/upload'))

    def post(self):
        visits = fetch_visits(10)
        self.render_response('index.html', visits=visits)

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/upload', UploadHandler),
    ('/view/([^/]+)?', ViewBlobHandler),
], debug=True)

Nel codice appena aggiunto sono presenti diverse chiamate chiave:

  • In MainHandler.get, c'è una chiamata a blobstore.create_upload_url. Questa chiamata genera l'URL a cui il modulo POST, chiamando il gestore del caricamento per inviare il file a Blobstore.
  • In UploadHandler.post, c'è una chiamata a blobstore_handlers.BlobstoreUploadHandler.get_uploads. Questa è la vera magia che inserisce il file in Blobstore e restituisce un ID univoco e persistente per quel file, ovvero la sua BlobKey.
  • In ViewBlobHandler.get, la chiamata di blobstore_handlers.BlobstoreDownloadHandler.send con l'BlobKey di un file comporta il recupero del file e l'inoltro al browser dell'utente finale

Queste chiamate rappresentano la maggior parte dell'accesso alle funzionalità aggiunte all'app. Ecco una rappresentazione grafica di questo secondo e ultimo insieme di modifiche a main.py:

da2960525ac1b90d.png

Aggiorna il modello HTML

Alcuni degli aggiornamenti dell'applicazione principale influiscono sull'interfaccia utente (UI) dell'app, pertanto sono necessarie modifiche corrispondenti nel modello web, in particolare due:

  1. È necessario un modulo di caricamento file con tre elementi di input: un file e una coppia di pulsanti di invio, rispettivamente per il caricamento e il salto del file.
  2. Aggiorna l'output delle visite più recenti aggiungendo un link "visualizza" per le visite con un caricamento file corrispondente o "nessuno" in caso contrario.

PRIMA:

<!doctype html>
<html>
<head>
<title>VisitMe Example</title>
<body>

<h1>VisitMe example</h1>
<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
    <li>{{ visit.timestamp.ctime }} from {{ visit.visitor }}</li>
{% endfor %}
</ul>

</body>
</html>

Implementa le modifiche nell'elenco riportato sopra per includere il modello aggiornato:

AFTER:

<!doctype html>
<html>
<head>
<title>VisitMe Example</title>
<body>

<h1>VisitMe example</h1>
{% if upload_url %}

<h3>Welcome... upload a file? (optional)</h3>
<form action="{{ upload_url }}" method="POST" enctype="multipart/form-data">
    <input type="file" name="file"><p></p>
    <input type="submit"> <input type="submit" value="Skip">
</form>

{% else %}

<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
<li>{{ visit.timestamp.ctime() }}
    <i><code>
    {% if visit.file_blob %}
        (<a href="/view/{{ visit.file_blob }}" target="_blank">view</a>)
    {% else %}
        (none)
    {% endif %}
    </code></i>
    from {{ visit.visitor }}
</li>
{% endfor %}
</ul>

{% endif %}

</body>
</html>

Questa immagine illustra gli aggiornamenti richiesti per index.html:

8583e975f25aa9e7.png

Un'ultima modifica è che Jinja2 preferisce i suoi modelli in una cartella templates, quindi crea questa cartella e sposta index.html al suo interno. Con questo passaggio finale, hai completato tutte le modifiche necessarie per aggiungere l'utilizzo di Blobstore all'app di esempio del Modulo 0.

(Facoltativo) "Miglioramento" di Cloud Storage

Lo spazio di archiviazione Blobstore si è poi evoluto in Cloud Storage. Ciò significa che i caricamenti di Blobstore sono visibili nella console Cloud, in particolare nel browser Cloud Storage. La domanda è: dove? La risposta è il bucket Cloud Storage predefinito della tua app App Engine. Il nome corrisponde al nome di dominio completo della tua app App Engine, PROJECT_ID.appspot.com. È molto comodo perché tutti gli ID progetto sono univoci, giusto?

Gli aggiornamenti apportati all'applicazione di esempio inseriscono i file caricati in quel bucket, ma gli sviluppatori hanno la possibilità di scegliere una località più specifica. Il bucket predefinito è accessibile in modo programmatico tramite google.appengine.api.app_identity.get_default_gcs_bucket_name(), richiedendo una nuova importazione se vuoi accedere a questo valore, ad esempio per utilizzarlo come prefisso per organizzare i file caricati. Ad esempio, l'ordinamento per tipo di file:

f61f7a23a1518705.png

Per implementare un'operazione simile per le immagini, ad esempio, avrai un codice simile a questo, insieme a un codice che controlla i tipi di file per scegliere il nome del bucket desiderato:

ROOT_BUCKET = app_identity.get_default_gcs_bucket_name()
IMAGE_BUCKET = '%s/%s' % (ROOT_BUCKET, 'images')

Inoltre, convaliderai le immagini caricate utilizzando uno strumento come il modulo imghdr della libreria standard di Python per confermare il tipo di immagine. Infine, probabilmente vorrai limitare le dimensioni dei caricamenti in caso di malintenzionati.

Supponiamo che tutto questo sia stato fatto. Come possiamo aggiornare la nostra app per supportare la specifica della posizione in cui archiviare i file caricati? La chiave è modificare la chiamata a blobstore.create_upload_url in MainHandler.get per specificare la posizione desiderata in Cloud Storage per il caricamento aggiungendo il parametro gs_bucket_name in questo modo:

blobstore.create_upload_url('/upload', gs_bucket_name=IMAGE_BUCKET))

Poiché si tratta di un aggiornamento facoltativo per specificare dove devono essere caricati i file, non fa parte del file main.py nel repository. In alternativa, nel repository è disponibile un'alternativa denominata main-gcs.py per la tua revisione. Anziché utilizzare una "cartella" bucket separata, il codice in main-gcs.py archivia i caricamenti nel bucket "root" (PROJECT_ID.appspot.com) proprio come main.py, ma fornisce l'impalcatura necessaria se dovessi derivare l'esempio in qualcosa di più, come suggerito in questa sezione. Di seguito è riportata un'illustrazione delle "differenze" tra main.py e main-gcs.py.

256e1ea68241a501.png

6. Riepilogo/pulizia

Questa sezione conclude questo codelab con il deployment dell'app, la verifica del suo funzionamento come previsto e l'analisi dell'output generato. Dopo la convalida dell'app, esegui i passaggi di pulizia e valuta i passaggi successivi.

Esegui il deployment e verifica l'applicazione

Esegui nuovamente il deployment dell'app con gcloud app deploy e verifica che funzioni come previsto, con un'esperienza utente (UX) diversa da quella dell'app del Modulo 0. Ora nell'app sono presenti due schermate diverse, la prima è il prompt del modulo di caricamento dei file di visita:

f5b5f9f19d8ae978.pngDa qui, gli utenti finali caricano un file e fanno clic su "Invia" o su "Salta" per non caricare nulla. In entrambi i casi, il risultato è la schermata della visita più recente, ora arricchita con i link "Visualizza" o "Nessuno" tra i timestamp della visita e le informazioni sul visitatore:

f5ac6b98ee8a34cb.png

Complimenti per aver completato questo codelab aggiungendo l'utilizzo dell'archivio BLOB di App Engine all'app di esempio del Modulo 0. Il tuo codice dovrebbe ora corrispondere a quello nella cartella FINISH (Modulo 15). Anche l'alternativa main-gcs.py è presente in quella cartella.

Esegui la pulizia

Generale

Se hai finito, ti consigliamo di disattivare l'app App Engine per evitare addebiti. Tuttavia, se vuoi eseguire altri test o esperimenti, la piattaforma App Engine ha una quota senza costi, quindi, se non superi questo livello di utilizzo, non ti verranno addebitati costi. Questo vale per il computing, ma potrebbero essere addebitati anche i costi per i servizi App Engine pertinenti, quindi consulta la pagina dei prezzi per ulteriori informazioni. Se questa migrazione coinvolge altri servizi Cloud, questi vengono fatturati separatamente. In entrambi i casi, se applicabile, consulta la sezione "Specifico per questo codelab" di seguito.

Per completezza, il deployment su una piattaforma di serverless computing di Google Cloud come App Engine comporta costi minimi di build e archiviazione. Cloud Build ha una propria quota senza costi, così come Cloud Storage. L'archiviazione di questa immagine utilizza parte di questa quota. Tuttavia, potresti vivere in una regione che non dispone di un livello senza costi, quindi tieni d'occhio l'utilizzo dello spazio di archiviazione per ridurre al minimo i potenziali costi. Le "cartelle" di Cloud Storage specifiche che devi esaminare includono:

  • console.cloud.google.com/storage/browser/LOC.artifacts.PROJECT_ID.appspot.com/containers/images
  • console.cloud.google.com/storage/browser/staging.PROJECT_ID.appspot.com
  • I link allo spazio di archiviazione riportati sopra dipendono dal tuo PROJECT_ID e dalla tua *LOC*azione, ad esempio "us" se la tua app è ospitata negli Stati Uniti.

Se invece non intendi continuare con questa applicazione o con altri codelab di migrazione correlati e vuoi eliminare tutto, chiudi il progetto.

Specifico per questo codelab

I servizi elencati di seguito sono univoci per questo codelab. Per saperne di più, consulta la documentazione di ciascun prodotto:

Passaggi successivi

La migrazione logica successiva da considerare è trattata nel Modulo 16, che mostra agli sviluppatori come eseguire la migrazione dal servizio Blobstore di App Engine all'utilizzo della libreria client di Cloud Storage. I vantaggi dell'upgrade includono la possibilità di accedere a più funzionalità di Cloud Storage e di acquisire familiarità con una libreria client che funziona per le app esterne ad App Engine, sia in Google Cloud, in altri cloud o anche on-premise. Se ritieni di non aver bisogno di tutte le funzionalità disponibili in Cloud Storage o se temi che possa influire sui costi, puoi continuare a utilizzare l'archivio BLOB di App Engine.

Oltre al Modulo 16, sono possibili molte altre migrazioni, ad esempio Cloud NDB e Cloud Datastore, Cloud Tasks o Cloud Memorystore. Sono disponibili anche migrazioni tra prodotti a Cloud Run e Cloud Functions. Il repository di migrazione contiene tutti gli esempi di codice, i link a tutti i codelab e i video disponibili, oltre a fornire indicazioni sulle migrazioni da considerare e sull'"ordine" di migrazione pertinente.

7. Risorse aggiuntive

Problemi/feedback relativi ai codelab

Se riscontri problemi con questo codelab, cerca il problema prima di segnalarlo. Link per cercare e creare nuovi problemi:

Risorse per la migrazione

I link alle cartelle del repository per il Modulo 0 (START) e il Modulo 15 (FINISH) sono disponibili nella tabella seguente. Puoi accedervi anche dal repository per tutte le migrazioni dei codelab di App Engine, che puoi clonare o scaricare come file ZIP.

Codelab

Python 2

Python 3

Modulo 0

code

N/A

Modulo 15 (questo codelab)

code

N/A

Risorse online

Di seguito sono riportate le risorse online che potrebbero essere pertinenti per questo tutorial:

App Engine

Google Cloud

Python

Video

Licenza

Questo lavoro è concesso in licenza ai sensi di una licenza Creative Commons Attribution 2.0 Generic.