diff --git a/appstoreserverlibrary/api_client.py b/appstoreserverlibrary/api_client.py index 12699b89..89238c07 100644 --- a/appstoreserverlibrary/api_client.py +++ b/appstoreserverlibrary/api_client.py @@ -15,18 +15,25 @@ from .models.CheckTestNotificationResponse import CheckTestNotificationResponse from .models.ConsumptionRequest import ConsumptionRequest from .models.DefaultConfigurationRequest import DefaultConfigurationRequest +from .models.DefaultConfigurationResponse import DefaultConfigurationResponse from .models.Environment import Environment from .models.ExtendRenewalDateRequest import ExtendRenewalDateRequest from .models.ExtendRenewalDateResponse import ExtendRenewalDateResponse from .models.GetImageListResponse import GetImageListResponse from .models.GetMessageListResponse import GetMessageListResponse from .models.HistoryResponse import HistoryResponse +from .models.ImageSize import ImageSize from .models.MassExtendRenewalDateRequest import MassExtendRenewalDateRequest from .models.MassExtendRenewalDateResponse import MassExtendRenewalDateResponse from .models.MassExtendRenewalDateStatusResponse import MassExtendRenewalDateStatusResponse from .models.NotificationHistoryRequest import NotificationHistoryRequest from .models.NotificationHistoryResponse import NotificationHistoryResponse from .models.OrderLookupResponse import OrderLookupResponse +from .models.PerformanceTestRequest import PerformanceTestRequest +from .models.PerformanceTestResponse import PerformanceTestResponse +from .models.PerformanceTestResultResponse import PerformanceTestResultResponse +from .models.RealtimeUrlRequest import RealtimeUrlRequest +from .models.RealtimeUrlResponse import RealtimeUrlResponse from .models.RefundHistoryResponse import RefundHistoryResponse from .models.SendTestNotificationResponse import SendTestNotificationResponse from .models.Status import Status @@ -389,6 +396,62 @@ class APIError(IntEnum): https://developer.apple.com/documentation/appstoreserverapi/transactionidisnotoriginaltransactioniderror """ + INVALID_PERFORMANCE_TEST_REQUEST = 4000211 + """ + An error the API returns that indicates the performance test request is invalid. + + https://developer.apple.com/documentation/retentionmessaging/invalidperformancetestrequesterror + """ + + INVALID_REQUEST_ID = 4000212 + """ + An error that indicates the request ID is invalid. + + https://developer.apple.com/documentation/retentionmessaging/invalidrequestiderror + """ + + EXISTING_PERFORMANCE_TEST_RUN = 4000213 + """ + An error that indicates an error with an existing test. + + https://developer.apple.com/documentation/retentionmessaging/existingperformancetestrunerror + """ + + BAD_REQUEST_REALTIME_URL = 4000215 + """ + An error that indicates the URL is invalid. + + https://developer.apple.com/documentation/retentionmessaging/badrequestrealtimeurlerror + """ + + BAD_REQUEST_IMAGE_SIZE = 4000216 + """ + An error that indicates the image size provided is invalid. + + https://developer.apple.com/documentation/retentionmessaging/badrequestimagesizeerror + """ + + BAD_REQUEST_TOO_MANY_BULLET_POINTS = 4000218 + """ + An error that indicates there are too many bullet points. + + https://developer.apple.com/documentation/retentionmessaging/badrequesttoomanybulletpointserror + """ + + BAD_REQUEST_BULLET_POINT_TEXT_TOO_LONG = 4000219 + """ + An error that indicates the text for a bullet point is too long. + + https://developer.apple.com/documentation/retentionmessaging/badrequestbulletpointtexttoolongerror + """ + + BAD_REQUEST_ABOVE_IMAGE_REQUIRES_AN_IMAGE = 4000224 + """ + An error that indicates that no image object is included, but the request indicates that the header should be placed above the image. + + https://developer.apple.com/documentation/retentionmessaging/badrequestaboveimagerequiresanimageerror + """ + SUBSCRIPTION_EXTENSION_INELIGIBLE = 4030004 """ An error that indicates the subscription doesn't qualify for a renewal-date extension due to its subscription state. @@ -445,6 +508,13 @@ class APIError(IntEnum): https://developer.apple.com/documentation/retentionmessaging/imageinuseerror """ + FORBIDDEN_NO_PASSING_TEST = 4030026 + """ + An error that indicates that passing a performance test is required before you can set a URL for the production environment. + + https://developer.apple.com/documentation/retentionmessaging/forbiddennopassingtesterror + """ + ACCOUNT_NOT_FOUND = 4040001 """ An error that indicates the App Store account wasn’t found. @@ -529,6 +599,13 @@ class APIError(IntEnum): https://developer.apple.com/documentation/retentionmessaging/messagenotfounderror """ + PERFORMANCE_TEST_RUN_NOT_FOUND = 4040018 + """ + An error the API returns if the service can't find the specified test run. + + https://developer.apple.com/documentation/retentionmessaging/performancetestrunnotfounderror + """ + APP_TRANSACTION_DOES_NOT_EXIST_ERROR = 4040019 """ An error response that indicates an app transaction doesn’t exist for the specified customer. @@ -536,6 +613,20 @@ class APIError(IntEnum): https://developer.apple.com/documentation/appstoreserverapi/apptransactiondoesnotexisterror """ + DEFAULT_MESSAGE_NOT_FOUND = 4040020 + """ + An error that indicates a default message isn’t configured. + + https://developer.apple.com/documentation/retentionmessaging/defaultmessagenotfounderror + """ + + REALTIME_URL_NOT_FOUND = 4040021 + """ + An error that indicates that the URL for your endpoint isn’t configured. + + https://developer.apple.com/documentation/retentionmessaging/realtimeurlnotfounderror + """ + IMAGE_ALREADY_EXISTS = 4090000 """ An error that indicates the image identifier already exists. @@ -875,16 +966,20 @@ def set_app_account_token(self, original_transaction_id: str, update_app_account """ self._make_request(f"/inApps/v1/transactions/{original_transaction_id}/appAccountToken", "PUT", {}, update_app_account_token_request, None, None) - def upload_image(self, image_identifier: UUID, image: bytes): + def upload_image(self, image_identifier: UUID, image: bytes, image_size: Optional[ImageSize] = None): """ Upload an image to use for retention messaging. :param image_identifier: A UUID you provide to uniquely identify the image you upload. :param image: The image file to upload. + :param image_size: The size of the image you upload. :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/upload-image """ - self._make_request(f"/inApps/v1/messaging/image/{image_identifier}", "PUT", {}, image, None, "image/png") + query_parameters = {} + if image_size is not None: + query_parameters["imageSize"] = [image_size.value] + self._make_request(f"/inApps/v1/messaging/image/{image_identifier}", "PUT", query_parameters, image, None, "image/png") def delete_image(self, image_identifier: UUID): """ @@ -959,7 +1054,70 @@ def delete_default_message(self, product_id: str, locale: str): :see: https://developer.apple.com/documentation/retentionmessaging/delete-default-message """ self._make_request(f"/inApps/v1/messaging/default/{product_id}/{locale}", "DELETE", {}, None, None, None) - + + def get_default_message(self, product_id: str, locale: str) -> DefaultConfigurationResponse: + """ + Gets the default message for a specific product in a specific locale, if it’s configured. + + :param product_id: The product identifier of the message. + :param locale: The locale of the message. + :return: The response body that contains the default configuration information. + :raises APIException: If a response was returned indicating the request could not be processed + :see: https://developer.apple.com/documentation/retentionmessaging/get-default-message + """ + return self._make_request(f"/inApps/v1/messaging/default/{product_id}/{locale}", "GET", {}, None, DefaultConfigurationResponse, None) + + def configure_realtime_url(self, realtime_url_request: RealtimeUrlRequest): + """ + Configures the URL for your Get Retention Message endpoint in the sandbox and production environments. + + :param realtime_url_request: The request body that includes your endpoint’s URL. + :raises APIException: If a response was returned indicating the request could not be processed + :see: https://developer.apple.com/documentation/retentionmessaging/configure-realtime-url + """ + self._make_request("/inApps/v1/messaging/realtime/url", "PUT", {}, realtime_url_request, None, None) + + def delete_realtime_url(self): + """ + Deletes the URL for your Get Retention Message endpoint, in the sandbox or production environments. + + :raises APIException: If a response was returned indicating the request could not be processed + :see: https://developer.apple.com/documentation/retentionmessaging/delete-realtime-url + """ + self._make_request("/inApps/v1/messaging/realtime/url", "DELETE", {}, None, None, None) + + def get_realtime_url(self) -> RealtimeUrlResponse: + """ + Gets the URL for real-time messages that points to your Get Retention Message endpoint, which you previously configured. + + :return: The response body that contains the URL for your Get Retention Message endpoint. + :raises APIException: If a response was returned indicating the request could not be processed + :see: https://developer.apple.com/documentation/retentionmessaging/get-realtime-url + """ + return self._make_request("/inApps/v1/messaging/realtime/url", "GET", {}, None, RealtimeUrlResponse, None) + + def initiate_performance_test(self, performance_test_request: PerformanceTestRequest) -> PerformanceTestResponse: + """ + Initiates a performance test of your Get Retention Message endpoint in the sandbox environment. + + :param performance_test_request: The request body which specifies a transaction identifier of an In-App Purchase to use for this test. + :return: The performance test response object. + :raises APIException: If a response was returned indicating the request could not be processed + :see: https://developer.apple.com/documentation/retentionmessaging/initiate-performance-test + """ + return self._make_request("/inApps/v1/messaging/performanceTest", "POST", {}, performance_test_request, PerformanceTestResponse, None) + + def get_performance_test_results(self, request_id: str) -> PerformanceTestResultResponse: + """ + Gets the results of the performance test for the specified identifier. + + :param request_id: The ID of the performance test to return, which you receive in the PerformanceTestResponse when you call Initiate Performance Test. + :return: An object the API returns that describes the performance test results. + :raises APIException: If a response was returned indicating the request could not be processed + :see: https://developer.apple.com/documentation/retentionmessaging/get-performance-test-results + """ + return self._make_request(f"/inApps/v1/messaging/performanceTest/result/{request_id}", "GET", {}, None, PerformanceTestResultResponse, None) + def get_app_transaction_info(self, transaction_id: str) -> AppTransactionInfoResponse: """ Get a customer's app transaction information for your app. @@ -1192,16 +1350,20 @@ async def set_app_account_token(self, original_transaction_id: str, update_app_a """ await self._make_request(f"/inApps/v1/transactions/{original_transaction_id}/appAccountToken", "PUT", {}, update_app_account_token_request, None, None) - async def upload_image(self, image_identifier: UUID, image: bytes): + async def upload_image(self, image_identifier: UUID, image: bytes, image_size: Optional[ImageSize] = None): """ Upload an image to use for retention messaging. :param image_identifier: A UUID you provide to uniquely identify the image you upload. :param image: The image file to upload. + :param image_size: The optional size of the image. :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/upload-image """ - await self._make_request(f"/inApps/v1/messaging/image/{image_identifier}", "PUT", {}, image, None, "image/png") + query_parameters = {} + if image_size is not None: + query_parameters["imageSize"] = [image_size.value] + await self._make_request(f"/inApps/v1/messaging/image/{image_identifier}", "PUT", query_parameters, image, None, "image/png") async def delete_image(self, image_identifier: UUID): """ @@ -1276,7 +1438,70 @@ async def delete_default_message(self, product_id: str, locale: str): :see: https://developer.apple.com/documentation/retentionmessaging/delete-default-message """ await self._make_request(f"/inApps/v1/messaging/default/{product_id}/{locale}", "DELETE", {}, None, None, None) - + + async def get_default_message(self, product_id: str, locale: str) -> DefaultConfigurationResponse: + """ + Get the default message for a specific product in a specific locale. + + :param product_id: The product identifier for the default configuration. + :param locale: The locale for the default configuration. + :return: A response that contains the default configuration information. + :raises APIException: If a response was returned indicating the request could not be processed + :see: https://developer.apple.com/documentation/retentionmessaging/get-default-message + """ + return await self._make_request(f"/inApps/v1/messaging/default/{product_id}/{locale}", "GET", {}, None, DefaultConfigurationResponse, None) + + async def configure_realtime_url(self, realtime_url_request: RealtimeUrlRequest): + """ + Configure the real-time URL for retention messaging. + + :param realtime_url_request: The request body that contains the real-time URL. + :raises APIException: If a response was returned indicating the request could not be processed + :see: https://developer.apple.com/documentation/retentionmessaging/configure-realtime-url + """ + await self._make_request("/inApps/v1/messaging/realtime/url", "PUT", {}, realtime_url_request, None, None) + + async def delete_realtime_url(self): + """ + Delete the real-time URL for retention messaging. + + :raises APIException: If a response was returned indicating the request could not be processed + :see: https://developer.apple.com/documentation/retentionmessaging/delete-realtime-url + """ + await self._make_request("/inApps/v1/messaging/realtime/url", "DELETE", {}, None, None, None) + + async def get_realtime_url(self) -> RealtimeUrlResponse: + """ + Get the real-time URL for retention messaging. + + :return: A response that contains the real-time URL information. + :raises APIException: If a response was returned indicating the request could not be processed + :see: https://developer.apple.com/documentation/retentionmessaging/get-realtime-url + """ + return await self._make_request("/inApps/v1/messaging/realtime/url", "GET", {}, None, RealtimeUrlResponse, None) + + async def initiate_performance_test(self, performance_test_request: PerformanceTestRequest) -> PerformanceTestResponse: + """ + Initiates a performance test of your Get Retention Message endpoint in the sandbox environment. + + :param performance_test_request: The request body which specifies a transaction identifier of an In-App Purchase to use for this test. + :return: The performance test response object. + :raises APIException: If a response was returned indicating the request could not be processed + :see: https://developer.apple.com/documentation/retentionmessaging/initiate-performance-test + """ + return await self._make_request("/inApps/v1/messaging/performanceTest", "POST", {}, performance_test_request, PerformanceTestResponse, None) + + async def get_performance_test_results(self, request_id: str) -> PerformanceTestResultResponse: + """ + Gets the results of the performance test for the specified identifier. + + :param request_id: The ID of the performance test to return, which you receive in the PerformanceTestResponse when you call Initiate Performance Test. + :return: An object the API returns that describes the performance test results. + :raises APIException: If a response was returned indicating the request could not be processed + :see: https://developer.apple.com/documentation/retentionmessaging/get-performance-test-results + """ + return await self._make_request(f"/inApps/v1/messaging/performanceTest/result/{request_id}", "GET", {}, None, PerformanceTestResultResponse, None) + async def get_app_transaction_info(self, transaction_id: str) -> AppTransactionInfoResponse: """ Get a customer's app transaction information for your app. diff --git a/appstoreserverlibrary/models/BulletPoint.py b/appstoreserverlibrary/models/BulletPoint.py new file mode 100644 index 00000000..1380c34f --- /dev/null +++ b/appstoreserverlibrary/models/BulletPoint.py @@ -0,0 +1,35 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from uuid import UUID + +from attr import define +import attr + +@define +class BulletPoint: + """ + The text and its bullet-point image to include in a retention message’s bulleted list. + + https://developer.apple.com/documentation/retentionmessaging/bulletpoint + """ + + text: str = attr.ib(validator=attr.validators.max_len(66)) + """ + The text of the individual bullet point. + + https://developer.apple.com/documentation/retentionmessaging/bulletpointtext + """ + + imageIdentifier: UUID = attr.ib() + """ + The identifier of the image to use as the bullet point. + + https://developer.apple.com/documentation/retentionmessaging/imageidentifier + """ + + altText: str = attr.ib(validator=attr.validators.max_len(150)) + """ + The alternative text you provide for the corresponding image of the bullet point. + + https://developer.apple.com/documentation/retentionmessaging/alttext + """ diff --git a/appstoreserverlibrary/models/DefaultConfigurationRequest.py b/appstoreserverlibrary/models/DefaultConfigurationRequest.py index f7f3d6bc..2660163a 100644 --- a/appstoreserverlibrary/models/DefaultConfigurationRequest.py +++ b/appstoreserverlibrary/models/DefaultConfigurationRequest.py @@ -18,5 +18,7 @@ class DefaultConfigurationRequest: """ The message identifier of the message to configure as a default message. + Note: In a future version, this field will become required. + https://developer.apple.com/documentation/retentionmessaging/messageidentifier """ diff --git a/appstoreserverlibrary/models/DefaultConfigurationResponse.py b/appstoreserverlibrary/models/DefaultConfigurationResponse.py new file mode 100644 index 00000000..56d714ea --- /dev/null +++ b/appstoreserverlibrary/models/DefaultConfigurationResponse.py @@ -0,0 +1,21 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from uuid import UUID + +from attr import define +import attr + +@define +class DefaultConfigurationResponse: + """ + The response body that contains the default configuration information. + + https://developer.apple.com/documentation/retentionmessaging/defaultconfigurationresponse + """ + + messageIdentifier: UUID = attr.ib() + """ + The message identifier of the retention message you configured as a default. + + https://developer.apple.com/documentation/retentionmessaging/messageidentifier + """ diff --git a/appstoreserverlibrary/models/GetImageListResponseItem.py b/appstoreserverlibrary/models/GetImageListResponseItem.py index 4f00945e..4c582a3a 100644 --- a/appstoreserverlibrary/models/GetImageListResponseItem.py +++ b/appstoreserverlibrary/models/GetImageListResponseItem.py @@ -6,6 +6,7 @@ from attr import define import attr +from .ImageSize import ImageSize from .ImageState import ImageState from .LibraryUtility import AttrsRawValueAware @@ -34,4 +35,16 @@ class GetImageListResponseItem(AttrsRawValueAware): rawImageState: Optional[str] = ImageState.create_raw_attr('imageState') """ See imageState + """ + + imageSize: Optional[ImageSize] = ImageSize.create_main_attr('rawImageSize') + """ + The size of the image. + + https://developer.apple.com/documentation/retentionmessaging/imagesize + """ + + rawImageSize: Optional[str] = ImageSize.create_raw_attr('imageSize') + """ + See imageSize """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/HeaderPosition.py b/appstoreserverlibrary/models/HeaderPosition.py new file mode 100644 index 00000000..7c9519c7 --- /dev/null +++ b/appstoreserverlibrary/models/HeaderPosition.py @@ -0,0 +1,14 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from enum import Enum + +from .LibraryUtility import AppStoreServerLibraryEnumMeta + +class HeaderPosition(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): + """ + The position where the header text appears in a message. + + https://developer.apple.com/documentation/retentionmessaging/headerposition + """ + ABOVE_BODY = "ABOVE_BODY" + ABOVE_IMAGE = "ABOVE_IMAGE" diff --git a/appstoreserverlibrary/models/ImageSize.py b/appstoreserverlibrary/models/ImageSize.py new file mode 100644 index 00000000..1669d3e6 --- /dev/null +++ b/appstoreserverlibrary/models/ImageSize.py @@ -0,0 +1,14 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from enum import Enum + +from .LibraryUtility import AppStoreServerLibraryEnumMeta + +class ImageSize(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): + """ + The size of an image. + + https://developer.apple.com/documentation/retentionmessaging/imagesize + """ + FULL_SIZE = "FULL_SIZE" + BULLET_POINT = "BULLET_POINT" diff --git a/appstoreserverlibrary/models/PerformanceTestConfig.py b/appstoreserverlibrary/models/PerformanceTestConfig.py new file mode 100644 index 00000000..a0f35050 --- /dev/null +++ b/appstoreserverlibrary/models/PerformanceTestConfig.py @@ -0,0 +1,49 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from typing import Optional + +from attr import define +import attr + +@define +class PerformanceTestConfig: + """ + An object that enumerates the test configuration parameters. + + https://developer.apple.com/documentation/retentionmessaging/performancetestconfig + """ + + maxConcurrentRequests: Optional[int] = attr.ib(default=None) + """ + The maximum number of concurrent requests the API allows. + + https://developer.apple.com/documentation/retentionmessaging/maxconcurrentrequests + """ + + totalRequests: Optional[int] = attr.ib(default=None) + """ + The total number of requests to make during the test. + + https://developer.apple.com/documentation/retentionmessaging/totalrequests + """ + + totalDuration: Optional[int] = attr.ib(default=None) + """ + The total duration of the test in milliseconds. + + https://developer.apple.com/documentation/retentionmessaging/totalduration + """ + + responseTimeThreshold: Optional[int] = attr.ib(default=None) + """ + The maximum time your server has to respond when the system calls your Get Retention Message endpoint in the sandbox environment. + + https://developer.apple.com/documentation/retentionmessaging/responsetimethreshold + """ + + successRateThreshold: Optional[int] = attr.ib(default=None) + """ + The success rate threshold percentage. + + https://developer.apple.com/documentation/retentionmessaging/successratethreshold + """ diff --git a/appstoreserverlibrary/models/PerformanceTestRequest.py b/appstoreserverlibrary/models/PerformanceTestRequest.py new file mode 100644 index 00000000..66768bcd --- /dev/null +++ b/appstoreserverlibrary/models/PerformanceTestRequest.py @@ -0,0 +1,19 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from attr import define +import attr + +@define +class PerformanceTestRequest: + """ + The request object you provide for a performance test that contains an original transaction identifier. + + https://developer.apple.com/documentation/retentionmessaging/performancetestrequest + """ + + originalTransactionId: str = attr.ib() + """ + The original transaction identifier of an In-App Purchase you initiate in the sandbox environment, to use as the purchase for this test. + + https://developer.apple.com/documentation/retentionmessaging/originaltransactionid + """ diff --git a/appstoreserverlibrary/models/PerformanceTestResponse.py b/appstoreserverlibrary/models/PerformanceTestResponse.py new file mode 100644 index 00000000..e5e81915 --- /dev/null +++ b/appstoreserverlibrary/models/PerformanceTestResponse.py @@ -0,0 +1,30 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from typing import Optional + +from attr import define +import attr + +from .PerformanceTestConfig import PerformanceTestConfig + +@define +class PerformanceTestResponse: + """ + The performance test response object. + + https://developer.apple.com/documentation/retentionmessaging/performancetestresponse + """ + + config: Optional[PerformanceTestConfig] = attr.ib(default=None) + """ + The performance test configuration object. + + https://developer.apple.com/documentation/retentionmessaging/performancetestconfig + """ + + requestId: Optional[str] = attr.ib(default=None) + """ + The performance test request identifier. + + https://developer.apple.com/documentation/retentionmessaging/requestid + """ diff --git a/appstoreserverlibrary/models/PerformanceTestResponseTimes.py b/appstoreserverlibrary/models/PerformanceTestResponseTimes.py new file mode 100644 index 00000000..b8d4fe5e --- /dev/null +++ b/appstoreserverlibrary/models/PerformanceTestResponseTimes.py @@ -0,0 +1,49 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from typing import Optional + +from attr import define +import attr + +@define +class PerformanceTestResponseTimes: + """ + An object that describes test response times. + + https://developer.apple.com/documentation/retentionmessaging/performancetestresponsetimes + """ + + average: Optional[int] = attr.ib(default=None) + """ + Average response time in milliseconds. + + https://developer.apple.com/documentation/retentionmessaging/average + """ + + p50: Optional[int] = attr.ib(default=None) + """ + The 50th percentile response time in milliseconds. + + https://developer.apple.com/documentation/retentionmessaging/p50 + """ + + p90: Optional[int] = attr.ib(default=None) + """ + The 90th percentile response time in milliseconds. + + https://developer.apple.com/documentation/retentionmessaging/p90 + """ + + p95: Optional[int] = attr.ib(default=None) + """ + The 95th percentile response time in milliseconds. + + https://developer.apple.com/documentation/retentionmessaging/p95 + """ + + p99: Optional[int] = attr.ib(default=None) + """ + The 99th percentile response time in milliseconds. + + https://developer.apple.com/documentation/retentionmessaging/p99 + """ diff --git a/appstoreserverlibrary/models/PerformanceTestResultResponse.py b/appstoreserverlibrary/models/PerformanceTestResultResponse.py new file mode 100644 index 00000000..bd246b8d --- /dev/null +++ b/appstoreserverlibrary/models/PerformanceTestResultResponse.py @@ -0,0 +1,96 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from typing import Dict, Optional + +from attr import define, Attribute +import attr + +from .LibraryUtility import AttrsRawValueAware, metadata_key, metadata_type_key +from .PerformanceTestConfig import PerformanceTestConfig +from .PerformanceTestResponseTimes import PerformanceTestResponseTimes +from .PerformanceTestStatus import PerformanceTestStatus +from .SendAttemptResult import SendAttemptResult + +def _failures_value_set(self, _: Attribute, value: Optional[Dict[SendAttemptResult, int]]): + new_raw = {k.value: v for k, v in value.items()} if value is not None else None + if new_raw != getattr(self, 'rawFailures'): + object.__setattr__(self, 'rawFailures', new_raw) + return value + +def _raw_failures_value_set(self, _: Attribute, value: Optional[Dict[str, int]]): + new_typed = {} + if value is not None: + for k, v in value.items(): + if k in SendAttemptResult: + new_typed[SendAttemptResult(k)] = v + new_typed = new_typed if new_typed else None + if new_typed != getattr(self, 'failures'): + object.__setattr__(self, 'failures', new_typed) + return value + +@define +class PerformanceTestResultResponse(AttrsRawValueAware): + """ + An object the API returns that describes the performance test results. + + https://developer.apple.com/documentation/retentionmessaging/performancetestresultresponse + """ + + config: Optional[PerformanceTestConfig] = attr.ib(default=None) + """ + A PerformanceTestConfig object that enumerates the test parameters. + + https://developer.apple.com/documentation/retentionmessaging/performancetestconfig + """ + + target: Optional[str] = attr.ib(default=None) + """ + The target URL for the performance test. + + https://developer.apple.com/documentation/retentionmessaging/target + """ + + result: Optional[PerformanceTestStatus] = PerformanceTestStatus.create_main_attr('rawResult') + """ + A PerformanceTestStatus object that describes the overall result of the test. + + https://developer.apple.com/documentation/retentionmessaging/performanceteststatus + """ + + rawResult: Optional[str] = PerformanceTestStatus.create_raw_attr('result') + """ + See result + """ + + successRate: Optional[int] = attr.ib(default=None) + """ + An integer that describes he success rate percentage of the performance test. + + https://developer.apple.com/documentation/retentionmessaging/successrate + """ + + numPending: Optional[int] = attr.ib(default=None) + """ + An integer that describes the number of pending requests in the performance test. + + https://developer.apple.com/documentation/retentionmessaging/numpending + """ + + responseTimes: Optional[PerformanceTestResponseTimes] = attr.ib(default=None) + """ + A PerformanceTestResponseTimes object that enumerates the response times measured during the test. + + https://developer.apple.com/documentation/retentionmessaging/performancetestresponsetimes + """ + + failures: Optional[Dict[SendAttemptResult, int]] = attr.ib(default=None, on_setattr=_failures_value_set, metadata={metadata_key: 'rawFailures', metadata_type_key: 'main'}) + """ + A map of server-to-server notification failure reasons and counts that represent the number of failures encountered during the performance test. + + https://developer.apple.com/documentation/retentionmessaging/failures + """ + + rawFailures: Optional[Dict[str, int]] = attr.ib(default=None, kw_only=True, on_setattr=_raw_failures_value_set, metadata={metadata_key: 'failures', metadata_type_key: 'raw'}) + """ + See failures + """ diff --git a/appstoreserverlibrary/models/PerformanceTestStatus.py b/appstoreserverlibrary/models/PerformanceTestStatus.py new file mode 100644 index 00000000..92c9501c --- /dev/null +++ b/appstoreserverlibrary/models/PerformanceTestStatus.py @@ -0,0 +1,15 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from enum import Enum + +from .LibraryUtility import AppStoreServerLibraryEnumMeta + +class PerformanceTestStatus(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): + """ + The status of the performance test. + + https://developer.apple.com/documentation/retentionmessaging/performanceteststatus + """ + PENDING = "PENDING" + PASS = "PASS" + FAIL = "FAIL" diff --git a/appstoreserverlibrary/models/RealtimeUrlRequest.py b/appstoreserverlibrary/models/RealtimeUrlRequest.py new file mode 100644 index 00000000..36a64463 --- /dev/null +++ b/appstoreserverlibrary/models/RealtimeUrlRequest.py @@ -0,0 +1,19 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from attr import define +import attr + +@define +class RealtimeUrlRequest: + """ + The request body for configuring the URL of your Get Retention Message endpoint. + + https://developer.apple.com/documentation/retentionmessaging/realtimeurlrequest + """ + + realtimeURL: str = attr.ib(validator=attr.validators.max_len(256)) + """ + A string that contains the URL of your Get Retention Message endpoint for configuration. + + https://developer.apple.com/documentation/retentionmessaging/realtimeurl + """ diff --git a/appstoreserverlibrary/models/RealtimeUrlResponse.py b/appstoreserverlibrary/models/RealtimeUrlResponse.py new file mode 100644 index 00000000..5cd75f07 --- /dev/null +++ b/appstoreserverlibrary/models/RealtimeUrlResponse.py @@ -0,0 +1,21 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from typing import Optional + +from attr import define +import attr + +@define +class RealtimeUrlResponse: + """ + The response body that contains the URL for your Get Retention Message endpoint. + + https://developer.apple.com/documentation/retentionmessaging/realtimeurlresponse + """ + + realtimeURL: Optional[str] = attr.ib() + """ + A string that contains the URL you provided for your Get Retention Message endpoint. + + https://developer.apple.com/documentation/retentionmessaging/realtimeurl + """ diff --git a/appstoreserverlibrary/models/UploadMessageRequestBody.py b/appstoreserverlibrary/models/UploadMessageRequestBody.py index 7d9472e4..2cc17f67 100644 --- a/appstoreserverlibrary/models/UploadMessageRequestBody.py +++ b/appstoreserverlibrary/models/UploadMessageRequestBody.py @@ -1,14 +1,17 @@ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. -from typing import Optional +from typing import List, Optional from attr import define import attr +from .BulletPoint import BulletPoint +from .HeaderPosition import HeaderPosition +from .LibraryUtility import AttrsRawValueAware from .UploadMessageImage import UploadMessageImage @define -class UploadMessageRequestBody: +class UploadMessageRequestBody(AttrsRawValueAware): """ The request body for uploading a message, which includes the message text and an optional image reference. @@ -35,3 +38,22 @@ class UploadMessageRequestBody: https://developer.apple.com/documentation/retentionmessaging/uploadmessageimage """ + + headerPosition: Optional[HeaderPosition] = HeaderPosition.create_main_attr('rawHeaderPosition') + """ + The position of header text, which defaults to placing header text above the body. + + https://developer.apple.com/documentation/retentionmessaging/headerposition + """ + + rawHeaderPosition: Optional[str] = HeaderPosition.create_raw_attr('headerPosition') + """ + See headerPosition + """ + + bulletPoints: Optional[List[BulletPoint]] = attr.ib(default=None, validator=attr.validators.optional(attr.validators.max_len(5))) + """ + An optional array of bullet points. + + https://developer.apple.com/documentation/retentionmessaging/bulletpoint + """ diff --git a/tests/resources/models/getDefaultMessageResponse.json b/tests/resources/models/getDefaultMessageResponse.json new file mode 100644 index 00000000..e3d2954b --- /dev/null +++ b/tests/resources/models/getDefaultMessageResponse.json @@ -0,0 +1,3 @@ +{ + "messageIdentifier": "a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890" +} diff --git a/tests/resources/models/getImageListResponse.json b/tests/resources/models/getImageListResponse.json index 652d2e96..b668dd12 100644 --- a/tests/resources/models/getImageListResponse.json +++ b/tests/resources/models/getImageListResponse.json @@ -2,7 +2,8 @@ "imageIdentifiers": [ { "imageIdentifier": "a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890", - "imageState": "APPROVED" + "imageState": "APPROVED", + "imageSize": "FULL_SIZE" } ] } diff --git a/tests/resources/models/getRealtimeUrlResponse.json b/tests/resources/models/getRealtimeUrlResponse.json new file mode 100644 index 00000000..699d7fb5 --- /dev/null +++ b/tests/resources/models/getRealtimeUrlResponse.json @@ -0,0 +1,3 @@ +{ + "realtimeURL": "https://example.com/realtime" +} diff --git a/tests/resources/models/performanceTestResponse.json b/tests/resources/models/performanceTestResponse.json new file mode 100644 index 00000000..09973994 --- /dev/null +++ b/tests/resources/models/performanceTestResponse.json @@ -0,0 +1,10 @@ +{ + "config": { + "maxConcurrentRequests": 10, + "totalRequests": 100, + "totalDuration": 60000, + "responseTimeThreshold": 500, + "successRateThreshold": 95 + }, + "requestId": "c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d" +} diff --git a/tests/resources/models/performanceTestResultResponse.json b/tests/resources/models/performanceTestResultResponse.json new file mode 100644 index 00000000..a2ac006a --- /dev/null +++ b/tests/resources/models/performanceTestResultResponse.json @@ -0,0 +1,24 @@ +{ + "config": { + "maxConcurrentRequests": 10, + "totalRequests": 100, + "totalDuration": 60000, + "responseTimeThreshold": 500, + "successRateThreshold": 95 + }, + "target": "https://example.com/retention", + "result": "PASS", + "successRate": 98, + "numPending": 0, + "responseTimes": { + "average": 120, + "p50": 100, + "p90": 200, + "p95": 250, + "p99": 400 + }, + "failures": { + "TIMED_OUT": 1, + "NO_RESPONSE": 1 + } +} diff --git a/tests/test_api_client.py b/tests/test_api_client.py index 3d683c57..c0fbb7d9 100644 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -36,8 +36,14 @@ from appstoreserverlibrary.models.UpdateAppAccountTokenRequest import UpdateAppAccountTokenRequest from appstoreserverlibrary.models.UserStatus import UserStatus from appstoreserverlibrary.models.DefaultConfigurationRequest import DefaultConfigurationRequest +from appstoreserverlibrary.models.HeaderPosition import HeaderPosition +from appstoreserverlibrary.models.ImageSize import ImageSize from appstoreserverlibrary.models.ImageState import ImageState from appstoreserverlibrary.models.MessageState import MessageState +from appstoreserverlibrary.models.BulletPoint import BulletPoint +from appstoreserverlibrary.models.PerformanceTestRequest import PerformanceTestRequest +from appstoreserverlibrary.models.PerformanceTestStatus import PerformanceTestStatus +from appstoreserverlibrary.models.RealtimeUrlRequest import RealtimeUrlRequest from appstoreserverlibrary.models.UploadMessageImage import UploadMessageImage from appstoreserverlibrary.models.UploadMessageRequestBody import UploadMessageRequestBody from uuid import UUID @@ -597,6 +603,7 @@ def test_get_image_list(self): self.assertEqual(1, len(response.imageIdentifiers)) self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.imageIdentifiers[0].imageIdentifier) self.assertEqual(ImageState.APPROVED, response.imageIdentifiers[0].imageState) + self.assertEqual(ImageSize.FULL_SIZE, response.imageIdentifiers[0].imageSize) def test_upload_message(self): client = self.get_client_with_body(b'', @@ -654,6 +661,113 @@ def test_delete_default_message(self): None) client.delete_default_message('com.example.product', 'en-US') + def test_get_default_message(self): + client = self.get_client_with_body_from_file('tests/resources/models/getDefaultMessageResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v1/messaging/default/com.example.product/en-US', + {}, + None) + response = client.get_default_message('com.example.product', 'en-US') + self.assertIsNotNone(response) + self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.messageIdentifier) + + def test_upload_image_with_image_size(self): + client = self.get_client_with_body(b'', + 'PUT', + 'https://local-testing-base-url/inApps/v1/messaging/image/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', + {'imageSize': ['FULL_SIZE']}, + None, + 200, + bytes([1, 2, 3]), + 'image/png') + client.upload_image(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), bytes([1, 2, 3]), ImageSize.FULL_SIZE) + + def test_configure_realtime_url(self): + client = self.get_client_with_body(b'', + 'PUT', + 'https://local-testing-base-url/inApps/v1/messaging/realtime/url', + {}, + {'realtimeURL': 'https://example.com/realtime'}) + realtime_url_request = RealtimeUrlRequest(realtimeURL='https://example.com/realtime') + client.configure_realtime_url(realtime_url_request) + + def test_delete_realtime_url(self): + client = self.get_client_with_body(b'', + 'DELETE', + 'https://local-testing-base-url/inApps/v1/messaging/realtime/url', + {}, + None) + client.delete_realtime_url() + + def test_get_realtime_url(self): + client = self.get_client_with_body_from_file('tests/resources/models/getRealtimeUrlResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v1/messaging/realtime/url', + {}, + None) + response = client.get_realtime_url() + self.assertIsNotNone(response) + self.assertEqual('https://example.com/realtime', response.realtimeURL) + + def test_upload_message_with_bullet_points(self): + client = self.get_client_with_body(b'', + 'PUT', + 'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', + {}, + {'header': 'Header text', 'body': 'Body text', + 'image': {'imageIdentifier': 'b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901', 'altText': 'Alt text'}, + 'headerPosition': 'ABOVE_IMAGE', + 'bulletPoints': [{'text': 'Bullet 1', 'imageIdentifier': 'c3d4e5f6-a7b8-9012-c3d4-e5f6a7b89012', 'altText': 'Bullet alt'}]}) + image = UploadMessageImage(imageIdentifier=UUID('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901'), altText='Alt text') + bullet_point = BulletPoint(text='Bullet 1', imageIdentifier=UUID('c3d4e5f6-a7b8-9012-c3d4-e5f6a7b89012'), altText='Bullet alt') + upload_message_request_body = UploadMessageRequestBody(header='Header text', body='Body text', image=image, headerPosition=HeaderPosition.ABOVE_IMAGE, bulletPoints=[bullet_point]) + client.upload_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), upload_message_request_body) + + def test_initiate_performance_test(self): + client = self.get_client_with_body_from_file('tests/resources/models/performanceTestResponse.json', + 'POST', + 'https://local-testing-base-url/inApps/v1/messaging/performanceTest', + {}, + {'originalTransactionId': '70000500092808'}) + performance_test_request = PerformanceTestRequest(originalTransactionId='70000500092808') + response = client.initiate_performance_test(performance_test_request) + self.assertIsNotNone(response) + self.assertEqual('c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d', response.requestId) + self.assertIsNotNone(response.config) + self.assertEqual(10, response.config.maxConcurrentRequests) + self.assertEqual(100, response.config.totalRequests) + self.assertEqual(60000, response.config.totalDuration) + self.assertEqual(500, response.config.responseTimeThreshold) + self.assertEqual(95, response.config.successRateThreshold) + + def test_get_performance_test_results(self): + client = self.get_client_with_body_from_file('tests/resources/models/performanceTestResultResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v1/messaging/performanceTest/result/c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d', + {}, + None) + response = client.get_performance_test_results('c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d') + self.assertIsNotNone(response) + self.assertIsNotNone(response.config) + self.assertEqual(10, response.config.maxConcurrentRequests) + self.assertEqual(100, response.config.totalRequests) + self.assertEqual(60000, response.config.totalDuration) + self.assertEqual(500, response.config.responseTimeThreshold) + self.assertEqual(95, response.config.successRateThreshold) + self.assertEqual('https://example.com/retention', response.target) + self.assertEqual(PerformanceTestStatus.PASS, response.result) + self.assertEqual('PASS', response.rawResult) + self.assertEqual(98, response.successRate) + self.assertEqual(0, response.numPending) + self.assertIsNotNone(response.responseTimes) + self.assertEqual(120, response.responseTimes.average) + self.assertEqual(100, response.responseTimes.p50) + self.assertEqual(200, response.responseTimes.p90) + self.assertEqual(250, response.responseTimes.p95) + self.assertEqual(400, response.responseTimes.p99) + self.assertEqual({SendAttemptResult.TIMED_OUT: 1, SendAttemptResult.NO_RESPONSE: 1}, response.failures) + self.assertEqual({'TIMED_OUT': 1, 'NO_RESPONSE': 1}, response.rawFailures) + def test_get_app_transaction_info_success(self): client = self.get_client_with_body_from_file('tests/resources/models/appTransactionInfoResponse.json', 'GET', diff --git a/tests/test_api_client_async.py b/tests/test_api_client_async.py index f6a9ad1b..409e9e8e 100644 --- a/tests/test_api_client_async.py +++ b/tests/test_api_client_async.py @@ -42,8 +42,14 @@ from appstoreserverlibrary.models.UpdateAppAccountTokenRequest import UpdateAppAccountTokenRequest from appstoreserverlibrary.models.UserStatus import UserStatus from appstoreserverlibrary.models.DefaultConfigurationRequest import DefaultConfigurationRequest +from appstoreserverlibrary.models.HeaderPosition import HeaderPosition +from appstoreserverlibrary.models.ImageSize import ImageSize from appstoreserverlibrary.models.ImageState import ImageState from appstoreserverlibrary.models.MessageState import MessageState +from appstoreserverlibrary.models.BulletPoint import BulletPoint +from appstoreserverlibrary.models.PerformanceTestRequest import PerformanceTestRequest +from appstoreserverlibrary.models.PerformanceTestStatus import PerformanceTestStatus +from appstoreserverlibrary.models.RealtimeUrlRequest import RealtimeUrlRequest from appstoreserverlibrary.models.UploadMessageImage import UploadMessageImage from appstoreserverlibrary.models.UploadMessageRequestBody import UploadMessageRequestBody from uuid import UUID @@ -602,6 +608,7 @@ async def test_get_image_list(self): self.assertEqual(1, len(response.imageIdentifiers)) self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.imageIdentifiers[0].imageIdentifier) self.assertEqual(ImageState.APPROVED, response.imageIdentifiers[0].imageState) + self.assertEqual(ImageSize.FULL_SIZE, response.imageIdentifiers[0].imageSize) async def test_upload_message(self): client = self.get_client_with_body(b'', @@ -658,7 +665,114 @@ async def test_delete_default_message(self): {}, None) await client.delete_default_message('com.example.product', 'en-US') - + + async def test_get_default_message(self): + client = self.get_client_with_body_from_file('tests/resources/models/getDefaultMessageResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v1/messaging/default/com.example.product/en-US', + {}, + None) + response = await client.get_default_message('com.example.product', 'en-US') + self.assertIsNotNone(response) + self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.messageIdentifier) + + async def test_upload_image_with_image_size(self): + client = self.get_client_with_body(b'', + 'PUT', + 'https://local-testing-base-url/inApps/v1/messaging/image/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', + {'imageSize': ['FULL_SIZE']}, + None, + 200, + bytes([1, 2, 3]), + 'image/png') + await client.upload_image(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), bytes([1, 2, 3]), ImageSize.FULL_SIZE) + + async def test_configure_realtime_url(self): + client = self.get_client_with_body(b'', + 'PUT', + 'https://local-testing-base-url/inApps/v1/messaging/realtime/url', + {}, + {'realtimeURL': 'https://example.com/realtime'}) + realtime_url_request = RealtimeUrlRequest(realtimeURL='https://example.com/realtime') + await client.configure_realtime_url(realtime_url_request) + + async def test_delete_realtime_url(self): + client = self.get_client_with_body(b'', + 'DELETE', + 'https://local-testing-base-url/inApps/v1/messaging/realtime/url', + {}, + None) + await client.delete_realtime_url() + + async def test_get_realtime_url(self): + client = self.get_client_with_body_from_file('tests/resources/models/getRealtimeUrlResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v1/messaging/realtime/url', + {}, + None) + response = await client.get_realtime_url() + self.assertIsNotNone(response) + self.assertEqual('https://example.com/realtime', response.realtimeURL) + + async def test_upload_message_with_bullet_points(self): + client = self.get_client_with_body(b'', + 'PUT', + 'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', + {}, + {'header': 'Header text', 'body': 'Body text', + 'image': {'imageIdentifier': 'b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901', 'altText': 'Alt text'}, + 'headerPosition': 'ABOVE_IMAGE', + 'bulletPoints': [{'text': 'Bullet 1', 'imageIdentifier': 'c3d4e5f6-a7b8-9012-c3d4-e5f6a7b89012', 'altText': 'Bullet alt'}]}) + image = UploadMessageImage(imageIdentifier=UUID('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901'), altText='Alt text') + bullet_point = BulletPoint(text='Bullet 1', imageIdentifier=UUID('c3d4e5f6-a7b8-9012-c3d4-e5f6a7b89012'), altText='Bullet alt') + upload_message_request_body = UploadMessageRequestBody(header='Header text', body='Body text', image=image, headerPosition=HeaderPosition.ABOVE_IMAGE, bulletPoints=[bullet_point]) + await client.upload_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), upload_message_request_body) + + async def test_initiate_performance_test(self): + client = self.get_client_with_body_from_file('tests/resources/models/performanceTestResponse.json', + 'POST', + 'https://local-testing-base-url/inApps/v1/messaging/performanceTest', + {}, + {'originalTransactionId': '70000500092808'}) + performance_test_request = PerformanceTestRequest(originalTransactionId='70000500092808') + response = await client.initiate_performance_test(performance_test_request) + self.assertIsNotNone(response) + self.assertEqual('c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d', response.requestId) + self.assertIsNotNone(response.config) + self.assertEqual(10, response.config.maxConcurrentRequests) + self.assertEqual(100, response.config.totalRequests) + self.assertEqual(60000, response.config.totalDuration) + self.assertEqual(500, response.config.responseTimeThreshold) + self.assertEqual(95, response.config.successRateThreshold) + + async def test_get_performance_test_results(self): + client = self.get_client_with_body_from_file('tests/resources/models/performanceTestResultResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v1/messaging/performanceTest/result/c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d', + {}, + None) + response = await client.get_performance_test_results('c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d') + self.assertIsNotNone(response) + self.assertIsNotNone(response.config) + self.assertEqual(10, response.config.maxConcurrentRequests) + self.assertEqual(100, response.config.totalRequests) + self.assertEqual(60000, response.config.totalDuration) + self.assertEqual(500, response.config.responseTimeThreshold) + self.assertEqual(95, response.config.successRateThreshold) + self.assertEqual('https://example.com/retention', response.target) + self.assertEqual(PerformanceTestStatus.PASS, response.result) + self.assertEqual('PASS', response.rawResult) + self.assertEqual(98, response.successRate) + self.assertEqual(0, response.numPending) + self.assertIsNotNone(response.responseTimes) + self.assertEqual(120, response.responseTimes.average) + self.assertEqual(100, response.responseTimes.p50) + self.assertEqual(200, response.responseTimes.p90) + self.assertEqual(250, response.responseTimes.p95) + self.assertEqual(400, response.responseTimes.p99) + self.assertEqual({SendAttemptResult.TIMED_OUT: 1, SendAttemptResult.NO_RESPONSE: 1}, response.failures) + self.assertEqual({'TIMED_OUT': 1, 'NO_RESPONSE': 1}, response.rawFailures) + async def test_get_app_transaction_info_success(self): client = self.get_client_with_body_from_file('tests/resources/models/appTransactionInfoResponse.json', 'GET',