Python 샘플: 게임 옵션과 예고편이 포함된 앱 제출

이 문서는 이런 작업에 Microsoft Store 제출 API를 사용하는 방법을 설명하는 Python 코드 예제를 제공합니다.

  • Microsoft Store 제출 API와 함께 사용할 Azure AD 액세스 토큰을 가져옵니다.
  • 앱 제출 만들기
  • 게임예고편 고급 목록 옵션을 포함하여 앱 제출용 스토어 목록 데이터를 구성합니다.
  • 앱 제출을 위한 패키지, 목록 이미지, 예고편 파일 등이 포함된 ZIP 파일을 업로드합니다.
  • 앱 제출을 커밋합니다.

앱 제출 만들기

이 코드는 Microsoft Store 제출 API를 사용하여 게임 옵션과 예고편이 포함된 앱 제출을 만들어 커밋하는 다른 예제 클래스와 함수를 호출합니다. 이 코드를 용도에 맞게 조정하려면 다음을 수행합니다.

import time

from devcenterclient import DevCenterClient, DevCenterAccessTokenClient
import submissiondatasamples as samples

# Add your tenant ID, client ID, and client secret here.
tenant = ""
client = ""
secret = ""
acc_token_client = DevCenterAccessTokenClient(tenant, client, secret)

acc_token = acc_token_client.get_access_token("https://manage.devcenter.microsoft.com")
dev_center = DevCenterClient("manage.devcenter.microsoft.com", acc_token)

# The application ID is taken from your app dashboard page's URI in Dev Center,
# e.g. https://developer.microsoft.com/en-us/dashboard/apps/{application_id}/
application_id = ""

# Get the application object, and cancel any in progress submissions.
is_ok, app = dev_center.get_application(application_id)
assert is_ok

if "pendingApplicationSubmission" in app:
    in_progress_submission_id = app["pendingApplicationSubmission"]["id"]
    is_ok = dev_center.cancel_in_progress_submission(application_id, in_progress_submission_id)
    assert is_ok

# Create a new submission, based on the last published submission.
is_ok, submission = dev_center.create_submission(application_id)
assert is_ok
submission_id = submission["id"]

# The following fields are required:
submission["applicationCategory"] = "Games_Fighting"
submission["listings"] = samples.get_listings_object()
submission["Pricing"] = samples.get_pricing_object()
submission["packages"] = [samples.get_package_object()]
submission["allowTargetFutureDeviceFamilies"] = samples.get_device_families_object()

# The app must have the hasAdvancedListingPermission set to True in order for gaming options
# and trailers to be applied. If that's not the case, you can still update the app and
# its submissions through the API, but gaming options and trailers won't be saved.
if not "hasAdvancedListingPermission" in app or not app["hasAdvancedListingPermission"]:
    print("This application does not support gaming options or trailers.")
else:
    submission["gamingOptions"] = [samples.get_gaming_options_object()]
    submission["trailers"] = [samples.get_trailer_object()]

# Continue updating the submission_json object with additional options as needed.
# After you've finished, call the Update API with the code below to save it:
is_ok, submission = dev_center.update_submission(application_id, submission_id, submission)
assert is_ok

# All images and packages should be located in a single ZIP file. In the submission JSON, 
# the file names for all objects requiring them (icons, packages, etc.) must exactly 
# match the file names from the ZIP file.
zip_file_path = ""
is_ok = dev_center.upload_zip_file_for_submission(application_id, submission_id, zip_file_path)
assert is_ok

# Committing the submission will start the submission process for it. Once committed,
# the submission can no longer be changed.
is_ok = dev_center.commit_submission(application_id, submission_id)
assert is_ok

# After committing, you can poll the commit API for the status of the submission's process using
# the following code.
waiting_for_commit_start = True
while waiting_for_commit_start:
    is_ok, submission_status = dev_center.get_submission_status(application_id, submission_id)
    assert is_ok
    waiting_for_commit_start = submission_status == "CommitStarted"
    if waiting_for_commit_start:
        time.sleep(60)

