Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions appstoreserverlibrary/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from .models.UploadMessageRequestBody import UploadMessageRequestBody
from .models.GetMessageListResponse import GetMessageListResponse
from .models.DefaultConfigurationRequest import DefaultConfigurationRequest
from .models.GetImageListResponse import GetImageListResponse

T = TypeVar('T')

Expand Down Expand Up @@ -528,6 +529,41 @@ class APIError(IntEnum):
https://developer.apple.com/documentation/retentionmessaging/imagenotapprovederror
"""

INVALID_IMAGE_ERROR = 4000104
"""
An error that indicates the image is invalid (wrong format, size, or has transparency).

https://developer.apple.com/documentation/retentionmessaging/invalidimageerror
"""

IMAGE_NOT_FOUND_ERROR = 4040002
"""
An error that indicates the specified image was not found.

https://developer.apple.com/documentation/retentionmessaging/imagenotfounderror
"""

IMAGE_IN_USE_ERROR = 4030002
"""
An error that indicates the image is in use by a message and cannot be deleted.

https://developer.apple.com/documentation/retentionmessaging/imageinuseerror
"""

IMAGE_ALREADY_EXISTS_ERROR = 4090002
"""
An error that indicates the image identifier already exists.

https://developer.apple.com/documentation/retentionmessaging/imagealreadyexistserror
"""

MAXIMUM_NUMBER_OF_IMAGES_REACHED_ERROR = 4030019
"""
An error that indicates the maximum number of retention images (2000) has been reached.

https://developer.apple.com/documentation/retentionmessaging/maximumnumberofimagesreachederror
"""


@define
class APIException(Exception):
Expand Down Expand Up @@ -635,6 +671,28 @@ def _make_request(self, path: str, method: str, queryParameters: Dict[str, Union
def _execute_request(self, method: str, url: str, params: Dict[str, Union[str, List[str]]], headers: Dict[str, str], json: Dict[str, Any]) -> requests.Response:
return requests.request(method, url, params=params, headers=headers, json=json)

def _make_binary_request(self, path: str, method: str, binary_data: bytes, content_type: str) -> None:
"""Make a request with binary data (e.g., image upload)."""
url = self._get_full_url(path)
headers = self._get_headers()
headers['Content-Type'] = content_type
# Remove Accept header for binary uploads
if 'Accept' in headers:
del headers['Accept']

response = requests.request(method, url, headers=headers, data=binary_data)

# Parse response for errors (successful uploads return 200 with no body)
if not (200 <= response.status_code < 300):
Comment on lines +685 to +686

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment doesn't seem to match the logic. We treat everything between 200 and 299 a success in the code. Could the API actually return something other than 200 for success?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably not! the docs only specify 200

if response.headers.get('content-type') == 'application/json':
try:
response_body = response.json()
raise APIException(response.status_code, response_body.get('errorCode'), response_body.get('errorMessage'))
except (ValueError, KeyError):
raise APIException(response.status_code)
else:
raise APIException(response.status_code)

def extend_renewal_date_for_all_active_subscribers(self, mass_extend_renewal_date_request: MassExtendRenewalDateRequest) -> MassExtendRenewalDateResponse:
"""
Uses a subscription's product identifier to extend the renewal date for all of its eligible active subscribers.
Expand Down Expand Up @@ -879,6 +937,37 @@ def delete_default_retention_message(self, product_id: str, locale: str) -> None
"""
self._make_request("/inApps/v1/messaging/default/" + product_id + "/" + locale, "DELETE", {}, None, None)

def upload_retention_image(self, image_identifier: str, image_data: bytes) -> None:
"""
Upload an image to use for retention messaging.
https://developer.apple.com/documentation/retentionmessaging/upload-image

:param image_identifier: A UUID you provide to uniquely identify the image you upload.
:param image_data: The PNG image file data (must be 3840x2160 pixels, no transparency).
:raises APIException: If a response was returned indicating the request could not be processed
"""
self._make_binary_request("/inApps/v1/messaging/image/" + image_identifier, "PUT", image_data, "image/png")

def get_retention_image_list(self) -> GetImageListResponse:
"""
Get the image identifier and state for all uploaded images.
https://developer.apple.com/documentation/retentionmessaging/get-image-list

