1. Обзор
Серия обучающих материалов Serverless Migration Station (самостоятельные практические уроки) и сопутствующих видеороликов призвана помочь разработчикам бессерверных приложений Google Cloud модернизировать свои приложения, проведя их через одну или несколько миграций, в первую очередь, через отказ от устаревших сервисов. Это делает ваши приложения более портативными, предоставляет больше возможностей и гибкости, позволяя интегрироваться с более широким спектром облачных продуктов и получать к ним доступ, а также упрощает обновление до более новых версий языков программирования. Хотя изначально серия ориентирована на самых первых пользователей облачных сервисов, в первую очередь разработчиков App Engine (стандартная среда), она достаточно широка, чтобы охватить и другие бессерверные платформы, такие как Cloud Functions и Cloud Run , или другие, если это применимо.
В этом практическом занятии по модулю 15 объясняется, как добавить использование blobstore App Engine в пример приложения из модуля 0. После этого вы будете готовы перенести это использование в облачное хранилище в модуле 16.
Вы узнаете, как
- Добавить возможность использования API/библиотеки App Engine Blobstore.
- Хранить пользовательские загрузки в сервис
blobstore - Подготовьтесь к следующему шагу — миграции в облачное хранилище.
Что вам понадобится
- Проект на платформе Google Cloud Platform с активным платежным аккаунтом GCP.
- Базовые навыки работы с Python.
- Практические навыки работы с распространенными командами Linux.
- Базовые знания разработки и развертывания приложений на платформе App Engine.
- Рабочий модуль 0 приложения App Engine (получите из репозитория)
Опрос
Как вы будете использовать этот учебный материал?
Как бы вы оценили свой опыт работы с Python?
Как бы вы оценили свой опыт использования сервисов Google Cloud?
2. Предыстория
Для перехода с API App Engine Blobstore необходимо добавить его использование в существующее базовое приложение App Engine ndb из модуля 0. В примере приложения пользователю отображаются десять последних посещений. Мы модифицируем приложение таким образом, чтобы оно предлагало пользователю загрузить артефакт (файл), соответствующий его «посещению». Если пользователь не хочет этого делать, есть опция «пропустить». Независимо от решения пользователя, на следующей странице отображается тот же результат, что и в приложении из модуля 0 (и многих других модулях этой серии). После реализации интеграции App Engine blobstore мы сможем перенести приложение в Cloud Storage в следующем практическом занятии (модуль 16).
App Engine предоставляет доступ к системам шаблонизации Django и Jinja2 , и одно из отличий этого примера (помимо добавления доступа к Blobstore) заключается в том, что в модуле 0 используется не Django, а Jinja2, а в модуле 15. Ключевым шагом в модернизации приложений App Engine является миграция веб-фреймворков с webapp2 на Flask. Последний использует Jinja2 в качестве системы шаблонизации по умолчанию, поэтому мы начинаем двигаться в этом направлении, внедряя Jinja2, оставаясь при этом на webapp2 для доступа к Blobstore. Поскольку Flask использует Jinja2 по умолчанию, это означает, что в модуле 16 никаких изменений в шаблоне не потребуется.
3. Подготовка/Предварительные работы
Прежде чем перейти к основной части руководства, настройте свой проект, получите код и разверните базовое приложение, чтобы начать работу с работающим кодом.
1. Настройка проекта
Если вы уже развернули приложение «Модуль 0», мы рекомендуем повторно использовать тот же проект (и код). В качестве альтернативы вы можете создать совершенно новый проект или использовать другой существующий проект. Убедитесь, что у проекта есть активный платежный аккаунт и включен App Engine.
2. Получите базовый образец приложения.
Одно из предварительных условий для выполнения этого практического задания — наличие работающего примера приложения из Модуля 0. Если у вас его нет, вы можете получить его из папки «START» Модуля 0 (ссылка ниже). В этом практическом задании вы шаг за шагом пройдете весь процесс, и в конце получите код, похожий на тот, что находится в папке «FINISH» Модуля 15.
- НАЧАЛО: Папка «Модуль 0» (Python 2)
- ЗАВЕРШЕНИЕ: Папка модуля 15 (Python 2)
- Весь репозиторий (для клонирования или загрузки ZIP-файла )
Структура каталога файлов, запускающих модуль 0, должна выглядеть следующим образом:
$ ls README.md index.html app.yaml main.py
3. (Повторное) развертывание базового приложения
Осталось выполнить следующие подготовительные шаги:
- Вспомните, как работает инструмент командной строки
gcloud - Повторно разверните демонстрационное приложение с помощью
gcloud app deploy - Убедитесь, что приложение работает в App Engine без проблем.
После успешного выполнения этих шагов и проверки работоспособности вашего веб-приложения (с результатом, аналогичным приведенному ниже), вы готовы добавить в приложение кэширование.