Azure AD 액세스 토큰을 가져와 제출 API를 호출

다음 예제에서는 다음 클래스를 정의합니다.

  • DevCenterAccessTokenClient 클래스는 tenantId, clientIdclientSecret 값을 사용하여 Microsoft Store 제출 API에 사용할 Azure AD 액세스 토큰을 생성하는 도우미 메서드를 정의합니다.
  • DevCenterClient 클래스는 Microsoft Store 제출 API의 다양한 메서드를 호출하고, 앱 제출을 위해 패키지, 목록 이미지, 예고편 파일이 포함된 ZIP 파일을 업로드하는 도우미 메서드를 정의합니다.
import http.client
import json
import requests

class DevCenterAccessTokenClient(object):
    """A client for acquiring access tokens from AAD to use with the Dev Center Client."""
    def __init__(self, tenant_id, client_id, client_secret):
        self.tenant_id = tenant_id
        self.client_id = client_id
        self.client_secret = client_secret

    def get_access_token(self, resource):
        """Acquires an access token to the specific resource via the AAD tenant."""
        body_format = "grant_type=client_credentials&client_id={0}&client_secret={1}&resource={2}"
        body = body_format.format(self.client_id, self.client_secret, resource)
        access_headers = {"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"}
        token_conn = http.client.HTTPSConnection("login.microsoftonline.com")
        token_relative_path = "/{0}/oauth2/token".format(self.tenant_id)
        token_conn.request("POST", token_relative_path, body, headers=access_headers)

        token_response = token_conn.getresponse()
        token_json = json.loads(token_response.read().decode())
        token_conn.close()
        return token_json["access_token"]

class DevCenterClient(object):
    """A client for the Dev Center API."""
    def __init__(self, base_uri, access_token):
        self.base_uri = base_uri
        self.request_headers = {
            "Authorization": "Bearer " + access_token,
            "Content-type": "application/json",
            "User-Agent": "Python"
        }

    def get_application(self, application_id):
        """Returns the application as defined in Dev Center."""
        path = "/v1.0/my/applications/{0}".format(application_id)
        return self._get(path)

    def cancel_in_progress_submission(self, application_id, submission_id):
        """Cancels the in-progress submission."""
        path = "/v1.0/my/applications/{0}/submissions/{1}".format(application_id, submission_id)
        return self._delete(path)

    def create_submission(self, application_id):
        """Creates a new submission in Dev Center. This is identical to clicking
        the Create Submission button in Dev Center."""
        path = "/v1.0/my/applications/{0}/submissions".format(application_id)
        return self._post(path)

    def update_submission(self, application_id, submission_id, submission):
        """Updates the submission in Dev Center using the JSON provided."""
        path = "/v1.0/my/applications/{0}/submissions/{1}"
        path = path.format(application_id, submission_id)
        return self._put(path, submission)
    
    def get_submission(self, application_id, submission_id):
        """Gets the submission in Dev Center."""
        path = "/v1.0/my/applications/{0}/submissions/{1}"
        path = path.format(application_id, submission_id)
        return self._get(path)

    def commit_submission(self, application_id, submission_id):
        """Commits the submission to Dev Center. Once committed, Dev Center will
        begin processing the submission and verify package integrity and send
        it for certification."""
        path = "/v1.0/my/applications/{0}/submissions/{1}/commit"
        path = path.format(application_id, submission_id)
        return self._post(path)

    def get_submission_status(self, application_id, submission_id):
        """Returns the current state of the submission in Dev Center,
        such as is the submission in certification, committed, publishing,
        etc."""
        path = "/v1.0/my/applications/{0}/submissions/{1}/status"
        path = path.format(application_id, submission_id)
        response_ok, response_obj = self._get(path)
        if "status" in response_obj:
            return (response_ok, response_obj["status"])
        else:
            return (response_ok, "Unknown")

    def upload_zip_file_for_submission(self, application_id, submission_id, zip_file_path):
        """Uploads a ZIP file for the Submission API for the submission object."""
        is_ok, submission = self.get_submission(application_id, submission_id)
        if not is_ok:
            raise "Failed to get submission."

        zip_file = open(zip_file_path, 'rb')
        upload_uri = submission["fileUploadUrl"].replace("+", "%2B")
        upload_headers = {"x-ms-blob-type": "BlockBlob"}
        upload_response = requests.put(upload_uri, zip_file, headers=upload_headers)
        upload_response.raise_for_status()

    def _get(self, path):
        return self._invoke("GET", path)

    def _post(self, path, obj=None):
        return self._invoke("POST", path, obj)

    def _put(self, path, obj=None):
        return self._invoke("PUT", path, obj)

    def _delete(self, path):
        return self._invoke("DELETE", path)

    def _invoke(self, method, path, obj=None):
        body = ""
        if not obj is None:
            body = json.dumps(obj)
        conn = http.client.HTTPSConnection(self.base_uri)
        conn.request(method, path, body, self.request_headers)
        response = conn.getresponse()
        response_body = response.read().decode()
        response_body_length = int(response.headers["Content-Length"])
        response_obj = None
        if not response_body is None and response_body_length != 0:
            response_obj = json.loads(response_body)
        response_ok = self._response_ok(response)
        conn.close()
        return (response_ok, response_obj)

    def _response_ok(self, response):
        status_code = int(response.status)
        return status_code >= 200 and status_code <= 299