:return: A response that contains status information for all images.
:raises APIException: If a response was returned indicating the request could not be processed
"""
return self._make_request("/inApps/v1/messaging/image/list", "GET", {}, None, GetImageListResponse)

def delete_retention_image(self, image_identifier: str) -> None:
"""
Delete a previously uploaded image.
https://developer.apple.com/documentation/retentionmessaging/delete-image

:param image_identifier: The identifier of the image to delete.
:raises APIException: If a response was returned indicating the request could not be processed
"""
self._make_request("/inApps/v1/messaging/image/" + image_identifier, "DELETE", {}, None, None)

class AsyncAppStoreServerAPIClient(BaseAppStoreServerAPIClient):
def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str, environment: Environment):
super().__init__(signing_key=signing_key, key_id=key_id, issuer_id=issuer_id, bundle_id=bundle_id, environment=environment)
Expand All @@ -902,6 +991,28 @@ async def _make_request(self, path: str, method: str, queryParameters: Dict[str,
async def _execute_request(self, method: str, url: str, params: Dict[str, Union[str, List[str]]], headers: Dict[str, str], json: Dict[str, Any]):
return await self.http_client.request(method, url, params=params, headers=headers, json=json)

async def _make_binary_request(self, path: str, method: str, binary_data: bytes, content_type: str) -> None:
"""Make an async request with binary data (e.g., image upload)."""
url = self._get_full_url(path)
headers = self._get_headers()
headers['Content-Type'] = content_type
# Remove Accept header for binary uploads
if 'Accept' in headers:
del headers['Accept']

response = await self.http_client.request(method, url, headers=headers, content=binary_data)

# Parse response for errors (successful uploads return 200 with no body)
if not (200 <= response.status_code < 300):
if response.headers.get('content-type') == 'application/json':
try:
response_body = response.json()
raise APIException(response.status_code, response_body.get('errorCode'), response_body.get('errorMessage'))
except (ValueError, KeyError):
raise APIException(response.status_code)
else:
raise APIException(response.status_code)

async def extend_renewal_date_for_all_active_subscribers(self, mass_extend_renewal_date_request: MassExtendRenewalDateRequest) -> MassExtendRenewalDateResponse:
"""
Uses a subscription's product identifier to extend the renewal date for all of its eligible active subscribers.
Expand Down Expand Up @@ -1145,3 +1256,34 @@ async def delete_default_retention_message(self, product_id: str, locale: str) -
:raises APIException: If a response was returned indicating the request could not be processed
"""
await self._make_request("/inApps/v1/messaging/default/" + product_id + "/" + locale, "DELETE", {}, None, None)

async def upload_retention_image(self, image_identifier: str, image_data: bytes) -> None:
"""
Upload an image to use for retention messaging.
https://developer.apple.com/documentation/retentionmessaging/upload-image

:param image_identifier: A UUID you provide to uniquely identify the image you upload.
:param image_data: The PNG image file data (must be 3840x2160 pixels, no transparency).
:raises APIException: If a response was returned indicating the request could not be processed
"""
await self._make_binary_request("/inApps/v1/messaging/image/" + image_identifier, "PUT", image_data, "image/png")

async def get_retention_image_list(self) -> GetImageListResponse:
"""
Get the image identifier and state for all uploaded images.
https://developer.apple.com/documentation/retentionmessaging/get-image-list

:return: A response that contains status information for all images.
:raises APIException: If a response was returned indicating the request could not be processed
"""
return await self._make_request("/inApps/v1/messaging/image/list", "GET", {}, None, GetImageListResponse)

async def delete_retention_image(self, image_identifier: str) -> None:
"""
Delete a previously uploaded image.
https://developer.apple.com/documentation/retentionmessaging/delete-image