4. Обновите конфигурационные файлы.
app.yaml
Существенных изменений в конфигурации приложения нет, однако, как упоминалось ранее, мы переходим от шаблонизации Django (по умолчанию) к Jinja2, поэтому для переключения пользователям необходимо указать последнюю версию Jinja2, доступную на серверах App Engine, и сделать это можно, добавив ее в раздел встроенных сторонних библиотек файла app.yaml .
ДО:
runtime: python27
threadsafe: yes
api_version: 1
handlers:
- url: /.*
script: main.app
Отредактируйте файл app.yaml , добавив новый раздел libraries , как показано здесь:
ПОСЛЕ:
runtime: python27
threadsafe: yes
api_version: 1
handlers:
- url: /.*
script: main.app
libraries:
- name: jinja2
version: latest
Обновлять другие конфигурационные файлы не требуется, поэтому перейдем к файлам приложения.
5. Измените файлы приложения.
Поддержка импорта и Jinja2
Первый набор изменений в файле main.py включает добавление использования API Blobstore и замену шаблонизатора Django на Jinja2. Вот что меняется:
- Цель модуля
os— создать путь к файлу шаблона Django. Поскольку мы переходим на Jinja2, где это обрабатывается автоматически, использование `os, а также средства рендеринга шаблонов Django `google.appengine.ext.webapp.templateбольше не требуется, поэтому они удаляются. - Импортируйте API Blobstore:
google.appengine.ext.blobstore - Импортируйте обработчики Blobstore, которые есть в оригинальной
webappплатформе — они недоступны вwebapp2:google.appengine.ext.webapp.blobstore_handlers - Импортируйте поддержку Jinja2 из пакета
webapp2_extras
ДО:
import os
import webapp2
from google.appengine.ext import ndb
from google.appengine.ext.webapp import template
Внесите изменения, указанные в списке выше, заменив текущий раздел импорта в main.py приведенным ниже фрагментом кода.
ПОСЛЕ:
import webapp2
from webapp2_extras import jinja2
from google.appengine.ext import blobstore, ndb
from google.appengine.ext.webapp import blobstore_handlers
После импорта добавьте шаблонный код для поддержки использования Jinja2, как описано в документации webapp2_extras . Следующий фрагмент кода оборачивает стандартный класс обработчика запросов webapp2 функциональностью Jinja2, поэтому добавьте этот блок кода в main.py сразу после импорта:
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))
Добавить поддержку хранилища BLOB-объектов.
В отличие от других миграций в этой серии, где мы сохраняем функциональность или вывод тестового приложения идентичными (или почти идентичными) без (значительных) изменений в пользовательском интерфейсе, этот пример представляет собой более радикальное отступление от нормы. Вместо того чтобы сразу регистрировать новое посещение, а затем отображать десять последних, мы обновляем приложение таким образом, чтобы оно запрашивало у пользователя файл для регистрации посещения. Затем конечные пользователи могут либо загрузить соответствующий файл, либо выбрать «Пропустить», чтобы ничего не загружать вообще. После завершения этого шага отображается страница «последние посещения».
Это изменение позволяет нашему приложению использовать сервис Blobstore для хранения (и, возможно, последующего отображения) изображения или другого типа файла на странице последних посещений.
Обновите модель данных и внедрите ее использование.
Мы добавляем больше данных, а именно обновляем модель данных, чтобы она хранила идентификатор (называемый " BlobKey ") файла, загруженного в Blobstore, и добавляем ссылку для сохранения этого идентификатора в store_visit() . Поскольку эти дополнительные данные возвращаются вместе со всем остальным при запросе, fetch_visits() остается без изменений.
Вот фотографии до и после внесения этих обновлений, касающихся file_blob и свойства ndb.BlobKeyProperty :
ДО:
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)
ПОСЛЕ:
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)
Вот наглядное представление изменений, которые уже были внесены:

