Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Apple App Store Server Python Library
The [Python](https://github.com/apple/app-store-server-library-python) server library for the [App Store Server API](https://developer.apple.com/documentation/appstoreserverapi) and [App Store Server Notifications](https://developer.apple.com/documentation/appstoreservernotifications). Also available in [Swift](https://github.com/apple/app-store-server-library-swift), [Node.js](https://github.com/apple/app-store-server-library-node), and [Java](https://github.com/apple/app-store-server-library-java).
The [Python](https://github.com/apple/app-store-server-library-python) server library for the [App Store Server API](https://developer.apple.com/documentation/appstoreserverapi), [App Store Server Notifications](https://developer.apple.com/documentation/appstoreservernotifications), and [Retention Messaging API](https://developer.apple.com/documentation/retentionmessaging). Also available in [Swift](https://github.com/apple/app-store-server-library-swift), [Node.js](https://github.com/apple/app-store-server-library-node), and [Java](https://github.com/apple/app-store-server-library-java).

## Table of Contents
1. [Installation](#installation)
Expand Down
366 changes: 326 additions & 40 deletions appstoreserverlibrary/api_client.py

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions appstoreserverlibrary/models/AlternateProduct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
from typing import Optional
from uuid import UUID

from attr import define
import attr

@define
class AlternateProduct:
"""
A switch-plan message and product ID you provide in a real-time response to your Get Retention Message endpoint.

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

messageIdentifier: Optional[UUID] = attr.ib(default=None)
"""
The message identifier of the text to display in the switch-plan retention message.

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

productId: Optional[str] = attr.ib(default=None)
"""
The product identifier of the subscription the retention message suggests for your customer to switch to.

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

from typing import Optional
from uuid import UUID

from attr import define
import attr

from .Environment import Environment
from .LibraryUtility import AttrsRawValueAware

@define
class DecodedRealtimeRequestBody(AttrsRawValueAware):
"""
The decoded request body the App Store sends to your server to request a real-time retention message.

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

originalTransactionId: str = attr.ib()
"""
The original transaction identifier of the customer's subscription.

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

appAppleId: int = attr.ib()
"""
The unique identifier of the app in the App Store.

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

productId: str = attr.ib()
"""
The unique identifier of the auto-renewable subscription.

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

userLocale: str = attr.ib()
"""
The device's locale.

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

requestIdentifier: UUID = attr.ib()
"""
A UUID the App Store server creates to uniquely identify each request.

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

signedDate: int = attr.ib()
"""
The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature (JWS) data.

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

environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment', raw_required=True)
"""
The server environment, either sandbox or production.

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

rawEnvironment: str = Environment.create_raw_attr('environment', required=True)
"""
See environment
"""

22 changes: 22 additions & 0 deletions appstoreserverlibrary/models/DefaultConfigurationRequest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.

from typing import Optional
from uuid import UUID

from attr import define
import attr

@define
class DefaultConfigurationRequest:
"""
The request body that contains the default configuration information.

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

messageIdentifier: Optional[UUID] = attr.ib(default=None)
"""
The message identifier of the message to configure as a default message.

https://developer.apple.com/documentation/retentionmessaging/messageidentifier
"""
23 changes: 23 additions & 0 deletions appstoreserverlibrary/models/GetImageListResponse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright (c) 2025 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
"""
37 changes: 37 additions & 0 deletions appstoreserverlibrary/models/GetImageListResponseItem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.

from typing import Optional
from uuid import UUID

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[UUID] = 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
"""
23 changes: 23 additions & 0 deletions appstoreserverlibrary/models/GetMessageListResponse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.

from typing import Optional, List

from attr import define
import attr

from .GetMessageListResponseItem import GetMessageListResponseItem

@define
class GetMessageListResponse:
"""
A response that contains status information for all messages.

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

messageIdentifiers: Optional[List[GetMessageListResponseItem]] = attr.ib(default=None)
"""
An array of all message identifiers and their message state.

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

from typing import Optional
from uuid import UUID

from attr import define
import attr

from .MessageState import MessageState
from .LibraryUtility import AttrsRawValueAware

@define
class GetMessageListResponseItem(AttrsRawValueAware):
"""
A message identifier and status information for a message.

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

messageIdentifier: Optional[UUID] = attr.ib(default=None)
"""
The identifier of the message.

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

messageState: Optional[MessageState] = MessageState.create_main_attr('rawMessageState')
"""
The current state of the message.

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

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

from enum import Enum

from .LibraryUtility import AppStoreServerLibraryEnumMeta

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

https://developer.apple.com/documentation/retentionmessaging/imagestate
"""
PENDING = "PENDING"
APPROVED = "APPROVED"
REJECTED = "REJECTED"
61 changes: 46 additions & 15 deletions appstoreserverlibrary/models/LibraryUtility.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from enum import EnumMeta
from functools import lru_cache
from typing import Any, List, Type, TypeVar
from uuid import UUID

from attr import Attribute, has, ib, fields
from cattr import override
Expand All @@ -22,21 +23,39 @@ def __contains__(c, val):
return False
return True

def create_main_attr(c, raw_field_name: str) -> Any:
def create_main_attr(c, raw_field_name: str, raw_required: bool = False) -> Any:
def value_set(self, _: Attribute, value: c):
newValue = value.value if value is not None else None
if raw_required and newValue is None:
raise ValueError(f"{raw_field_name} cannot be set to None when field is required")
if newValue != getattr(self, raw_field_name):
object.__setattr__(self, raw_field_name, newValue)
return value
return ib(default=None, on_setattr=value_set, metadata={metadata_key: raw_field_name, metadata_type_key: 'main'})
def create_raw_attr(c, field_name: str) -> Any:

def create_raw_attr(c, field_name: str, required: bool = False) -> Any:
def value_set(self, _: Attribute, value: str):
if required and value is None:
raise ValueError(f"raw{field_name[0].upper() + field_name[1:]} cannot be None")
newValue = c(value) if value in c else None
if newValue != getattr(self, field_name):
object.__setattr__(self, field_name, newValue)
return value
return ib(default=None, kw_only=True, on_setattr=value_set, metadata={metadata_key: field_name, metadata_type_key: 'raw'})

def validate_not_none(instance, attribute, value):
if value is None:
raise ValueError(f"{attribute.name} cannot be None")

if required:
from attr import Factory
def factory(instance):
main_value = getattr(instance, field_name)
if main_value is not None:
return main_value.value
raise ValueError(f"Either {field_name} or raw{field_name[0].upper() + field_name[1:]} must be provided")
return ib(default=Factory(factory, takes_self=True), kw_only=True, on_setattr=value_set, validator=validate_not_none, metadata={metadata_key: field_name, metadata_type_key: 'raw'})
else:
return ib(default=None, kw_only=True, on_setattr=value_set, metadata={metadata_key: field_name, metadata_type_key: 'raw'})

class AttrsRawValueAware:
def __attrs_post_init__(self):
Expand All @@ -57,15 +76,27 @@ def __attrs_post_init__(self):
@lru_cache(maxsize=None)
def _get_cattrs_converter(destination_class: Type[T]) -> cattrs.Converter:
c = cattrs.Converter()
attributes: List[Attribute] = fields(destination_class)
cattrs_overrides = {}
for attribute in attributes:
if metadata_type_key in attribute.metadata:
matching_name: str = attribute.metadata[metadata_key]
if attribute.metadata[metadata_type_key] == 'raw':
cattrs_overrides[matching_name] = override(omit=True)
raw_field = 'raw' + matching_name[0].upper() + matching_name[1:]
cattrs_overrides[raw_field] = override(rename=matching_name)
c.register_structure_hook_factory(has, lambda cl: make_dict_structure_fn(cl, c, **cattrs_overrides))
c.register_unstructure_hook_factory(has, lambda cl: make_dict_unstructure_fn(cl, c, **cattrs_overrides))

# Register UUID hooks to ensure lowercase serialization
c.register_unstructure_hook(UUID, lambda uuid: str(uuid).lower() if uuid is not None else None)
c.register_structure_hook(UUID, lambda d, _: UUID(d) if d is not None else None)

# Need a function here because it must be a lambda based on cl, which is not always destination_class
def make_overrides(cl):
attributes: List[Attribute] = fields(cl)
cattrs_overrides = {}
# Use omit_if_default to prevent null fields from being serialized to JSON
for attribute in attributes:
if metadata_type_key in attribute.metadata:
matching_name: str = attribute.metadata[metadata_key]
if attribute.metadata[metadata_type_key] == 'raw':
cattrs_overrides[matching_name] = override(omit=True)
raw_field = 'raw' + matching_name[0].upper() + matching_name[1:]
cattrs_overrides[raw_field] = override(rename=matching_name, omit_if_default=True)
elif attribute.default is None and attribute.name not in cattrs_overrides:
cattrs_overrides[attribute.name] = override(omit_if_default=True)
return cattrs_overrides

c.register_structure_hook_factory(has, lambda cl: make_dict_structure_fn(cl, c, **make_overrides(cl)))
c.register_unstructure_hook_factory(has, lambda cl: make_dict_unstructure_fn(cl, c, **make_overrides(cl)))
return c
21 changes: 21 additions & 0 deletions appstoreserverlibrary/models/Message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright (c) 2025 Apple Inc. Licensed under MIT License.
from typing import Optional
from uuid import UUID

from attr import define
import attr

@define
class Message:
"""
A message identifier you provide in a real-time response to your Get Retention Message endpoint.

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

messageIdentifier: Optional[UUID] = attr.ib(default=None)
"""
The identifier of the message to display to the customer.

https://developer.apple.com/documentation/retentionmessaging/messageidentifier
"""
Loading