앱 제출 목록 데이터 가져오기

다음 예제에서는 새 샘플 앱 제출을 위해 JSON 형식의 목록 데이터를 반환하는 도우미 함수를 정의합니다.

def get_listings_object():
    """Gets a sample listings map for a submission."""
    listings = {
        # Each listing is targeted at a specific language-locale code, e.g. EN-US.
        "en-us" : {
            # This structure holds basic information to display in the store.
            "baseListing" : {
                "copyrightAndTrademarkInfo" : "(C) 2017 Microsoft",
                # Up to 7 keywords may be provided in a listing.
                "keywords"  : ["SampleApp", "SampleFightingGame", "GameOptions"],
                "licenseTerms" : "http://example.com/licenseTerms.aspx",
                "privacyPolicy" : "http://example.com/privacyPolicy.aspx",
                "supportContact" : "support@example.com",
                "websiteUrl" : "http://example.com",
                "description" : "A sample game showing off gameplay options code.",
                "features" : ["Doesn't crash", "Likes to eat chips"],
                "releaseNotes" : "Initial release",
                "recommendedHardware" : [],
                # If your app works better with specific hardware (or needs it), you can
                # add or update values here.
                "hardwarePreferences": ["Keyboard", "Mouse"],
                # The title of the app must match a reserved name for the app in Dev Center.
                # If it doesn't, attempting to update the submission will fail.
                "title" : "Super Dev Center API Simulator 2017",
                "images" : [
                    # There are several types of images available; at least one screenshot
                    # is required.
                    {
                        # The file name is relative to the root of the uploaded ZIP file.
                        "fileName" : "img/screenshot.png",
                        "description" : "A basic screenshot of the app.",
                        "imageType" : "Screenshot"
                    }
                ]
            },
            # If there are any specific overrides to above information for Windows 8,
            # Windows 8.1, Windows Phone 7.1, 8.0, or 8.1, you can add information here.
            "platformOverrides" : {}
        }
    }
    return listings

def get_package_object():
    """Gets a sample package for the submission in Dev Center."""
    package = {
        # The file name is relative to the root of the uploaded ZIP file.
        "fileName" : "bin/super_dev_ctr_api_sim.appxupload",
        # If you haven't begun to upload the file yet, set this value to "PendingUpload".
        "fileStatus" : "PendingUpload"
    }
    return package

def get_pricing_object():
    """Gets a sample pricing object for a submission."""
    pricing = {
        # How long the trial period is, if one is allowed. Valid values are NoFreeTrial,
        # OneDay, SevenDays, FifteenDays, ThirtyDays, or TrialNeverExpires.
        "trialPeriod" : "NoFreeTrial",
        # Maps to the default price for the app.
        "priceId" : "Free",
        # If you'd like to offer your app in different markets at different prices, you
        # can provide priceId values per language/locale code.
        "marketSpecificPricing" : {}
    }
    return pricing

