UnityCloudBuild API をPythonから利用してみた

[Unity Cloud Build](https://build.cloud.unity3d.com/)のAPIをPythonから利用してみました。他のシステムと連携してビルドしたファイルをアップロードする際に利用できるかなーと思います。

# Unity Cloud Buildを利用してみた
BitBucket(旧:Stash)のソースコードからビルドできる点もポイントが高かったです。初期ビルドの設定はボタンぽちぽちで5分くらいだったので説明省略しています。

初回は手動でビルドして2回目以降をAPI経由で利用するといった用途を想定しています。[今回利用するAPIの詳細は全て公式ドキュメントに記載されています。](https://build-api.cloud.unity3d.com/docs/1.0.0/index.html?_ga=1.162227839.2109684341.1463119148#intro)

## 1. ビルド実行
ビルドに失敗するとHTTPStatus:202が返ってきます。ビルド完了したファイルのURLは別ファイル

```python:u.py
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals

import requests

API_TOKEN = "****"
ORGID = "****"
PROJECTS = "****"
BUILD_TARGETS = "****"


def get_url(func_name, extend=None):
    auth_base = "orgs/{orgid}/projects/{projectid}/buildtargets/{buildtargetid}".format(orgid=ORGID,
                                                                                        projectid=PROJECTS,
                                                                                        buildtargetid=BUILD_TARGETS)
    if extend:
        url_base = "https://build-api.cloud.unity3d.com/api/v1/{auth}/{func}{extend}"
        return url_base.format(auth=auth_base, func=func_name, extend=extend)

    url_base = "https://build-api.cloud.unity3d.com/api/v1/{auth}/{func}"
    return url_base.format(auth=auth_base, func=func_name)


def get_headers():
    return {
        "Content-Type": "application/json",
        "Authorization": "Basic {token}".format(token=API_TOKEN),
    }


def execute_build():
    """
    ビルド実行

    # 準正常系 既にビルド中
    [{"buildtargetid":"****","buildTargetName":"****","error":"Cannot start build - already a build pending."}] 202
    """
    response = requests.post(get_url("builds"), headers=get_headers(), data={})
    print(response.text), response.status_code
    return

# ビルド実行
execute_build()

```


## 2. ビルドステータス取得
ビルドステータスは次の通り { queued , sentToBuilder , started , restarted , success , failure , canceled , unknown }
無課金だとqueuedの時間が長いです。ビルド成功したらsuccessとなります

```python:u.py

def get_builds():
    """
    最新の25件のビルド情報が取得できる。
    それより古いデータ取得したい時はper_pageとpageパラメータを修正すること
    """
    response = requests.get(get_url("builds"), headers=get_headers())
    print(response.text), response.status_code
    return


def get_build(number):
    """
    指定したビルド情報が取得できる

    # 準正常系
    存在しない場合は404 : {"error":"The requested object was not found."}

    :param number: int
    """
    response = requests.get(get_url("builds", extend="/" + str(number)), headers=get_headers())
    print(response.text), response.status_code
    return


def get_latest_build():
    """
    最新1件のビルドが取得できる
    """
    build_start_result = requests.get(get_url("builds", extend="?per_page=1&page=1"), headers=get_headers())
    print(build_start_result.text), build_start_result.status_code
    return

# 特定ビルド取得
get_build(6)

# 最新ビルド取得
get_latest_build()

# ビルド一括取得
get_build_status()
```

## 3. ビルドキャンセル
実装してから、APIからキャンセルしたいことなんて、あるのだろうか？ と思いました。
HTMLのDELETEクエリーでキャンセルする仕様です。DELETEクエリーは今年初の利用。REST原則に忠実でAPI作ったエンジニアの性格が、なんとなく伝わりました。

```python:u.py

def cancel_all():
    """
    全てのビルドをキャンセルする
    """
    response = requests.delete(get_url("builds"), headers=get_headers())
    print(response.text), response.status_code


def cancel(number):
    """
    特定ビルドをキャンセルする

    # 正常系 キャンセル成功
    204

    # 準正常系 存在しない
    {"error":"The requested object was not found."} 404

    # 準正常系 ビルド完了済みのビルドを指定したとき
    204

    :param number: int
    """
    response = requests.delete(get_url("builds", extend="/" + str(number)), headers=get_headers())
    print(response.text), response.status_code


# 特定ビルドキャンセル
cancel(7)

# 全ビルドキャンセル
cancel_all()

```


## 4. ダウンロードURL取得
ビルド成功したらS3にファイルがアップロードされます。ダウンロードするためのURLを取得するための関数です。

```python:u.py

def get_latest_build_download_urls():
    """
    最新の成功したビルドのダウンロードURL

    :return: str
    """
    response = requests.get(get_url("builds", extend="?buildStatus=success&per_page=1&page=1"),
                                      headers=get_headers())
    data = ujson.loads(response.text)
    assert len(data) > 0
    return data[0].get('links').get('download_primary').get('href')

# 最新の成功したビルドのダウンロードURL
print get_latest_build_download_urls()

```


# 全部まとめた完成品のソース

```python:u.py
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals

import requests
import ujson

API_TOKEN = "****"
ORGID = "****"
PROJECTS = "****"
BUILD_TARGETS = "****"


def get_url(func_name, extend=None):
    auth_base = "orgs/{orgid}/projects/{projectid}/buildtargets/{buildtargetid}".format(orgid=ORGID,
                                                                                        projectid=PROJECTS,
                                                                                        buildtargetid=BUILD_TARGETS)
    if extend:
        url_base = "https://build-api.cloud.unity3d.com/api/v1/{auth}/{func}{extend}"
        return url_base.format(auth=auth_base, func=func_name, extend=extend)

    url_base = "https://build-api.cloud.unity3d.com/api/v1/{auth}/{func}"
    return url_base.format(auth=auth_base, func=func_name)


def get_headers():
    return {
        "Content-Type": "application/json",
        "Authorization": "Basic {token}".format(token=API_TOKEN),
    }


def execute_build():
    """
    ビルド実行

    # 準正常系 既にビルド中
    [{"buildtargetid":"****","buildTargetName":"****","error":"Cannot start build - already a build pending."}] 202
    """
    response = requests.post(get_url("builds"), headers=get_headers(), data={})
    print(response.text), response.status_code
    return


def get_builds():
    """
    最新の25件のビルド情報が取得できる。
    それより古いデータ取得したい時はper_pageとpageパラメータを修正すること
    """
    response = requests.get(get_url("builds"), headers=get_headers())
    print(response.text), response.status_code
    return


def get_build(number):
    """
    指定したビルド情報が取得できる

    # 準正常系
    存在しない場合は404 : {"error":"The requested object was not found."}

    :param number: int
    """
    response = requests.get(get_url("builds", extend="/" + str(number)), headers=get_headers())
    print(response.text), response.status_code
    return


def get_latest_build():
    """
    最新1件のビルドが取得できる
    """
    build_start_result = requests.get(get_url("builds", extend="?per_page=1&page=1"), headers=get_headers())
    print(build_start_result.text), build_start_result.status_code
    return


def get_latest_build_download_urls():
    """
    最新の成功したビルドのダウンロードURL

    :return: str
    """
    response = requests.get(get_url("builds", extend="?buildStatus=success&per_page=1&page=1"),
                                      headers=get_headers())
    data = ujson.loads(response.text)
    assert len(data) > 0
    return data[0].get('links').get('download_primary').get('href')


def cancel_all():
    """
    全てのビルドをキャンセルする
    """
    response = requests.delete(get_url("builds"), headers=get_headers())
    print(response.text), response.status_code


def cancel(number):
    """
    特定ビルドをキャンセルする

    # 正常系 キャンセル成功
    204

    # 準正常系 存在しない
    {"error":"The requested object was not found."} 404

    # 準正常系 ビルド完了済みのビルドを指定したとき
    204

    :param number: int
    """
    response = requests.delete(get_url("builds", extend="/" + str(number)), headers=get_headers())
    print(response.text), response.status_code


# 特定ビルドキャンセル
# cancel(7)

# 全ビルドキャンセル
# cancel_all()

# 特定ビルド取得
# get_build(6)

# 最新ビルド取得
# get_latest_build()

# ビルド一括取得
# get_build_status()

# ビルド実行
# execute_build()

# 最新の成功したビルドのダウンロードURL
print get_latest_build_download_urls()

```