Ejemplo de Python: envío de aplicación con opciones de juego y tráileres

En este artículo se proporcionan ejemplos de código Python que muestran cómo usar la API de envío de Microsoft Store para estas tareas:

  • Obtenga un token de acceso de Azure AD para usarlo con la API de envío de Microsoft Store.
  • Crear un envío de aplicación
  • Configure los datos de la descripción de la Store para el envío de aplicación, incluidas las opciones de descripciones avanzadas de juegos y avances.
  • Cargue el archivo ZIP que contiene los paquetes, las imágenes de descripciones y los archivos de avances para el envío de aplicación.
  • Confirme el envío de aplicación.

Crear un envío de aplicación

Este código llama a otras clases y funciones de ejemplo para usar la API de envío de Microsoft Store para crear y confirmar un envío de aplicación que contiene opciones de juego y un avance. Para adaptar este código a su propio uso:

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)

Obtención de un token de acceso de Azure AD e invocación de la API de envío

En el ejemplo siguiente se definen estas clases:

  • La clase DevCenterAccessTokenClient define un método auxiliar que usa los valores tenantId, clientId y clientSecret para crear un token de acceso de Azure AD para usarlo con la API de envío de Microsoft Store.
  • La clase DevCenterClient define métodos auxiliares que invocan una variedad de métodos en la API de envío de Microsoft Store y cargan el archivo ZIP que contiene los paquetes, las imágenes de descripciones y los archivos de tráileres para el envío de aplicación.
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

Obtención de datos de la descripción del envío de aplicación

En el siguiente ejemplo se definen las funciones auxiliares que devuelven datos de la descripción con formato JSON para un nuevo envío de aplicación de muestra.

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