def get_device_families_object():
    """Gets a sample device families object for a submission."""
    device_families = {
        # Supported values are Desktop, Mobile, Xbox, and Holographic. To make
        # the app available on that specific platform, set the value to True.
        "Desktop" : True,
        "Mobile" : False,
        "Xbox" : True,
        "Holographic" : False
    }
    return device_families

def get_gaming_options_object():
    """Gets a sample gaming options object for a submission."""
    gaming_options = {
        # The genres of your app.
        "Genres" : ["Games_Fighting"],

        # Set this to True if your game supports local multiplayer. This field is required.
        "IsLocalMultiplayer" : True,

        # If local multiplayer is supported, you must provide the minimum and maximum players
        # supported. Valid values are between 2 and 1000 inclusive.
        "LocalMultiplayerMinPlayers" : 2,
        "LocalMultiplayerMaxPlayers" : 4,

        # Set this to True if your game supports local co-op play. This field is required.
        "IsLocalCooperative" : True,

        # If local co-op is supported, you must provide the minimum and maximum players
        # supported. Valid values are between 2 and 1000 inclusive.
        "LocalCooperativeMinPlayers" : 2,
        "LocalCooperativeMaxPlayers" : 4,

        # Set this to True if your game supports online multiplayer. This field is required.
        "IsOnlineMultiplayer" : True,

        # If online multiplayer is supported, you must provide the minimum and maximum players
        # supported. Valid values are between 2 and 1000 inclusive.
        "OnlineMultiplayerMinPlayers" : 2,
        "OnlineMultiplayerMaxPlayers" : 4,

        # Set this to true if your game supports online co-op play. This field is required.
        "IsOnlineCooperative" : True,

        # If online co-op is supported, you must provide the minimum and maximum players
        # supported. Valid values are between 2 and 1000 inclusive.
        "OnlineCooperativeMinPlayers" : 2,
        "OnlineCooperativeMaxPlayers" : 4,

        # If your game supports broadcasting a stream to other players, set this field to True.
        # The field is required.
        "IsBroadcastingPrivilegeGranted" : True,

        # If your game supports cross-device play (e.g. a player can play on an Xbox One with
        # their friend who's playing on a PC), set this field to True. This field is required.
        "IsCrossPlayEnabled" : True,

        # If your game supports Kinect usage, set this field to "Enabled", otherwise, set it to
        # "Disabled". This field is required.
        "KinectDataForExternal" : "Disabled",

        # Free text about any other peripherals that your game supports. This field is optional.
        "OtherPeripherals" : "Supports the usage of all fighting joysticks."
    }
    return gaming_options

def get_trailer_object():
    """Gets a sample trailer object for the submission in Dev Center."""
    trailer = {
        # This is the filename of the trailer. The file name is a relative path to the
        # root of the ZIP file to be uploaded to the API.
        "VideoFileName" : "trailers/main/my_awesome_trailer.mpeg",

        # Aside from the video itself, a trailer can have image assets such as screenshots
        # or alternate images. These are separated by language-locale code, e.g. EN-US.
        "TrailerAssets" : {
            "en-us" : {

                # The title of the trailer to display in the store.
                "Title" : "Main Trailer",

                # The list of images provided with the trailer that are shown
                # when the trailer isn't playing.
                "ImageList" : [
                    {
                        # The file name of the image. The file name is a relative
                        # path to the root of the ZIP
                        # file to be uploaded to the API.
                        "FileName" : "trailers/main/thumbnail.png",

                        # A plaintext description of what the image represents.
                        "Description" : "The thumbnail for the trailer shown " +
                                        "before the user clicks play"
                    },
                    {
                        "FileName" : "trailers/main/alt-img.png",
                        "Description" : "The image to show after the trailer plays"
                    }
                ]
            }
        }
    }
    return trailer