Автоматизация публикации приложений в Google Play
И действительно, это хоть и не сложное, но рутинное занятие может быть автоматизированно, благо Google предоставляет Google Play Developer Publishing API, позволяющее публиковать и обновлять приложения с помощью ваших собственных скриптов, применять автоматические настройки и автоматизированно внедрять новые версии.
С чего начать?
Начнём с официального референса от Google: Android Publisher API-Ref
Там даётся несколько таблиц с описанием методов API.
Нас будут интересовать вот эти:
Название | Метод | HTTP-request | Описание |
---|---|---|---|
commit | POST | /packageName/edits/editId:commit | Закрепляет совершённые изменения |
delete | DELETE | /packageName/edits/editId | Удаляет изменения |
get | GET | /packageName/edits/editId | Возвращает данные об изменении |
insert | POST | /packageName/edits | Создаёт новое изменения |
validate | POST | /packageName/edits/editId:validate | Проверяет, возможно ли закрепить текущие изменения |
addexternallyhosted | POST | /packageName/edits/editId/apks/externallyHosted | Позволяет загружать apk из внешнего источника |
list | GET | /packageName/edits/editId/apks | Перечисляет все apk для данного edit’а |
upload | POST | /packageName/edits/editId/apks | Загружает apk на сервера Google |
API-Endpoint:
https://www.googleapis.com/androidpublisher/v3/applications
Все запросы выполняются относительно endpoint’а, если не указано другое.
Метод
upload
использует другой endpoint:https://www.googleapis.com/upload/androidpublisher/v3/applications
Во всех запросах
packageName
иeditId
являются соответственно packageName’ом вашего приложения и ID текущего изменения.
Подготовка материалов
Для начала работы нужно авторизоваться в GooglePlay Console и далее пройти по такому пути:
GooglePlay Console > Настройки > Аккаунт разработчика > Доступ к API
Вы увидите это или нечто подобное:
Жмём на кнопку СВЯЗАТЬ
, ждём обновления данных, листаем страницу вниз до плашки Аккаунты приложений
и жмём там кнопку СОЗДАТЬ АККАУНТ ПРИЛОЖЕНИЯ
.
Нам предлагают перейти в Google Cloud Console, что мы и делаем. Уже в консоли жмём кнопку СОЗДАТЬ СЕРВИСНЫЙ АККАУНТ
. Её может быть не видно, т.к. она иногда скрывается в меню. Чтобы раскрыть это меню, нажмите на три точки рядом с кнопкой ПОКАЗАТЬ ИНФОРМАЦИОННУЮ ПАНЕЛЬ
.
Появится такое окошко:
Заполняйте первые два поля по вашему усмотрению, а вот с третьим следует быть осторожнее. Я выбрал роль Владелец
, т.к. она самая удобная и имеет право на всё и не надо заморачиваться, просто берёшь и делаешь нужные вещи.
Важно
Если вы работаете один, то такой подход вполне допустим. Однако, следует быть осторожным, т.к. если злоумышленник получит доступ к закрытому ключу, то у него будут полные права доступа к вашей консоли.
Далее нам надо создать закрытый ключ. Мы будем использовать JSON-формат ключа, т.к. он самый удобный.
Важно
На этом этапе желательно использовать браузер Google Chrome. Я сам долго мучился и не понимал причину, по которой у меня не скачивался ключ. Как оказалось, дело было в браузере.
После всех действий страница сервисных аккаунтов должна приобрести подобный вид:
Если всё в порядке, возвращаемся на вкладку GooglePlay Console и жмём кнопку ГОТОВО
. Страница обновляется. На плашке с сервисными аккаунтами появился наш аккаунт. Нам нужно ещё раз выдать права. Жмём кнопку ОТКРЫТЬ ДОСТУП
.
Страница ещё раз перезагрузится и появится новое окно:
В выпадающем списке Роли
вы можете выбрать роль для этого аккаунта, либо самому выставить нужные параметры в списке ниже(если к выбранным параметрам нет подходящей роли, то у аккаунта будет Специальная роль).
Жмём кнопку Добавить
и всё, можно переходить к кодингу.
Начнём кодить
Целевым ЯП будет Python 3.6.2
, писать код будем в PyCharm CE 2018.1.4
.
Нам понадобятся библиотека google-api-python-client
pip install google-api-python-client
Открываем PyCharm и создаём новый проект. Когда всё инициализировалось, создаём файл __main__.py
.
Также, в папку проекта нужно переместить JSON-ключ, который мы получили при создании сервисного аккаунта.
Импорты
# file: __main__.py
import httplib2
from oauth2client.service_account import ServiceAccountCredentials
from oauth2client.client import AccessTokenRefreshError
from googleapiclient.discovery import build
Инициализируем глобальные переменные
# file: __main__.py
# Может принимать значения "alpha", "beta", "production" или "rollout"
TRACK = "alpha"
package_name = "your.package.name"
key_filename = "your_key_filename.json"
scope = ['https://www.googleapis.com/auth/androidpublisher']
Определяем точку входа приложения и основную функцию
# file: __main__.py
def main():
return
if __name__ == '__main__':
main()
Определяем функцию авторизации и создания сервиса
# file: __main__.py
def build_androidpublisher_service():
creds = ServiceAccountCredentials.from_json_keyfile_name(key_filename, scope)
http_auth = creds.authorize(http=httplib2.Http())
return build('androidpublisher', 'v3', http=http_auth)
Определяем функцию создания edit’а
# file: __main__.py
def generate_edit(service):
edit_request = service.edits().insert(body={}, packageName=package_name)
result = edit_request.execute()
return result['id']
Давайте проверим работоспособность кода и попробуем получить список загруженных apk-файлов
# file: __main__.py
# scope: main()
try:
service = build_androidpublisher_service()
edit_id = generate_edit(service)
apks_result = service.edits().apks().list(editId=edit_id, packageName=package_name).execute()
for apk in apks_result['apks']:
print(apk)
except AccessTokenRefreshError:
print("The credentials have been revoked or expired, re-run the app to re-authorize")
Данный код выдаст вам нечто подобное:
{
"versionCode": 1,
"binary": { "sha1": "cce8282be8ad64ebe0af6a171c27ed55936f4ff5", "sha256": "10fa10b03d6ca51c34da59f7f78800b2250f3d3463be0501a5a3117ffce3aeed" }
}
Внимание
Данный код может вернуть пустой список, если ещё не было загружено ни одного apk-файла.
Тем не менее мы видим, что всё работает. Поэтому мы приступаем к нашей основной задаче.
# file: __main__.py
# scope: main()
try:
service = build_androidpublisher_service()
edit_id = generate_edit(service)
# Загружаем apk на сервера google
apk_response = service.edits().apks().upload(
editId=edit_id, packageName=package_name, media_body=apk_file
).execute()
# Выводим код версии, как подтверждение успешной загрузки
print("Version code {} has been uploaded".format(apk_response['versionCode']))
# Теперь надо установить Track, чтобы apk задеплоились в правильную ветку
track_response = service.edits().tracks().update(
editId=edit_id, packageName=package_name, track=TRACK,
body={"versionCodes": [apk_response['versionCode']]}
).execute()
# Выводим сообщение об успешной установке Track'а для VC
print("Track {} is set for version code(s) {}".format(
track_response['track'], str(track_response['versionCodes'])
))
# В конце надо закрепить edit
commit_request = service.edits().commit(editId=edit_id, packageName=package_name).execute()
# И вывести сообщение об этом
print("Edit \"{}\" has been committed".format(commit_request['id']))
except AccessTokenRefreshError:
print("The credentials have been revoked or expired, re-run the app to re-authorize")
Полностью код теперь выглядит так:
import httplib2
from googleapiclient.discovery import build
from oauth2client.service_account import ServiceAccountCredentials
from oauth2client.client import AccessTokenRefreshError
# Может принимать значения "alpha", "beta", "production" или "rollout"
TRACK = "alpha"
package_name = "your.package.name"
key_filename = "your_key_filename.json"
scope = ['https://www.googleapis.com/auth/androidpublisher']
def build_androidpublisher_service():
creds = ServiceAccountCredentials.from_json_keyfile_name(key_filename, scope)
http_auth = creds.authorize(http=httplib2.Http())
return build('androidpublisher', 'v3', http=http_auth)
def generate_edit(service):
edit_request = service.edits().insert(body={}, packageName=package_name)
result = edit_request.execute()
return result['id']
def main():
try:
service = build_androidpublisher_service()
edit_id = generate_edit(service)
# Загружаем apk на сервера google
apk_response = service.edits().apks().upload(
editId=edit_id, packageName=package_name, media_body=apk_file
).execute()
# Выводим код версии, как подтверждение успешной загрузки
print("Version code {} has been uploaded".format(apk_response['versionCode']))
# Теперь надо установить Track, чтобы apk задеплоились в правильную ветку
track_response = service.edits().tracks().update(
editId=edit_id, packageName=package_name, track=TRACK,
body={"versionCodes": [apk_response['versionCode']]}
).execute()
# Выводим сообщение об успешной установке Track'а для VC
print("Track {} is set for version code(s) {}".format(
track_response['track'], str(track_response['versionCodes'])
))
# В конце надо закрепить edit
commit_request = service.edits().commit(editId=edit_id, packageName=package_name).execute()
# И вывести сообщение об этом
print("Edit \"{}\" has been committed".format(commit_request['id']))
except AccessTokenRefreshError:
print("The credentials have been revoked or expired, re-run the app to re-authorize")
if __name__ == '__main__':
main()
Добавим немного argparse’а
import argparse
...
# Может принимать значения "alpha", "beta", "production" или "rollout"
TRACK = "alpha"
scope = ["https://www.googleapis.com/auth/androidpublisher"]
# Установим параметры командной строки
argparser = argparse.ArgumentParser(add_help=False)
argparser.add_argument("package_name", help="The package name. (e.g. com.android.sample)")
argparser.add_argument("apk_file", help="The path to the APK file.")
argparser.add_argument("key_file", default="key.json", help="The path to the json key file.")
...
def main():
args = argparser.parse_args()
package_name, apk_file, key_file = args.package_name, args.apk_file, args.key_file
try:
service = build_androidpublisher_service(key_file)
edit_id = generate_edit(service, package_name)
...
Модуль argparse может быть не установлен. Решается с помощью установки из PyPi:
pip install argparse
.
А как насёт GUI? Да без проблем! Установим Gooey
pip install Gooey
И модифицируем наш код:
from gooey import Gooey, GooeyParser
import httplib2
from googleapiclient.discovery import build
from oauth2client.service_account import ServiceAccountCredentials
from oauth2client.client import AccessTokenRefreshError
scope = ["https://www.googleapis.com/auth/androidpublisher"]
track_choices = ["alpha", "beta", "production", "rollout"]
# Установим параметры Gooey
parser = GooeyParser()
parser.add_argument(
"package_name",
metavar="Package name",
help="The package name. (e.g. com.android.sample)"
)
parser.add_argument(
"apk_file",
metavar="Apk file",
help="The path to the APK file.",
widget="FileChooser"
)
parser.add_argument(
"key_file",
metavar="Key file",
help="The path to the json key file.",
default="./key.json",
widget="FileChooser"
)
parser.add_argument(
"track",
metavar="Key file",
choices=track_choices,
help="Publish branch. May be \"alpha\", \"beta\", \"production\" or \"rollout\".",
default="alpha",
)
def build_androidpublisher_service(key_filename):
...
def generate_edit(service, package_name):
...
@Gooey(
program_name="GooglePlay Publisher",
program_description="Publish updates for your apps in easy way!",
required_cols=1, optional_cols=1
)
def main():
args = parser.parse_args()
package_name, apk_file, key_file = args.package_name, args.apk_file, args.key_file # type: str
TRACK = args.track # type: str
good_key, good_apk = key_file.endswith(".json"), apk_file.endswith(".apk")
if not good_key or not good_apk:
_wt = "key" if not good_key else "apk"
_ft = "json" if not good_key else "apk"
raise Exception(f"Your {_wt} is invalid. It should be .{_ft} file")
if TRACK not in track_choices:
raise Exception(f"Wrong branch chosen")
try:
...
except AccessTokenRefreshError:
raise Exception("The credentials have been revoked or expired, re-run the app to re-authorize")
except Exception as e:
raise Exception(str(e.args))
if __name__ == '__main__':
try:
main()
except Exception as e:
print(e)
raise Exception(f"Error occured! E: {e.args}")
И если теперь запустить проект, то у вместо CLI у нас будет замечательный GUI:
На этом всё. У нас есть замечательное приложение, которое упрощает публикацию обновлений приложений в GooglePlay. Есть даже вариант с GUI, если оно кому-то вообще нужно.
Листинги кодов
Чистый код
Скачать: main_clear.py
Код с argparse
Скачать: main_argparse.py
Код с Gooey
Скачать: main_gooey.py