1. Visão geral
A série de codelabs da estação de migração sem servidor (tutoriais práticos e individualizados) e os vídeos relacionados têm como objetivo ajudar desenvolvedores sem servidor do Google Cloud a modernizar apps, orientando-os em uma ou mais migrações, principalmente para evitar serviços legados. Isso torna seus apps mais portáteis e oferece mais opções e flexibilidade, o que permite a integração e o acesso a uma variedade maior de produtos do Cloud e o upgrade para versões de idiomas mais recentes com mais facilidade. Embora inicialmente voltada para os primeiros usuários do Cloud, principalmente desenvolvedores do App Engine (ambiente padrão), esta série é ampla o suficiente para incluir outras plataformas sem servidor, como o Cloud Functions e o Cloud Run, ou outros lugares, se aplicável.
Este codelab do Módulo 15 explica como adicionar o uso do App Engine blobstore
ao app de exemplo do Módulo 0. Assim você poderá migrar esse uso para o Cloud Storage a seguir no módulo 16.
Você vai aprender a
- Adicionar uso da biblioteca/API Blobstore do App Engine
- Armazenar uploads de usuários no serviço
blobstore
- Prepare-se para a próxima etapa da migração para o Cloud Storage
O que é necessário
- Um projeto do Google Cloud Platform com uma conta de faturamento ativa do GCP.
- Habilidades básicas em Python
- Conhecimento prático de comandos comuns do Linux
- Conhecimento básico sobre desenvolvimento e implantação de apps no App Engine
- Um app do App Engine do módulo 0 em funcionamento (get from repo)
Pesquisa
Como você vai usar este tutorial?
Como você classificaria sua experiência com Python?
Como você classificaria sua experiência de uso dos serviços do Google Cloud?
2. Contexto
Para migrar da API App Engine Blobstore, adicione o uso dela ao app ndb
de referência do App Engine no módulo 0. O app de exemplo mostra as dez visitas mais recentes ao usuário. Estamos modificando o app para pedir que o usuário final faça upload de um artefato (um arquivo) que corresponda à "visita". Se o usuário não quiser fazer isso, há um botão "pular" é a melhor opção. Independentemente da decisão do usuário, a próxima página renderiza o mesmo resultado que o aplicativo do Módulo 0 (e muitos dos outros módulos desta série). Com essa integração do blobstore
do App Engine implementada, podemos migrá-la para o Cloud Storage no próximo codelab (módulo 16).
O App Engine fornece acesso aos sistemas de modelos Django e Jinja2. Uma coisa que torna esse exemplo diferente (além de adicionar acesso ao Blobstore) é que ele alterna do uso do Django no Módulo 0 para o Jinja2 aqui no Módulo 15. Uma etapa importante na modernização de apps do App Engine é migrar frameworks da Web do webapp2
para o Flask. O segundo usa Jinja2 como seu sistema de modelagem padrão, então começamos a seguir nessa direção implementando o Jinja2 e permanecemos em webapp2
para acesso ao Blobstore. Como o Flask usa o Jinja2 por padrão, nenhuma alteração no modelo será necessária no módulo 16.
3. Configuração/Pré-trabalho
Antes de chegarmos à parte principal do tutorial, configure seu projeto, receba o código e implante o aplicativo de referência para começar a trabalhar com o código.
1. Configurar projeto
Se você já implantou o app do Módulo 0, recomendamos reutilizar o mesmo projeto (e código). Outra opção é criar um novo projeto ou reutilizar um projeto existente. Verifique se o projeto tem uma conta de faturamento ativa e se o App Engine está ativado.
2. Receber app de amostra do valor de referência
Um dos pré-requisitos deste codelab é ter um app de exemplo do Módulo 0 em funcionamento. Caso não o tenha, pode obtê-lo no Módulo 0 "INICIAR" da pasta (link abaixo). Este codelab orienta você em cada etapa e conclui com um código semelhante ao do Módulo 15, "FINISH". do Compute Engine.
- INÍCIO: pasta do módulo 0 (Python 2)
- FINISH: pasta do módulo 15 (Python 2)
- Repositório inteiro (para clonar ou fazer o download do arquivo ZIP)
O diretório dos arquivos STARTing do Módulo 0 deve se parecer com este:
$ ls README.md index.html app.yaml main.py
3. (Re) Implantar aplicativo de referência
As etapas de pré-trabalho restantes para serem executadas agora:
- Conheça melhor a ferramenta de linha de comando
gcloud
- Reimplantar o aplicativo de amostra com
gcloud app deploy
- Confirmar se o aplicativo é executado no App Engine sem problemas
Depois de executar essas etapas e conferir que seu app da Web funciona (com uma saída semelhante à mostrada abaixo), você poderá usar o armazenamento em cache.
4. Atualizar os arquivos de configuração
app.yaml
Não há alterações significativas na configuração do aplicativo. No entanto, como mencionado anteriormente, estamos mudando dos modelos Django (padrão) para o Jinja2. Portanto, para alternar, os usuários devem especificar a versão mais recente do Jinja2 disponível nos servidores do App Engine, e você faz isso adicionando-a à seção de bibliotecas de terceiros integrada do app.yaml
.
ANTES:
runtime: python27
threadsafe: yes
api_version: 1
handlers:
- url: /.*
script: main.app
Edite seu arquivo app.yaml
adicionando uma nova seção libraries
como esta:
DEPOIS:
runtime: python27
threadsafe: yes
api_version: 1
handlers:
- url: /.*
script: main.app
libraries:
- name: jinja2
version: latest
Nenhum outro arquivo de configuração precisa ser atualizado, então vamos passar para os arquivos de aplicativo.
5. Modificar arquivos do aplicativo
Suporte a importações e Jinja2
O primeiro conjunto de alterações para main.py
inclui a adição do uso da API Blobstore e a substituição dos modelos Django por Jinja2. Veja o que vai mudar:
- O objetivo do módulo
os
é criar um caminho de arquivo para um modelo Django. Como estamos mudando para Jinja2, onde isso é tratado, o uso deos
e do renderizador de modelo Django,google.appengine.ext.webapp.template
, não são mais necessários, eles estão sendo removidos. - Importe a API Blobstore:
google.appengine.ext.blobstore
- Importe os gerenciadores do Blobstore encontrados no framework
webapp
original. Eles não estão disponíveis emwebapp2
:google.appengine.ext.webapp.blobstore_handlers
- Importar o suporte a Jinja2 do pacote
webapp2_extras
ANTES:
import os
import webapp2
from google.appengine.ext import ndb
from google.appengine.ext.webapp import template
Implemente as mudanças na lista acima substituindo a seção de importação atual em main.py
pelo snippet de código abaixo.
DEPOIS:
import webapp2
from webapp2_extras import jinja2
from google.appengine.ext import blobstore, ndb
from google.appengine.ext.webapp import blobstore_handlers
Após as importações, adicione um código boilerplate para permitir o uso do Jinja2, conforme definido nos documentos do webapp2_extras
. O snippet de código a seguir encapsula a classe do gerenciador de solicitações padrão do webapp2 com a funcionalidade Jinja2. Portanto, adicione este bloco de código a main.py
logo após as importações:
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))
Adicionar suporte ao Blobstore
Ao contrário de outras migrações desta série, em que mantemos a funcionalidade ou a saída do app de exemplo idêntica (ou quase a mesma) sem mudanças (muito) na UX, esse exemplo se distingue de maneira mais radical da norma. Em vez de registrar imediatamente uma nova visita e exibir os 10 mais recentes, estamos atualizando o app para solicitar ao usuário um artefato de arquivo para registrar a visita. Os usuários finais podem fazer upload de um arquivo correspondente ou selecionar "Pular" para não fazer upload de nada. Depois que essa etapa for concluída, as "visitas mais recentes" é exibida.
Essa mudança permite que nosso aplicativo use o serviço Blobstore para armazenar (e possivelmente renderizar mais tarde) essa imagem ou outro tipo de arquivo na página de visitas mais recentes.
Atualizar o modelo de dados e implementar o uso dele
Estamos armazenando mais dados, atualizando especificamente o modelo de dados para armazenar o ID (chamado de "BlobKey
") do arquivo enviado ao Blobstore e adicionando uma referência para salvar isso em store_visit()
. Como esses dados extras são retornados com todo o restante na consulta, fetch_visits()
permanece o mesmo.
Confira o que acontece antes e depois dessas atualizações com file_blob
, um ndb.BlobKeyProperty
:
ANTES:
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)
DEPOIS:
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)
Veja uma representação pictórica das mudanças feitas até agora:
Suporte a uploads de arquivos
A mudança mais significativa de funcionalidade é o suporte a uploads de arquivos, seja solicitando ao usuário um arquivo, oferecendo suporte para a opção "pular" ou renderizar um arquivo correspondente a uma visita. Tudo isso faz parte do quadro. Estas são as mudanças necessárias para oferecer suporte a uploads de arquivos:
- A solicitação
GET
do gerenciador principal não busca mais as visitas mais recentes para exibição. Em vez disso, ele solicita que o usuário faça um upload. - Quando um usuário final envia um arquivo para upload ou pula esse processo, um
POST
do formulário passa o controle para o novoUploadHandler
, derivado degoogle.appengine.ext.webapp.blobstore_handlers.BlobstoreUploadHandler
. - O método
POST
deUploadHandler
realiza o upload, chamastore_visit()
para registrar a visita e aciona um redirecionamento HTTP 307 para enviar o usuário de volta a "/", em que... - O método
POST
do gerenciador principal consulta (viafetch_visits()
) e exibe as visitas mais recentes. Se o usuário selecionar "pular", nenhum arquivo é enviado, mas a visita continua registrada e, em seguida, pelo mesmo redirecionamento. - A exibição de visitas mais recentes inclui um novo campo exibido ao usuário, uma "visualização" com hiperlink se um arquivo de upload estiver disponível ou "nenhum" caso contrário. Essas alterações são percebidas no modelo HTML em conjunto com a adição de um formulário de upload (mais sobre isso em breve).
- Se um usuário final clicar na "visualização" para qualquer visita com um vídeo enviado, ele faz uma solicitação
GET
para um novoViewBlobHandler
, derivado degoogle.appengine.ext.webapp.blobstore_handlers.BlobstoreDownloadHandler
, renderizando o arquivo se for uma imagem (no navegador se compatível), solicitando o download, caso contrário, ou retornando um erro HTTP 404 se não for encontrado. - Além do novo par de classes de gerenciador e de um novo par de rotas para enviar tráfego a elas, o gerenciador principal precisa de um novo método
POST
para receber o redirecionamento 307 descrito acima.
Antes dessas atualizações, o app Module 0 tinha apenas um gerenciador principal com um método GET
e uma única rota:
ANTES:
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)
Com essas atualizações implementadas, agora há três gerenciadores: 1) gerenciador de upload com um método POST
e 2) um "blob de visualização" de download com um método GET
e 3) o gerenciador principal com os métodos GET
e POST
. Faça essas mudanças para que o restante do app fique como o exemplo abaixo.
DEPOIS:
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)
Há várias chamadas importantes nesse código que acabamos de adicionar:
- Em
MainHandler.get
, há uma chamada parablobstore.create_upload_url
. Essa chamada gera o URL para o formulárioPOST
s, chamando o gerenciador de upload para enviar o arquivo ao Blobstore. - Em
UploadHandler.post
, há uma chamada parablobstore_handlers.BlobstoreUploadHandler.get_uploads
. Essa é a mágica que coloca o arquivo no Blobstore e retorna um ID exclusivo e persistente para ele, oBlobKey
. - Em
ViewBlobHandler.get
, chamarblobstore_handlers.BlobstoreDownloadHandler.send
com oBlobKey
de um arquivo resulta na busca e encaminhamento dele para o navegador do usuário final.
Essas chamadas representam a maior parte do acesso aos recursos adicionados ao app. Confira uma representação pictórica desse segundo e último conjunto de mudanças em main.py
:
Atualizar modelo HTML
Algumas das atualizações do aplicativo principal afetam a interface do usuário (IU) do aplicativo. Portanto, são necessárias alterações correspondentes no modelo da Web: duas delas:
- É necessário um formulário de upload de arquivos com três elementos de entrada: um arquivo e um par de botões de envio para fazer upload e pular o arquivo, respectivamente.
- Atualizar o resultado das visitas mais recentes adicionando uma "visualização" para visitas com um upload de arquivo correspondente ou "nenhum" caso contrário.
ANTES:
<!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>
Implemente as mudanças da lista acima para incluir o modelo atualizado:
DEPOIS:
<!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>
Esta imagem ilustra as atualizações necessárias para index.html
:
Uma alteração final é que o Jinja2 prefere seus modelos em uma pasta templates
, portanto, crie essa pasta e mova index.html
para dentro dela. Com essa mudança final, você concluiu todas as mudanças necessárias para adicionar o uso do Blobstore ao app de exemplo do Módulo 0.
(opcional) "aprimoramento" do Cloud Storage
O armazenamento do Blobstore evoluiu para o próprio Cloud Storage. Isso significa que os uploads do Blobstore ficam visíveis no console do Cloud, especificamente no navegador do Cloud Storage. A questão é onde. A resposta é o bucket padrão do Cloud Storage no aplicativo do App Engine. O nome é o nome de domínio completo do seu aplicativo do App Engine, PROJECT_ID
.appspot.com
. Isso é muito conveniente, porque todos os IDs de projeto são exclusivos, certo?
As atualizações feitas no aplicativo de exemplo descartam os arquivos enviados para esse bucket, mas os desenvolvedores têm a opção de escolher um local mais específico. O bucket padrão pode ser acessado de maneira programática via google.appengine.api.app_identity.get_default_gcs_bucket_name()
, o que exige uma nova importação se você quiser acessar esse valor, por exemplo, para usar como prefixo para organizar os arquivos enviados. Por exemplo, classificação por tipo de arquivo:
Para implementar algo assim para imagens, por exemplo, você terá um código como este, junto com um código que verificou os tipos de arquivo para escolher o nome do bucket desejado:
ROOT_BUCKET = app_identity.get_default_gcs_bucket_name()
IMAGE_BUCKET = '%s/%s' % (ROOT_BUCKET, 'images')
Você também validará as imagens enviadas usando uma ferramenta como o módulo imghdr
da Biblioteca padrão do Python para confirmar o tipo de imagem. Por fim, é uma boa ideia limitar o tamanho dos uploads em caso de usuários de má-fé.
Digamos que tudo isso foi feito. Como podemos atualizar nosso aplicativo para oferecer suporte à especificação de onde armazenar os arquivos enviados? A chave é ajustar a chamada para blobstore.create_upload_url
em MainHandler.get
para especificar o local desejado no Cloud Storage para o upload adicionando o parâmetro gs_bucket_name
desta forma:
blobstore.create_upload_url('/upload', gs_bucket_name=IMAGE_BUCKET))
Como essa é uma atualização opcional caso você queira especificar para onde os uploads precisam ir, ela não faz parte do arquivo main.py
no repositório. Em vez disso, uma alternativa chamada main-gcs.py
está disponível para revisão no repositório. Em vez de usar uma "pasta" separada com um bucket, o código em main-gcs.py
armazena os uploads na pasta "raiz". bucket (PROJECT_ID
.appspot.com
), assim como main.py
, mas fornece a estrutura necessária para derivar a amostra em algo mais conforme sugerido nesta seção. Veja abaixo uma ilustração das "diferenças" entre main.py
e main-gcs.py
.
6. Resumo/limpeza
Esta seção encerra este codelab implantando o app, verificando se ele funciona conforme o esperado e em qualquer saída refletida. Após a validação do app, execute as etapas de limpeza e considere as próximas etapas.
Implante e verifique o aplicativo
Implante o app novamente com gcloud app deploy
e confirme se ele funciona conforme anunciado, com uma experiência do usuário (UX) diferente do app do Módulo 0. Agora seu app tem duas telas diferentes. A primeira é a solicitação do formulário de upload de arquivos de visita:
A partir daí, os usuários finais fazem upload de um arquivo e clicam em "Enviar" ou clique em "Pular" para não fazer upload de nada. Em ambos os casos, o resultado é a tela de visita mais recente, agora aumentada com "visualização" links ou "nenhum" entre os carimbos de data/hora das visitas e as informações do visitante:
Parabéns por concluir este codelab adicionando o uso do Blobstore do App Engine ao app de exemplo do Módulo 0. Seu código agora deve corresponder ao que está na pasta FINISH (módulo 15). O main-gcs.py
alternativo também está presente nessa pasta.
Limpar
Geral
Se você já tiver terminado por enquanto, recomendamos que desative seu aplicativo do App Engine para evitar cobranças. No entanto, se você quiser fazer mais testes, saiba que a plataforma do App Engine tem uma cota sem custo financeiro e, desde que você não exceda esse nível de uso, não haverá cobranças. Isso é para computação, mas também pode haver cobranças por serviços relevantes do App Engine. Portanto, consulte a página de preços para mais informações. Se essa migração envolver outros serviços do Cloud, eles serão faturados separadamente. Em ambos os casos, se aplicável, consulte a seção "Específico para este codelab". seção abaixo.
Para divulgação completa, a implantação em uma plataforma de computação sem servidor do Google Cloud, como o App Engine, incorre em menores custos de criação e armazenamento. O Cloud Build tem a própria cota sem custo financeiro, assim como o Cloud Storage. O armazenamento da imagem consome parte da cota. No entanto, talvez você more em uma região que não tenha esse nível sem custo financeiro, portanto, esteja ciente do uso do armazenamento para minimizar os possíveis custos. "Pastas" específicas do Cloud Storage que você deve analisar incluem:
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
- Os links de armazenamento acima dependem do
PROJECT_ID
e da *LOC
*ação, por exemplo, "us
" caso seu app esteja hospedado nos EUA.
Por outro lado, se você não for continuar com este aplicativo ou outros codelabs de migração relacionados e quiser excluir tudo completamente, encerre seu projeto.
Específicos deste codelab
Os serviços listados abaixo são exclusivos deste codelab. Consulte a documentação de cada produto para mais informações:
- O serviço Blobstore do App Engine se enquadra nas Cotas e limites de dados armazenados. Analise essa questão e a página de preços de serviços em pacote legados.
- o serviço App Engine Datastore é fornecido pelo Cloud Datastore (Cloud Firestore no modo Datastore), que também tem um nível sem custo financeiro; consulte a página de preços para mais informações.
Próximas etapas
A próxima migração lógica a ser considerada é abordada no Módulo 16, que mostra aos desenvolvedores como migrar do serviço Blobstore do App Engine para usar a biblioteca de cliente do Cloud Storage. Os benefícios do upgrade incluem a possibilidade de acessar mais recursos do Cloud Storage, familiarizar-se com uma biblioteca de cliente que funciona para aplicativos fora do App Engine, seja no Google Cloud, em outras nuvens ou até mesmo no local. Se você não precisar de todos os recursos disponíveis no Cloud Storage ou se estiver preocupado com seus efeitos no custo, fique à vontade para usar o Blobstore do App Engine.
Além do módulo 16, há uma série de outras migrações possíveis, como Cloud NBS, Cloud Datastore, Cloud Tasks ou Cloud Memorystore. Há também migrações entre produtos para o Cloud Run e o Cloud Functions. O repositório de migração apresenta todos os exemplos de código, links para todos os codelabs e vídeos disponíveis, além de orientações sobre quais migrações considerar e qualquer "ordem" relevante. o número de migrações.
7. Outros recursos
Problemas/feedback do codelab
Se você encontrar problemas com este codelab, pesquise seu problema antes de preenchê-lo. Links para pesquisar e criar novos problemas:
Recursos de migração
Os links para as pastas do repositório do Módulo 0 (INÍCIO) e do Módulo 15 (FINISH) podem ser encontrados na tabela abaixo. Eles também podem ser acessados no repositório de todas as migrações de codelab do App Engine. Você pode clonar ou fazer o download de um arquivo ZIP.
Codelab | Python 2 | Python 3 |
Module 0 | N/A | |
Módulo 15 (este codelab) | N/A |
Recursos on-line
Veja abaixo recursos on-line que podem ser relevantes para este tutorial:
App Engine
- Serviço Blobstore do App Engine
- Cotas e limites de dados armazenados do App Engine
- Documentação do App Engine
- Ambiente de execução do App Engine para Python 2 (ambiente padrão)
- Como usar as bibliotecas integradas do App Engine no App Engine para Python 2
- Informações de preços e cotas do App Engine
- Lançamento da plataforma App Engine de segunda geração (2018)
- Comparação entre a primeira e plataformas de segunda geração
- Suporte de longo prazo para ambientes de execução legados
- Repositório de exemplos de migração da documentação (em inglês)
- Repositório de amostras de migração com contribuição da comunidade
Google Cloud
- Python no Google Cloud Platform
- Bibliotecas de cliente do Python para Google Cloud
- "Sempre sem custo financeiro" do Google Cloud nível
- SDK Google Cloud (ferramenta de linha de comando gcloud)
- Toda a documentação do Google Cloud
Python
- Sistemas de modelos Django e Jinja2
- Framework da Web
webapp2
- Documentação do
webapp2
(link em inglês) - Links
webapp2_extras
webapp2_extras
Documentação do Jinja2
Vídeos
- Estação de migração sem servidor
- Expedições sem servidor
- Inscreva-se no Google Cloud Tech
- Inscreva-se no Google Developers
Licença
Este conteúdo está sob a licença Atribuição 2.0 Genérica da Creative Commons.