Поддержка загрузки файлов
Наиболее существенное изменение функциональности связано с поддержкой загрузки файлов, будь то запрос файла у пользователя, поддержка функции «пропустить» или отображение файла, соответствующего посещению. Все это является частью общей картины. Вот изменения, необходимые для поддержки загрузки файлов:
- Основной
GETзапрос обработчика больше не получает информацию о последних посещениях для отображения. Вместо этого он запрашивает у пользователя возможность загрузки файла. - Когда конечный пользователь отправляет файл для загрузки или пропускает этот процесс,
POSTиз формы передает управление новомуUploadHandler, производному отgoogle.appengine.ext.webapp.blobstore_handlers.BlobstoreUploadHandler. - Метод
POSTклассаUploadHandlerвыполняет загрузку, вызываетstore_visit()для регистрации посещения и запускает HTTP-перенаправление 307, чтобы отправить пользователя обратно на "/", где... - Основной обработчик
POST-запроса (черезfetch_visits()) отображает самые последние посещения. Если пользователь выбирает «пропустить», файл не загружается, но посещение все равно регистрируется, после чего происходит перенаправление. - В разделе «Последние посещения» появилось новое поле, отображаемое пользователю: либо гиперссылка «Просмотр», если доступен файл для загрузки, либо «Нет» в противном случае. Эти изменения реализованы в HTML-шаблоне вместе с добавлением формы загрузки (подробнее об этом позже).
- Если пользователь нажимает на ссылку «просмотреть» при посещении сайта с загруженным видео, он отправляет
GETзапрос к новому объектуViewBlobHandler, производному отgoogle.appengine.ext.webapp.blobstore_handlers.BlobstoreDownloadHandler. В этом случае файл либо отображается (если это изображение, в браузере, если поддерживается), либо предлагается его загрузить, если изображение не найдено, либо возвращается ошибка HTTP 404. - В дополнение к новой паре классов обработчиков, а также новой паре маршрутов для отправки трафика к ним, основному обработчику необходим новый метод
POSTдля приема перенаправления 307, описанного выше.
До этих обновлений приложение Module 0 содержало только основной обработчик с методом GET и единственным маршрутом:
ДО:
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)
После внесения этих изменений теперь имеется три обработчика: 1) обработчик загрузки с методом POST , 2) обработчик загрузки "просмотра BLOB-объекта" с методом GET и 3) основной обработчик с методами GET и POST . Внесите эти изменения, чтобы остальная часть вашего приложения выглядела следующим образом.
ПОСЛЕ:
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)
В этом коде мы только что добавили несколько ключевых вызовов:
- В
MainHandler.getвызывается методblobstore.create_upload_url. Этот вызов генерирует URL-адрес, на который отправляетсяPOSTзапрос из формы, и вызывает обработчик загрузки для отправки файла в Blobstore. - В
UploadHandler.postпроисходит вызов функцииblobstore_handlers.BlobstoreUploadHandler.get_uploads. Именно здесь происходит настоящая магия: файл помещается в Blobstore, и возвращается уникальный и постоянный идентификатор файла — егоBlobKey. - В
ViewBlobHandler.getвызовblobstore_handlers.BlobstoreDownloadHandler.sendс указаниемBlobKeyфайла приводит к загрузке файла и его переадресации в браузер конечного пользователя.
Эти вызовы составляют основную часть обращений к функциям, добавленным в приложение. Вот графическое представление второго и последнего набора изменений в main.py :

Обновить HTML-шаблон
Некоторые обновления основного приложения затрагивают пользовательский интерфейс (UI), поэтому требуются соответствующие изменения в веб-шаблоне, а именно два изменения:
- Для загрузки файла необходима форма с тремя полями ввода: самим файлом и парой кнопок отправки для загрузки файла и пропуска, соответственно.
- Обновите вывод последних посещений, добавив ссылку «просмотреть» для посещений с соответствующей загрузкой файла или ссылку «нет» в остальных случаях.
ДО:
<!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>
Внесите изменения, указанные в списке выше, чтобы создать обновленный шаблон:
ПОСЛЕ:
<!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>
На этом изображении показаны необходимые изменения в index.html :

