Python 範例:提交含遊戲選項與預告片的應用程式
本文提供 Python 程式碼範例,示範如何使用 Microsoft Store 提交 API 來進行下列工作:
- 取得 Azure AD 存取權杖,以搭配 Microsoft Store 提交 API 使用。
- 建立應用程式提交
- 設定應用程式提交的 Store 清單,包括遊戲和預告片進階清單選項。
- 上傳包含應用程式提交的套件、清單影像和預告片檔案的 ZIP 檔案。
- 認可應用程式提交。
建立應用程式提交
此程式碼會呼叫其他範例類別和函式,以使用 Microsoft Store 提交 API 來建立並認可包含遊戲選項和預告片的應用程式提交。 若要調整此程式碼以供您自己使用:
- 將
tenant
變數指派給應用程式的租用戶識別碼,並為您應用程式的用戶端識別碼和金鑰指派client
和secret
變數。 如需詳細資訊,請參閱如何將 Azure AD 應用程式與您的合作夥伴中心帳戶產生關聯 - 將
application_id
變數指派給您所要建立提交應用程式的 Store 識別碼。
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
、clientId
和clientSecret
值來建立 Azure AD 存取權杖,以搭配 Microsoft Store 提交 API 使用。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