:param image_identifier: The identifier of the image to delete.
:raises APIException: If a response was returned indicating the request could not be processed
"""
await self._make_request("/inApps/v1/messaging/image/" + image_identifier, "DELETE", {}, None, None)
21 changes: 21 additions & 0 deletions appstoreserverlibrary/models/GetImageListResponse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright (c) 2023 Apple Inc. Licensed under MIT License.

from typing import Optional, List
from attr import define
import attr
from .GetImageListResponseItem import GetImageListResponseItem

@define
class GetImageListResponse:
"""
A response that contains status information for all images.

https://developer.apple.com/documentation/retentionmessaging/getimagelistresponse
"""

imageIdentifiers: Optional[List[GetImageListResponseItem]] = attr.ib(default=None)
"""
An array of all image identifiers and their image state.

https://developer.apple.com/documentation/retentionmessaging/getimagelistresponseitem
"""
34 changes: 34 additions & 0 deletions appstoreserverlibrary/models/GetImageListResponseItem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright (c) 2023 Apple Inc. Licensed under MIT License.

from typing import Optional
from attr import define
import attr
from .ImageState import ImageState
from .LibraryUtility import AttrsRawValueAware

@define
class GetImageListResponseItem(AttrsRawValueAware):
"""
An image identifier and state information for an image.

https://developer.apple.com/documentation/retentionmessaging/getimagelistresponseitem
"""

imageIdentifier: Optional[str] = attr.ib(default=None)
"""
The identifier of the image.

https://developer.apple.com/documentation/retentionmessaging/imageidentifier
"""

imageState: Optional[ImageState] = ImageState.create_main_attr('rawImageState')
"""
The current state of the image.

https://developer.apple.com/documentation/retentionmessaging/imagestate
"""

rawImageState: Optional[str] = ImageState.create_raw_attr('imageState')
"""
See imageState
"""
26 changes: 26 additions & 0 deletions appstoreserverlibrary/models/ImageState.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright (c) 2023 Apple Inc. Licensed under MIT License.

from enum import Enum
from .LibraryUtility import AppStoreServerLibraryEnumMeta

class ImageState(Enum, metaclass=AppStoreServerLibraryEnumMeta):
"""
The approval state of an image.

https://developer.apple.com/documentation/retentionmessaging/imagestate
"""

PENDING = "PENDING"
"""
The image is awaiting approval.
"""

APPROVED = "APPROVED"
"""
The image is approved.
"""

REJECTED = "REJECTED"
"""
The image is rejected.
"""
8 changes: 4 additions & 4 deletions appstoreserverlibrary/models/UploadMessageImage.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ class UploadMessageImage:
https://developer.apple.com/documentation/retentionmessaging/uploadmessageimage
"""

imageIdentifier: Optional[str] = attr.ib(default=None)
imageIdentifier: str = attr.ib()
"""
The unique identifier of an image.
**Required.** The unique identifier of an image.

https://developer.apple.com/documentation/retentionmessaging/imageidentifier
"""

altText: Optional[str] = attr.ib(default=None)
altText: str = attr.ib()
"""
The alternative text you provide for the corresponding image.
**Required.** The alternative text you provide for the corresponding image.
Maximum length: 150

https://developer.apple.com/documentation/retentionmessaging/alttext
Expand Down
8 changes: 4 additions & 4 deletions appstoreserverlibrary/models/UploadMessageRequestBody.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ class UploadMessageRequestBody:
https://developer.apple.com/documentation/retentionmessaging/uploadmessagerequestbody
"""

header: Optional[str] = attr.ib(default=None)
header: str = attr.ib()
"""
The header text of the retention message that the system displays to customers.
**Required.** The header text of the retention message that the system displays to customers.
Maximum length: 66

https://developer.apple.com/documentation/retentionmessaging/header
"""

body: Optional[str] = attr.ib(default=None)
body: str = attr.ib()
"""
The body text of the retention message that the system displays to customers.
**Required.** The body text of the retention message that the system displays to customers.
Maximum length: 144

https://developer.apple.com/documentation/retentionmessaging/body
Expand Down
Loading