NVault

Автоматизация публикации приложений в Google Play

И действительно, это хоть и не сложное, но рутинное занятие может быть автоматизированно, благо Google предоставляет Google Play Developer Publishing API, позволяющее публиковать и обновлять приложения с помощью ваших собственных скриптов, применять автоматические настройки и автоматизированно внедрять новые версии.

С чего начать?

Начнём с официального референса от Google: Android Publisher API-Ref

Там даётся несколько таблиц с описанием методов API.

Нас будут интересовать вот эти:

НазваниеМетодHTTP-requestОписание
commitPOST/packageName/edits/editId:commitЗакрепляет совершённые изменения
deleteDELETE/packageName/edits/editIdУдаляет изменения
getGET/packageName/edits/editIdВозвращает данные об изменении
insertPOST/packageName/editsСоздаёт новое изменения
validatePOST/packageName/edits/editId:validateПроверяет, возможно ли закрепить текущие изменения
addexternallyhostedPOST/packageName/edits/editId/apks/externallyHostedПозволяет загружать apk из внешнего источника
listGET/packageName/edits/editId/apksПеречисляет все apk для данного edit’а
uploadPOST/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

Вы увидите это или нечто подобное:

screenshot_2018-07-15-dostup-k-api-google-play-console

Жмём на кнопку СВЯЗАТЬ, ждём обновления данных, листаем страницу вниз до плашки Аккаунты приложений и жмём там кнопку СОЗДАТЬ АККАУНТ ПРИЛОЖЕНИЯ.

cappakk

Нам предлагают перейти в Google Cloud Console, что мы и делаем. Уже в консоли жмём кнопку СОЗДАТЬ СЕРВИСНЫЙ АККАУНТ. Её может быть не видно, т.к. она иногда скрывается в меню. Чтобы раскрыть это меню, нажмите на три точки рядом с кнопкой ПОКАЗАТЬ ИНФОРМАЦИОННУЮ ПАНЕЛЬ.

Появится такое окошко:

creatscr

Заполняйте первые два поля по вашему усмотрению, а вот с третьим следует быть осторожнее. Я выбрал роль Владелец, т.к. она самая удобная и имеет право на всё и не надо заморачиваться, просто берёшь и делаешь нужные вещи.

Важно

Если вы работаете один, то такой подход вполне допустим. Однако, следует быть осторожным, т.к. если злоумышленник получит доступ к закрытому ключу, то у него будут полные права доступа к вашей консоли.

Далее нам надо создать закрытый ключ. Мы будем использовать JSON-формат ключа, т.к. он самый удобный.

creatkey

Важно

На этом этапе желательно использовать браузер Google Chrome. Я сам долго мучился и не понимал причину, по которой у меня не скачивался ключ. Как оказалось, дело было в браузере.

После всех действий страница сервисных аккаунтов должна приобрести подобный вид:

srvaccpage

Если всё в порядке, возвращаемся на вкладку GooglePlay Console и жмём кнопку ГОТОВО. Страница обновляется. На плашке с сервисными аккаунтами появился наш аккаунт. Нам нужно ещё раз выдать права. Жмём кнопку ОТКРЫТЬ ДОСТУП.

Страница ещё раз перезагрузится и появится новое окно:

uandperms

В выпадающем списке Роли вы можете выбрать роль для этого аккаунта, либо самому выставить нужные параметры в списке ниже(если к выбранным параметрам нет подходящей роли, то у аккаунта будет Специальная роль).

Жмём кнопку Добавить и всё, можно переходить к кодингу.

Начнём кодить

Целевым ЯП будет 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:

gui

На этом всё. У нас есть замечательное приложение, которое упрощает публикацию обновлений приложений в GooglePlay. Есть даже вариант с GUI, если оно кому-то вообще нужно.

Листинги кодов

Чистый код

Скачать: main_clear.py

Код с argparse

Скачать: main_argparse.py

Код с Gooey

Скачать: main_gooey.py