Последнее изменение заключается в том, что Jinja2 предпочитает размещать свои шаблоны в папке templates , поэтому создайте эту папку и переместите index.html внутрь неё. С этим последним шагом вы завершили все необходимые изменения для добавления использования Blobstore в пример приложения модуля 0.
(необязательно) «Улучшение» облачного хранилища
Хранилище Blobstore в конечном итоге превратилось в само облачное хранилище. Это означает, что загруженные данные из Blobstore видны в консоли облачного хранилища, а именно в браузере облачного хранилища. Вопрос в том, где именно. Ответ — в стандартном сегменте облачного хранилища вашего приложения App Engine. Его имя — это полное доменное имя вашего приложения App Engine, PROJECT_ID .appspot.com . Это очень удобно, потому что все идентификаторы проектов уникальны, не так ли?
Обновления, внесенные в демонстрационное приложение, помещают загруженные файлы в указанный сегмент, но у разработчиков есть возможность выбрать более конкретное местоположение. Доступ к сегменту по умолчанию осуществляется программно через google.appengine.api.app_identity.get_default_gcs_bucket_name() , для чего потребуется новый импорт, если вы хотите получить доступ к этому значению, например, для использования в качестве префикса для организации загруженных файлов. Например, сортировка по типу файла:

Чтобы реализовать нечто подобное, например, для изображений, вам понадобится примерно такой код, а также код, который проверяет типы файлов, чтобы выбрать нужное имя хранилища:
ROOT_BUCKET = app_identity.get_default_gcs_bucket_name()
IMAGE_BUCKET = '%s/%s' % (ROOT_BUCKET, 'images')
Также вам потребуется проверить загружаемые изображения с помощью такого инструмента, как модуль imghdr из стандартной библиотеки Python, чтобы подтвердить тип изображения. Наконец, вероятно, вам захочется ограничить размер загружаемых файлов на случай появления злоумышленников.
Допустим, все это уже сделано. Как нам обновить наше приложение, чтобы оно поддерживало указание места хранения загруженных файлов? Ключевой момент — изменить вызов blobstore.create_upload_url в MainHandler.get , чтобы указать желаемое местоположение в Cloud Storage для загрузки, добавив параметр gs_bucket_name следующим образом:
blobstore.create_upload_url('/upload', gs_bucket_name=IMAGE_BUCKET))
Поскольку это необязательное обновление, позволяющее указать, куда должны сохраняться загружаемые файлы, оно не является частью файла main.py в репозитории. Вместо этого в репозитории доступен альтернативный файл main-gcs.py , который вы можете просмотреть. Вместо использования отдельной папки в хранилище, код в main-gcs.py хранит загружаемые файлы в корневом хранилище ( PROJECT_ID ), как и main.py , но предоставляет необходимую структуру, если вы захотите преобразовать пример во что-то большее .appspot.com как указано в этом разделе. Ниже приведена иллюстрация различий между main.py и main-gcs.py .

6. Подведение итогов/Завершение
В этом разделе завершается выполнение данного практического задания путем развертывания приложения, проверки его корректной работы и отображения всех полученных результатов. После проверки приложения выполните необходимые действия по очистке и обдумайте дальнейшие шаги.
Разверните и проверьте приложение.
Переразверните приложение с помощью gcloud app deploy и убедитесь, что оно работает должным образом, отличаясь по пользовательскому интерфейсу (UX) от приложения Модуля 0. Теперь в вашем приложении два разных экрана, первый из которых — это форма для загрузки файла:
Далее пользователи либо загружают файл и нажимают «Отправить», либо нажимают «Пропустить», чтобы ничего не загружать. В любом случае отображается экран последнего посещения, дополненный ссылками «просмотреть» или «нет» между временными метками посещения и информацией о посетителе:

Поздравляем с завершением этого практического задания по добавлению использования App Engine Blobstore в пример приложения из модуля 0. Ваш код теперь должен соответствовать содержимому папки FINISH (модуль 15) . Альтернативный main-gcs.py также находится в этой папке.
Уборка
Общий
Если на этом пока всё, мы рекомендуем отключить ваше приложение App Engine, чтобы избежать дополнительных расходов. Однако, если вы хотите продолжить тестирование или эксперименты, платформа App Engine предоставляет бесплатную квоту , поэтому, пока вы не превысите этот лимит, с вас не должны взиматься дополнительные платежи. Это касается вычислительных ресурсов, но могут также взиматься плата за соответствующие услуги App Engine, поэтому проверьте страницу с ценами для получения дополнительной информации. Если эта миграция включает другие облачные сервисы, они оплачиваются отдельно. В любом случае, если применимо, см. раздел «Информация, относящаяся к этому практическому занятию» ниже.
Для полной ясности, развертывание на бессерверной вычислительной платформе Google Cloud, такой как App Engine, влечет за собой незначительные затраты на сборку и хранение . Cloud Build и Cloud Storage имеют собственную бесплатную квоту. Хранение образа использует часть этой квоты. Однако вы можете проживать в регионе, где нет такого бесплатного уровня, поэтому следите за использованием хранилища, чтобы минимизировать потенциальные затраты. К числу конкретных «папок» Cloud Storage, которые следует проверить, относятся:
-
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 - Приведенные выше ссылки на хранилища зависят от вашего
PROJECT_IDи *LOC*, например, "us", если ваше приложение размещено в США.
С другой стороны, если вы не собираетесь продолжать работу над этим приложением или другими связанными с миграцией кодовыми руководствами и хотите полностью удалить все, закройте свой проект .
Это относится именно к данному практическому занятию.
Перечисленные ниже услуги являются уникальными для данной учебной лаборатории. Для получения более подробной информации обратитесь к документации по каждому продукту:
- Сервис App Engine Blobstore подпадает под действие квот и ограничений на хранимые данные , поэтому ознакомьтесь с этим разделом, а также со страницей цен на устаревшие пакетные сервисы .
- Сервис App Engine Datastore предоставляется компанией Cloud Datastore (Cloud Firestore в режиме Datastore), которая также предлагает бесплатный тариф; подробную информацию можно найти на странице с ценами .
Следующие шаги
Следующий логичный вариант миграции рассматривается в Модуле 16, где показано, как разработчики могут перейти с сервиса App Engine Blobstore на использование клиентской библиотеки Cloud Storage. Преимущества перехода включают доступ к большему количеству функций Cloud Storage, а также знакомство с клиентской библиотекой, работающей для приложений вне App Engine, будь то в Google Cloud, других облаках или даже локально. Если вам не нужны все функции Cloud Storage или вас беспокоит влияние на стоимость, вы можете остаться на App Engine Blobstore.
Помимо модуля 16, существует множество других возможных вариантов миграции, таких как Cloud NDB и Cloud Datastore, Cloud Tasks или Cloud Memorystore. Также доступны миграции между продуктами Cloud Run и Cloud Functions. В репозитории миграции представлены все примеры кода, ссылки на все доступные практические занятия и видеоуроки, а также рекомендации по выбору подходящих вариантов миграции и соответствующему «порядку» миграции.
7. Дополнительные ресурсы
Вопросы/отзывы по Codelab
Если вы обнаружите какие-либо проблемы в этом практическом задании, пожалуйста, сначала найдите свою проблему, прежде чем сообщать о ней. Ссылки для поиска и создания новых проблем:
Миграционные ресурсы
Ссылки на папки репозитория для Модуля 0 (НАЧАЛО) и Модуля 15 (ЗАВЕРШЕНИЕ) можно найти в таблице ниже. К ним также можно получить доступ из репозитория для всех миграций кода App Engine, которые можно клонировать или загрузить в виде ZIP-файла.
Кодлаб | Python 2 | Python 3 |
Модуль 0 | Н/Д | |
Модуль 15 (данная практическая работа) | Н/Д |
Онлайн-ресурсы
Ниже приведены онлайн-ресурсы, которые могут быть полезны для данного урока:
App Engine
- Сервис App Engine Blobstore
- Квоты и лимиты на хранимые данные в App Engine
- Документация App Engine
- Среда выполнения Python 2 App Engine (стандартная среда)
- Использование встроенных библиотек App Engine в Python 2 App Engine
- Информация о ценах и квотах App Engine
- Запуск платформы App Engine второго поколения (2018)
- Сравнение платформ первого и второго поколений
- Долгосрочная поддержка устаревших сред выполнения.
- Примеры миграции документации в репозитории
- Репозиторий с примерами миграции, предоставленными сообществом.
Google Облако
- Python на платформе Google Cloud
- Клиентские библиотеки Python от Google Cloud
- Уровень Google Cloud «Всегда бесплатно»
- Google Cloud SDK (инструмент командной строки gcloud)
- Вся документация Google Cloud
Python
- Системы шаблонизации Django и Jinja2
- веб-фреймворк
webapp2 - документация
webapp2 -
webapp2_extraslinks - Документация Jinja2
webapp2_extras
Видео
- Станция миграции бессерверных приложений
- Бессерверные экспедиции
- Подпишитесь на Google Cloud Tech
- Подпишитесь на Google Developers
Лицензия
Данная работа распространяется под лицензией Creative Commons Attribution 2.0 Generic.