diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e9a429..e8bfe76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.13.0 + +Adds custom icons and interruption levels for Push Notifications. Set `icon` to show a service logo or avatar on the notification, and `interruption_level` to deliver it as `passive`, `active`, or `time-sensitive`. + ## 1.12.0 Adds the new `value` Live Activity type. diff --git a/README.md b/README.md index 68c8bc8..b949f7d 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,20 @@ activitysmith.notifications.send( ) ``` +### Custom Push Notification Icons + +![Push Notifications with custom icons](https://cdn.activitysmith.com/features/push-notifications-with-custom-icons.png) + +Set `icon` to a publicly accessible HTTPS image URL to show an avatar or service logo alongside a smaller ActivitySmith app icon. With a custom icon, iOS may omit the subtitle from the notification. + +```python +activitysmith.notifications.send( + title="MRR just crossed $10,000 🎉", + message="Up 18% this month from 31 new subscriptions.", + icon="https://cdn.activitysmith.com/integrations/icons/stripe.png", +) +``` + ### Rich Push Notifications with Media ![Rich Push Notification with image](https://cdn.activitysmith.com/features/rich-push-notification-with-image.png) @@ -134,23 +148,85 @@ activitysmith.notifications.send( ) ``` +### Push Notification Interruption Levels + +![Time Sensitive Push Notification](https://cdn.activitysmith.com/features/time-sensitive-push-notifications.png) + +Use `interruption_level` to choose how urgently iOS presents a Push Notification. Omit it for normal delivery. Explicit `active` has the same behavior as the default. + +| Value | Behavior | +| --- | --- | +| `passive` | Appears quietly in the notification list, without sound or waking the screen. | +| `active` | Normal delivery, subject to the device's notification settings and Focus. This is the default. | +| `time-sensitive` | Can bypass Scheduled Summary and Focus when the user allows Time Sensitive Notifications for ActivitySmith. | + +See Apple's [interruption levels](https://developer.apple.com/documentation/usernotifications/unnotificationinterruptionlevel) for the system behavior. + +The SDK also exports these values as `PushInterruptionLevel.PASSIVE`, `PushInterruptionLevel.ACTIVE`, and `PushInterruptionLevel.TIME_SENSITIVE`. + +#### Passive + +```python +activitysmith.notifications.send( + title="Daily summary", + message="All scheduled jobs completed successfully.", + interruption_level="passive", +) +``` + +#### Active (default) + +```python +activitysmith.notifications.send( + title="Deployment complete", + message="Your latest changes are live.", + interruption_level="active", +) +``` + +Removing the interruption level from this example produces the same delivery behavior. + +#### Time Sensitive + +```python +activitysmith.notifications.send( + title="🚨 checkout-api is down", + message="500 errors on 62% of requests. Customers can't complete payments.", + interruption_level="time-sensitive", +) +``` + ## Live Activities Choose the Live Activity type that matches what you want to show: -- ![Value Live Activity showing revenue with a growth badge](https://cdn.activitysmith.com/features/value-live-activity.png) **Value**: Show a single value on your Lock Screen, such as revenue, a queue count, or a temperature. +![Value Live Activity showing revenue with a growth badge](https://cdn.activitysmith.com/features/value-live-activity.png) + +**Value**: Show a single value on your Lock Screen, such as revenue, a queue count, or a temperature. + +![Stats Live Activity with six labeled sales metrics](https://cdn.activitysmith.com/features/stats-live-activity.png) + +**Stats**: Show up to 8 labeled values on your Lock Screen, from revenue and orders to uptime and conversion. + +![Alert Live Activity showing a customer reactivation update](https://cdn.activitysmith.com/features/alert-live-activity.png) + +**Alert**: Show status updates with a clear message, badge, and icon. When you add an action button, `color` controls the button tint. + +![Metrics Live Activity with CPU and memory values](https://cdn.activitysmith.com/features/metrics-live-activity-start.png) + +**Metrics**: Track two related values with segmented bars, such as CPU and memory. -- ![Stats Live Activity with six labeled sales metrics](https://cdn.activitysmith.com/features/stats-live-activity.png) **Stats**: Show up to 8 labeled values on your Lock Screen, from revenue and orders to uptime and conversion. +![Segmented Progress Live Activity showing a workflow step](https://cdn.activitysmith.com/features/update-live-activity.png) -- ![Alert Live Activity showing a customer reactivation update](https://cdn.activitysmith.com/features/alert-live-activity.png) **Alert**: Show status updates with a clear message, badge, and icon. When you add an action button, `color` controls the button tint. +**Segmented Progress**: Show progress through a known set of steps, like build, test, deploy, and verify. -- ![Metrics Live Activity with CPU and memory values](https://cdn.activitysmith.com/features/metrics-live-activity-start.png) **Metrics**: Track two related values with segmented bars, such as CPU and memory. +![Progress Live Activity showing percentage completion](https://cdn.activitysmith.com/features/progress-live-activity.png) -- ![Segmented Progress Live Activity showing a workflow step](https://cdn.activitysmith.com/features/update-live-activity.png) **Segmented Progress**: Show progress through a known set of steps, like build, test, deploy, and verify. +**Progress**: Show percentage progress for jobs that move continuously toward completion. -- ![Progress Live Activity showing percentage completion](https://cdn.activitysmith.com/features/progress-live-activity.png) **Progress**: Show percentage progress for jobs that move continuously toward completion. +![Timer Live Activity showing a benchmark run countdown](https://cdn.activitysmith.com/features/timer-live-activity.png) -- ![Timer Live Activity showing a benchmark run countdown](https://cdn.activitysmith.com/features/timer-live-activity.png) **Timer**: Count down from a duration, or count up from 00:00 while a job runs. +**Timer**: Count down from a duration, or count up from 00:00 while a job runs. ### Start & Update Live Activity diff --git a/activitysmith/__init__.py b/activitysmith/__init__.py index 4672fc5..bae1404 100644 --- a/activitysmith/__init__.py +++ b/activitysmith/__init__.py @@ -1,6 +1,7 @@ from .client import ( ActivitySmith, LiveActivityColor, + PushInterruptionLevel, action, alert_badge, alert_icon, @@ -11,6 +12,7 @@ __all__ = [ "ActivitySmith", "LiveActivityColor", + "PushInterruptionLevel", "action", "alert_badge", "alert_icon", diff --git a/activitysmith/client.py b/activitysmith/client.py index 07187f4..39682db 100644 --- a/activitysmith/client.py +++ b/activitysmith/client.py @@ -16,7 +16,7 @@ from .normalization import normalize_live_activity_request, normalize_metric_request -SDK_VERSION = "1.12.0" +SDK_VERSION = "1.13.0" SDK_HEADER_NAME = "X-ActivitySmith-SDK" SDK_HEADER_VALUE = f"python-v{SDK_VERSION}" @@ -44,9 +44,17 @@ def _has_actions(request: Any) -> bool: return True +_PUSH_INTERRUPTION_LEVELS = ("passive", "active", "time-sensitive") + + def _validate_push_request(request: Any) -> Any: if _has_media(request) and _has_actions(request): raise ValueError("ActivitySmith: media cannot be combined with actions") + level = _request_value(request, "interruption_level") + if hasattr(level, "value"): + level = level.value + if level is not None and level not in _PUSH_INTERRUPTION_LEVELS: + raise ValueError("ActivitySmith: interruption_level must be passive, active, or time-sensitive") return request @@ -222,6 +230,8 @@ def _build_push_request( channels: Any | None = None, tags: Any | None = None, metadata: Any | None = None, + icon: Any | None = None, + interruption_level: Any | None = None, ) -> Any: request_fields = _compact_dict( { @@ -235,6 +245,8 @@ def _build_push_request( "channels": channels, "tags": tags, "metadata": metadata, + "icon": icon, + "interruption_level": interruption_level, } ) @@ -254,6 +266,14 @@ def _build_push_request( return normalized +class PushInterruptionLevel: + """Push Notification interruption levels.""" + + PASSIVE = "passive" + ACTIVE = "active" + TIME_SENSITIVE = "time-sensitive" + + class LiveActivityColor: LIME = "lime" GREEN = "green" @@ -390,6 +410,8 @@ def send( channels: Any | None = None, tags: Any | None = None, metadata: Any | None = None, + icon: Any | None = None, + interruption_level: Any | None = None, ): request = _build_push_request( request, @@ -403,6 +425,8 @@ def send( channels=channels, tags=tags, metadata=metadata, + icon=icon, + interruption_level=interruption_level, ) normalized = normalize_metadata_request(_validate_push_request(_normalize_channels_target(request))) return self._api.send_push_notification( diff --git a/activitysmith_openapi/__init__.py b/activitysmith_openapi/__init__.py index be074a0..da2e10f 100644 --- a/activitysmith_openapi/__init__.py +++ b/activitysmith_openapi/__init__.py @@ -14,7 +14,7 @@ """ # noqa: E501 -__version__ = "1.12.0" +__version__ = "1.13.0" # import apis into sdk package from activitysmith_openapi.api.app_icon_badges_api import AppIconBadgesApi @@ -41,6 +41,8 @@ from activitysmith_openapi.models.app_icon_badge_count_update_request import AppIconBadgeCountUpdateRequest from activitysmith_openapi.models.app_icon_badge_count_update_response import AppIconBadgeCountUpdateResponse from activitysmith_openapi.models.bad_request_error import BadRequestError +from activitysmith_openapi.models.billing_blocked_error import BillingBlockedError +from activitysmith_openapi.models.billing_blocked_error_trial_period import BillingBlockedErrorTrialPeriod from activitysmith_openapi.models.channel_target import ChannelTarget from activitysmith_openapi.models.content_state_end import ContentStateEnd from activitysmith_openapi.models.content_state_start import ContentStateStart @@ -71,6 +73,7 @@ from activitysmith_openapi.models.metric_value_update_response import MetricValueUpdateResponse from activitysmith_openapi.models.no_recipients_error import NoRecipientsError from activitysmith_openapi.models.not_found_error import NotFoundError +from activitysmith_openapi.models.push_interruption_level import PushInterruptionLevel from activitysmith_openapi.models.push_notification_action import PushNotificationAction from activitysmith_openapi.models.push_notification_action_type import PushNotificationActionType from activitysmith_openapi.models.push_notification_request import PushNotificationRequest diff --git a/activitysmith_openapi/api/app_icon_badges_api.py b/activitysmith_openapi/api/app_icon_badges_api.py index 812dc28..0053614 100644 --- a/activitysmith_openapi/api/app_icon_badges_api.py +++ b/activitysmith_openapi/api/app_icon_badges_api.py @@ -93,6 +93,7 @@ def update_app_icon_badge_count( _response_types_map: Dict[str, Optional[str]] = { '200': "AppIconBadgeCountUpdateResponse", '400': "BadRequestError", + '402': "BillingBlockedError", '403': "ForbiddenError", '422': "UpdateAppIconBadgeCount422Response", '429': "RateLimitError", @@ -165,6 +166,7 @@ def update_app_icon_badge_count_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "AppIconBadgeCountUpdateResponse", '400': "BadRequestError", + '402': "BillingBlockedError", '403': "ForbiddenError", '422': "UpdateAppIconBadgeCount422Response", '429': "RateLimitError", @@ -237,6 +239,7 @@ def update_app_icon_badge_count_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "AppIconBadgeCountUpdateResponse", '400': "BadRequestError", + '402': "BillingBlockedError", '403': "ForbiddenError", '422': "UpdateAppIconBadgeCount422Response", '429': "RateLimitError", diff --git a/activitysmith_openapi/api/live_activities_api.py b/activitysmith_openapi/api/live_activities_api.py index 05991eb..db54610 100644 --- a/activitysmith_openapi/api/live_activities_api.py +++ b/activitysmith_openapi/api/live_activities_api.py @@ -682,8 +682,9 @@ def reconcile_live_activity_stream( _response_types_map: Dict[str, Optional[str]] = { '200': "LiveActivityStreamPutResponse", '400': "BadRequestError", + '402': "BillingBlockedError", '403': "ForbiddenError", - '404': "NoRecipientsError", + '422': "NoRecipientsError", '429': "SendPushNotification429Response", } response_data = self.api_client.call_api( @@ -757,8 +758,9 @@ def reconcile_live_activity_stream_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "LiveActivityStreamPutResponse", '400': "BadRequestError", + '402': "BillingBlockedError", '403': "ForbiddenError", - '404': "NoRecipientsError", + '422': "NoRecipientsError", '429': "SendPushNotification429Response", } response_data = self.api_client.call_api( @@ -832,8 +834,9 @@ def reconcile_live_activity_stream_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "LiveActivityStreamPutResponse", '400': "BadRequestError", + '402': "BillingBlockedError", '403': "ForbiddenError", - '404': "NoRecipientsError", + '422': "NoRecipientsError", '429': "SendPushNotification429Response", } response_data = self.api_client.call_api( @@ -977,8 +980,9 @@ def start_live_activity( _response_types_map: Dict[str, Optional[str]] = { '200': "LiveActivityStartResponse", '400': "BadRequestError", + '402': "BillingBlockedError", '403': "ForbiddenError", - '404': "NoRecipientsError", + '422': "NoRecipientsError", '429': "SendPushNotification429Response", } response_data = self.api_client.call_api( @@ -1048,8 +1052,9 @@ def start_live_activity_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "LiveActivityStartResponse", '400': "BadRequestError", + '402': "BillingBlockedError", '403': "ForbiddenError", - '404': "NoRecipientsError", + '422': "NoRecipientsError", '429': "SendPushNotification429Response", } response_data = self.api_client.call_api( @@ -1119,8 +1124,9 @@ def start_live_activity_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "LiveActivityStartResponse", '400': "BadRequestError", + '402': "BillingBlockedError", '403': "ForbiddenError", - '404': "NoRecipientsError", + '422': "NoRecipientsError", '429': "SendPushNotification429Response", } response_data = self.api_client.call_api( diff --git a/activitysmith_openapi/api/push_notifications_api.py b/activitysmith_openapi/api/push_notifications_api.py index 63bce4b..4a65371 100644 --- a/activitysmith_openapi/api/push_notifications_api.py +++ b/activitysmith_openapi/api/push_notifications_api.py @@ -56,7 +56,7 @@ def send_push_notification( ) -> PushNotificationResponse: """Send a push notification - Sends a push notification to devices matched by API key scope and optional target channels. Supports optional redirection URL, optional media preview or playback when the notification is expanded, and up to 4 interactive actions. `media` cannot be combined with `actions`. Optional tags to organize and filter notification history. + Sends a push notification to devices matched by API key scope and optional target channels. Supports optional redirection URL, optional media preview or playback when the notification is expanded, and up to 4 interactive actions. `media` cannot be combined with `actions`. Optional `icon` shows a custom image, such as a service logo or avatar, next to a smaller app icon. Optional `interruption_level` sets how urgently iOS presents the notification. Optional tags to organize and filter notification history. :param push_notification_request: (required) :type push_notification_request: PushNotificationRequest @@ -93,8 +93,9 @@ def send_push_notification( _response_types_map: Dict[str, Optional[str]] = { '200': "PushNotificationResponse", '400': "BadRequestError", + '402': "BillingBlockedError", '403': "ForbiddenError", - '404': "NoRecipientsError", + '422': "NoRecipientsError", '429': "SendPushNotification429Response", } response_data = self.api_client.call_api( @@ -127,7 +128,7 @@ def send_push_notification_with_http_info( ) -> ApiResponse[PushNotificationResponse]: """Send a push notification - Sends a push notification to devices matched by API key scope and optional target channels. Supports optional redirection URL, optional media preview or playback when the notification is expanded, and up to 4 interactive actions. `media` cannot be combined with `actions`. Optional tags to organize and filter notification history. + Sends a push notification to devices matched by API key scope and optional target channels. Supports optional redirection URL, optional media preview or playback when the notification is expanded, and up to 4 interactive actions. `media` cannot be combined with `actions`. Optional `icon` shows a custom image, such as a service logo or avatar, next to a smaller app icon. Optional `interruption_level` sets how urgently iOS presents the notification. Optional tags to organize and filter notification history. :param push_notification_request: (required) :type push_notification_request: PushNotificationRequest @@ -164,8 +165,9 @@ def send_push_notification_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "PushNotificationResponse", '400': "BadRequestError", + '402': "BillingBlockedError", '403': "ForbiddenError", - '404': "NoRecipientsError", + '422': "NoRecipientsError", '429': "SendPushNotification429Response", } response_data = self.api_client.call_api( @@ -198,7 +200,7 @@ def send_push_notification_without_preload_content( ) -> RESTResponseType: """Send a push notification - Sends a push notification to devices matched by API key scope and optional target channels. Supports optional redirection URL, optional media preview or playback when the notification is expanded, and up to 4 interactive actions. `media` cannot be combined with `actions`. Optional tags to organize and filter notification history. + Sends a push notification to devices matched by API key scope and optional target channels. Supports optional redirection URL, optional media preview or playback when the notification is expanded, and up to 4 interactive actions. `media` cannot be combined with `actions`. Optional `icon` shows a custom image, such as a service logo or avatar, next to a smaller app icon. Optional `interruption_level` sets how urgently iOS presents the notification. Optional tags to organize and filter notification history. :param push_notification_request: (required) :type push_notification_request: PushNotificationRequest @@ -235,8 +237,9 @@ def send_push_notification_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "PushNotificationResponse", '400': "BadRequestError", + '402': "BillingBlockedError", '403': "ForbiddenError", - '404': "NoRecipientsError", + '422': "NoRecipientsError", '429': "SendPushNotification429Response", } response_data = self.api_client.call_api( diff --git a/activitysmith_openapi/api_client.py b/activitysmith_openapi/api_client.py index 0b0e3ef..ce1b024 100644 --- a/activitysmith_openapi/api_client.py +++ b/activitysmith_openapi/api_client.py @@ -88,7 +88,7 @@ def __init__( self.default_headers[header_name] = header_value self.cookie = cookie # Set default User-Agent. - self.user_agent = 'OpenAPI-Generator/1.12.0/python' + self.user_agent = 'OpenAPI-Generator/1.13.0/python' self.client_side_validation = configuration.client_side_validation def __enter__(self): diff --git a/activitysmith_openapi/configuration.py b/activitysmith_openapi/configuration.py index 541b52f..4ea33f8 100644 --- a/activitysmith_openapi/configuration.py +++ b/activitysmith_openapi/configuration.py @@ -400,7 +400,7 @@ def to_debug_report(self): "OS: {env}\n"\ "Python Version: {pyversion}\n"\ "Version of the API: 1.0.0\n"\ - "SDK Package Version: 1.12.0".\ + "SDK Package Version: 1.13.0".\ format(env=sys.platform, pyversion=sys.version) def get_host_settings(self): diff --git a/activitysmith_openapi/docs/AppIconBadgesApi.md b/activitysmith_openapi/docs/AppIconBadgesApi.md index 2f63367..0241b41 100644 --- a/activitysmith_openapi/docs/AppIconBadgesApi.md +++ b/activitysmith_openapi/docs/AppIconBadgesApi.md @@ -84,6 +84,7 @@ Name | Type | Description | Notes |-------------|-------------|------------------| **200** | App Icon Badge Count updated | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| **400** | Bad request | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| +**402** | Billing access required | - | **403** | Forbidden | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| **422** | No matching devices found, or a targeted device needs to reconnect | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| **429** | Rate limit exceeded | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
* Retry-After - Seconds to wait before retrying after a request-rate 429. Live Activity capacity errors are separate and require freeing device capacity; they do not provide a timed retry guarantee.
| diff --git a/activitysmith_openapi/docs/BillingBlockedError.md b/activitysmith_openapi/docs/BillingBlockedError.md new file mode 100644 index 0000000..43eebc0 --- /dev/null +++ b/activitysmith_openapi/docs/BillingBlockedError.md @@ -0,0 +1,32 @@ +# BillingBlockedError + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**error** | **str** | | +**message** | **str** | | +**trial_period** | [**BillingBlockedErrorTrialPeriod**](BillingBlockedErrorTrialPeriod.md) | | [optional] +**upgrade_url** | **str** | | + +## Example + +```python +from activitysmith_openapi.models.billing_blocked_error import BillingBlockedError + +# TODO update the JSON string below +json = "{}" +# create an instance of BillingBlockedError from a JSON string +billing_blocked_error_instance = BillingBlockedError.from_json(json) +# print the JSON string representation of the object +print(BillingBlockedError.to_json()) + +# convert the object into a dict +billing_blocked_error_dict = billing_blocked_error_instance.to_dict() +# create an instance of BillingBlockedError from a dict +billing_blocked_error_from_dict = BillingBlockedError.from_dict(billing_blocked_error_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/activitysmith_openapi/docs/BillingBlockedErrorTrialPeriod.md b/activitysmith_openapi/docs/BillingBlockedErrorTrialPeriod.md new file mode 100644 index 0000000..105e046 --- /dev/null +++ b/activitysmith_openapi/docs/BillingBlockedErrorTrialPeriod.md @@ -0,0 +1,30 @@ +# BillingBlockedErrorTrialPeriod + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**started_at** | **datetime** | | +**ends_at** | **datetime** | | + +## Example + +```python +from activitysmith_openapi.models.billing_blocked_error_trial_period import BillingBlockedErrorTrialPeriod + +# TODO update the JSON string below +json = "{}" +# create an instance of BillingBlockedErrorTrialPeriod from a JSON string +billing_blocked_error_trial_period_instance = BillingBlockedErrorTrialPeriod.from_json(json) +# print the JSON string representation of the object +print(BillingBlockedErrorTrialPeriod.to_json()) + +# convert the object into a dict +billing_blocked_error_trial_period_dict = billing_blocked_error_trial_period_instance.to_dict() +# create an instance of BillingBlockedErrorTrialPeriod from a dict +billing_blocked_error_trial_period_from_dict = BillingBlockedErrorTrialPeriod.from_dict(billing_blocked_error_trial_period_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/activitysmith_openapi/docs/LiveActivitiesApi.md b/activitysmith_openapi/docs/LiveActivitiesApi.md index 29e401a..bac4b47 100644 --- a/activitysmith_openapi/docs/LiveActivitiesApi.md +++ b/activitysmith_openapi/docs/LiveActivitiesApi.md @@ -255,8 +255,9 @@ Name | Type | Description | Notes |-------------|-------------|------------------| **200** | Stream reconciled | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| **400** | Bad request (invalid stream_key, payload, action, or channel targeting input) | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| +**402** | Trial expired, trial allowance exhausted, or paid usage cap reached | - | **403** | Forbidden (API key scope or channel assignment violation) | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| -**404** | No recipients found for effective channel target | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| +**422** | No recipients found for effective channel target | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| **429** | Rate limit exceeded | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
* Retry-After - Seconds to wait before retrying after a request-rate 429. Live Activity capacity errors are separate and require freeing device capacity; they do not provide a timed retry guarantee.
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) @@ -338,8 +339,9 @@ Name | Type | Description | Notes |-------------|-------------|------------------| **200** | Live Activity started | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| **400** | Bad request (invalid payload or channel targeting input) | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| +**402** | Billing access required | - | **403** | Forbidden (API key scope or channel assignment violation) | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| -**404** | No recipients found for effective channel target | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| +**422** | No recipients found for effective channel target | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| **429** | Rate limit exceeded | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
* Retry-After - Seconds to wait before retrying after a request-rate 429. Live Activity capacity errors are separate and require freeing device capacity; they do not provide a timed retry guarantee.
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) diff --git a/activitysmith_openapi/docs/PushInterruptionLevel.md b/activitysmith_openapi/docs/PushInterruptionLevel.md new file mode 100644 index 0000000..245c58b --- /dev/null +++ b/activitysmith_openapi/docs/PushInterruptionLevel.md @@ -0,0 +1,15 @@ +# PushInterruptionLevel + +How urgently iOS presents the notification. `passive` arrives quietly in the notification list without sound or lighting up the screen. `active` is normal delivery and the default when omitted. `time-sensitive` can break through Focus and Scheduled Summary when the user allows Time Sensitive Notifications for ActivitySmith. + +## Enum + +* `PASSIVE` (value: `'passive'`) + +* `ACTIVE` (value: `'active'`) + +* `TIME_MINUS_SENSITIVE` (value: `'time-sensitive'`) + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/activitysmith_openapi/docs/PushNotificationRequest.md b/activitysmith_openapi/docs/PushNotificationRequest.md index 1c45c9e..eccf650 100644 --- a/activitysmith_openapi/docs/PushNotificationRequest.md +++ b/activitysmith_openapi/docs/PushNotificationRequest.md @@ -9,6 +9,8 @@ Name | Type | Description | Notes **title** | **str** | | **message** | **str** | | [optional] **subtitle** | **str** | | [optional] +**icon** | **str** | Optional HTTPS image URL without credentials for a custom notification icon. If the image cannot be loaded, the app icon is used. iOS may omit subtitle when displaying a custom icon. | [optional] +**interruption_level** | [**PushInterruptionLevel**](PushInterruptionLevel.md) | Optional. How urgently iOS presents the notification: `passive` (quiet, no sound or screen wake), `active` (default), or `time-sensitive` (can break through Focus and Scheduled Summary when allowed). Omit for normal delivery. | [optional] **media** | **str** | Optional HTTPS URL for an image, audio file, or video that users can preview or play when they expand the notification. If `redirection` is omitted, tapping the notification opens this URL. Cannot be combined with `actions`. | [optional] **redirection** | **str** | Optional HTTP, HTTPS, Shortcuts, or installed app URL opened when the user taps the notification body. Custom schemes such as spotify:// and spotify:track:123 require iOS 1.13.4 build 2 or later and an installed handler; no web fallback is provided. Internal and executable schemes are blocked. Overrides the default tap target from media. | [optional] **actions** | [**List[PushNotificationAction]**](PushNotificationAction.md) | Optional interactive actions shown when users expand the notification. Cannot be combined with `media`. | [optional] diff --git a/activitysmith_openapi/docs/PushNotificationResponse.md b/activitysmith_openapi/docs/PushNotificationResponse.md index 86e8e37..1e78b7f 100644 --- a/activitysmith_openapi/docs/PushNotificationResponse.md +++ b/activitysmith_openapi/docs/PushNotificationResponse.md @@ -1,12 +1,13 @@ # PushNotificationResponse +Apple APNs acceptance result. Acceptance does not confirm device delivery or presentation. Alerts remain eligible for APNs delivery retries for 24 hours; device settings and APNs storage policies still apply. ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**success** | **bool** | | -**devices_notified** | **int** | | [optional] +**success** | **bool** | True when APNs accepts the notification for at least one targeted device. | +**devices_notified** | **int** | Number of device tokens for which APNs accepted the notification, without confirming on-device delivery. | [optional] **users_notified** | **int** | | [optional] **effective_channel_slugs** | **List[str]** | | [optional] **tags** | **List[str]** | Optional tags to organize and filter notification history. | [optional] diff --git a/activitysmith_openapi/docs/PushNotificationsApi.md b/activitysmith_openapi/docs/PushNotificationsApi.md index 5dab075..ef3b424 100644 --- a/activitysmith_openapi/docs/PushNotificationsApi.md +++ b/activitysmith_openapi/docs/PushNotificationsApi.md @@ -12,7 +12,7 @@ Method | HTTP request | Description Send a push notification -Sends a push notification to devices matched by API key scope and optional target channels. Supports optional redirection URL, optional media preview or playback when the notification is expanded, and up to 4 interactive actions. `media` cannot be combined with `actions`. Optional tags to organize and filter notification history. +Sends a push notification to devices matched by API key scope and optional target channels. Supports optional redirection URL, optional media preview or playback when the notification is expanded, and up to 4 interactive actions. `media` cannot be combined with `actions`. Optional `icon` shows a custom image, such as a service logo or avatar, next to a smaller app icon. Optional `interruption_level` sets how urgently iOS presents the notification. Optional tags to organize and filter notification history. ### Example @@ -84,8 +84,9 @@ Name | Type | Description | Notes |-------------|-------------|------------------| **200** | Push notification sent | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| **400** | Bad request (invalid payload or channel targeting input) | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| +**402** | Billing access required | - | **403** | Forbidden (API key scope or channel assignment violation) | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| -**404** | No recipients found for effective channel target | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| +**422** | No recipients found for effective channel target | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
| **429** | Rate limit exceeded | * RateLimit - Current request quotas using IETF draft-8 structured fields. Each named policy includes r (remaining requests) and t (seconds until reset). Multiple policies may apply.
* RateLimit-Policy - Each named policy includes q (request quota) and w (window in seconds). Each feature has an independent IP limit of 600 requests per 60 seconds. Independent account limits per 60 seconds are: Push Notifications 60, badges 60, Live Activities 300, and metric updates 300.
* Retry-After - Seconds to wait before retrying after a request-rate 429. Live Activity capacity errors are separate and require freeing device capacity; they do not provide a timed retry guarantee.
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) diff --git a/activitysmith_openapi/models/__init__.py b/activitysmith_openapi/models/__init__.py index fd0ed6e..6fd8af3 100644 --- a/activitysmith_openapi/models/__init__.py +++ b/activitysmith_openapi/models/__init__.py @@ -21,6 +21,8 @@ from activitysmith_openapi.models.app_icon_badge_count_update_request import AppIconBadgeCountUpdateRequest from activitysmith_openapi.models.app_icon_badge_count_update_response import AppIconBadgeCountUpdateResponse from activitysmith_openapi.models.bad_request_error import BadRequestError +from activitysmith_openapi.models.billing_blocked_error import BillingBlockedError +from activitysmith_openapi.models.billing_blocked_error_trial_period import BillingBlockedErrorTrialPeriod from activitysmith_openapi.models.channel_target import ChannelTarget from activitysmith_openapi.models.content_state_end import ContentStateEnd from activitysmith_openapi.models.content_state_start import ContentStateStart @@ -51,6 +53,7 @@ from activitysmith_openapi.models.metric_value_update_response import MetricValueUpdateResponse from activitysmith_openapi.models.no_recipients_error import NoRecipientsError from activitysmith_openapi.models.not_found_error import NotFoundError +from activitysmith_openapi.models.push_interruption_level import PushInterruptionLevel from activitysmith_openapi.models.push_notification_action import PushNotificationAction from activitysmith_openapi.models.push_notification_action_type import PushNotificationActionType from activitysmith_openapi.models.push_notification_request import PushNotificationRequest diff --git a/activitysmith_openapi/models/billing_blocked_error.py b/activitysmith_openapi/models/billing_blocked_error.py new file mode 100644 index 0000000..58be5c8 --- /dev/null +++ b/activitysmith_openapi/models/billing_blocked_error.py @@ -0,0 +1,104 @@ +# coding: utf-8 + +""" + ActivitySmith API + + Send push notifications and Live Activities to your own devices via a single API key. + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from activitysmith_openapi.models.billing_blocked_error_trial_period import BillingBlockedErrorTrialPeriod +from typing import Optional, Set +from typing_extensions import Self + +class BillingBlockedError(BaseModel): + """ + BillingBlockedError + """ # noqa: E501 + error: StrictStr + message: StrictStr + trial_period: Optional[BillingBlockedErrorTrialPeriod] = None + upgrade_url: StrictStr + __properties: ClassVar[List[str]] = ["error", "message", "trial_period", "upgrade_url"] + + @field_validator('error') + def error_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['trial_expired', 'trial_exhausted', 'billing_overage_cap_reached']): + raise ValueError("must be one of enum values ('trial_expired', 'trial_exhausted', 'billing_overage_cap_reached')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of BillingBlockedError from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of trial_period + if self.trial_period: + _dict['trial_period'] = self.trial_period.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of BillingBlockedError from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "error": obj.get("error"), + "message": obj.get("message"), + "trial_period": BillingBlockedErrorTrialPeriod.from_dict(obj["trial_period"]) if obj.get("trial_period") is not None else None, + "upgrade_url": obj.get("upgrade_url") + }) + return _obj + + diff --git a/activitysmith_openapi/models/billing_blocked_error_trial_period.py b/activitysmith_openapi/models/billing_blocked_error_trial_period.py new file mode 100644 index 0000000..1b14616 --- /dev/null +++ b/activitysmith_openapi/models/billing_blocked_error_trial_period.py @@ -0,0 +1,90 @@ +# coding: utf-8 + +""" + ActivitySmith API + + Send push notifications and Live Activities to your own devices via a single API key. + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from datetime import datetime +from pydantic import BaseModel, ConfigDict +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class BillingBlockedErrorTrialPeriod(BaseModel): + """ + BillingBlockedErrorTrialPeriod + """ # noqa: E501 + started_at: datetime + ends_at: datetime + __properties: ClassVar[List[str]] = ["started_at", "ends_at"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of BillingBlockedErrorTrialPeriod from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of BillingBlockedErrorTrialPeriod from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "started_at": obj.get("started_at"), + "ends_at": obj.get("ends_at") + }) + return _obj + + diff --git a/activitysmith_openapi/models/push_interruption_level.py b/activitysmith_openapi/models/push_interruption_level.py new file mode 100644 index 0000000..d488fa9 --- /dev/null +++ b/activitysmith_openapi/models/push_interruption_level.py @@ -0,0 +1,38 @@ +# coding: utf-8 + +""" + ActivitySmith API + + Send push notifications and Live Activities to your own devices via a single API key. + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +from enum import Enum +from typing_extensions import Self + + +class PushInterruptionLevel(str, Enum): + """ + How urgently iOS presents the notification. `passive` arrives quietly in the notification list without sound or lighting up the screen. `active` is normal delivery and the default when omitted. `time-sensitive` can break through Focus and Scheduled Summary when the user allows Time Sensitive Notifications for ActivitySmith. + """ + + """ + allowed enum values + """ + PASSIVE = 'passive' + ACTIVE = 'active' + TIME_MINUS_SENSITIVE = 'time-sensitive' + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Create an instance of PushInterruptionLevel from a JSON string""" + return cls(json.loads(json_str)) + + diff --git a/activitysmith_openapi/models/push_notification_request.py b/activitysmith_openapi/models/push_notification_request.py index c62d06c..f71c50c 100644 --- a/activitysmith_openapi/models/push_notification_request.py +++ b/activitysmith_openapi/models/push_notification_request.py @@ -22,6 +22,7 @@ from typing_extensions import Annotated from activitysmith_openapi.models.channel_target import ChannelTarget from activitysmith_openapi.models.metadata_value import MetadataValue +from activitysmith_openapi.models.push_interruption_level import PushInterruptionLevel from activitysmith_openapi.models.push_notification_action import PushNotificationAction from typing import Optional, Set from typing_extensions import Self @@ -34,6 +35,8 @@ class PushNotificationRequest(BaseModel): title: StrictStr message: Optional[StrictStr] = None subtitle: Optional[StrictStr] = None + icon: Optional[Annotated[str, Field(strict=True)]] = Field(default=None, description="Optional HTTPS image URL without credentials for a custom notification icon. If the image cannot be loaded, the app icon is used. iOS may omit subtitle when displaying a custom icon.") + interruption_level: Optional[PushInterruptionLevel] = Field(default=None, description="Optional. How urgently iOS presents the notification: `passive` (quiet, no sound or screen wake), `active` (default), or `time-sensitive` (can break through Focus and Scheduled Summary when allowed). Omit for normal delivery.") media: Optional[Annotated[str, Field(strict=True)]] = Field(default=None, description="Optional HTTPS URL for an image, audio file, or video that users can preview or play when they expand the notification. If `redirection` is omitted, tapping the notification opens this URL. Cannot be combined with `actions`.") redirection: Optional[Annotated[str, Field(strict=True, max_length=2048)]] = Field(default=None, description="Optional HTTP, HTTPS, Shortcuts, or installed app URL opened when the user taps the notification body. Custom schemes such as spotify:// and spotify:track:123 require iOS 1.13.4 build 2 or later and an installed handler; no web fallback is provided. Internal and executable schemes are blocked. Overrides the default tap target from media.") actions: Optional[Annotated[List[PushNotificationAction], Field(max_length=4)]] = Field(default=None, description="Optional interactive actions shown when users expand the notification. Cannot be combined with `media`.") @@ -43,7 +46,17 @@ class PushNotificationRequest(BaseModel): target: Optional[ChannelTarget] = None tags: Optional[List[Annotated[str, Field(min_length=1, strict=True, max_length=64)]]] = Field(default=None, description="Optional tags to organize and filter notification history.") additional_properties: Dict[str, Any] = {} - __properties: ClassVar[List[str]] = ["metadata", "title", "message", "subtitle", "media", "redirection", "actions", "payload", "badge", "sound", "target", "tags"] + __properties: ClassVar[List[str]] = ["metadata", "title", "message", "subtitle", "icon", "interruption_level", "media", "redirection", "actions", "payload", "badge", "sound", "target", "tags"] + + @field_validator('icon') + def icon_validate_regular_expression(cls, value): + """Validates the regular expression""" + if value is None: + return value + + if not re.match(r"^https:\/\/", value): + raise ValueError(r"must validate the regular expression /^https:\/\//") + return value @field_validator('media') def media_validate_regular_expression(cls, value): @@ -149,6 +162,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "title": obj.get("title"), "message": obj.get("message"), "subtitle": obj.get("subtitle"), + "icon": obj.get("icon"), + "interruption_level": obj.get("interruption_level"), "media": obj.get("media"), "redirection": obj.get("redirection"), "actions": [PushNotificationAction.from_dict(_item) for _item in obj["actions"]] if obj.get("actions") is not None else None, diff --git a/activitysmith_openapi/models/push_notification_response.py b/activitysmith_openapi/models/push_notification_response.py index de1c376..816142d 100644 --- a/activitysmith_openapi/models/push_notification_response.py +++ b/activitysmith_openapi/models/push_notification_response.py @@ -26,10 +26,10 @@ class PushNotificationResponse(BaseModel): """ - PushNotificationResponse + Apple APNs acceptance result. Acceptance does not confirm device delivery or presentation. Alerts remain eligible for APNs delivery retries for 24 hours; device settings and APNs storage policies still apply. """ # noqa: E501 - success: StrictBool - devices_notified: Optional[StrictInt] = None + success: StrictBool = Field(description="True when APNs accepts the notification for at least one targeted device.") + devices_notified: Optional[StrictInt] = Field(default=None, description="Number of device tokens for which APNs accepted the notification, without confirming on-device delivery.") users_notified: Optional[StrictInt] = None effective_channel_slugs: Optional[List[StrictStr]] = None tags: Optional[List[Annotated[str, Field(min_length=1, strict=True, max_length=64)]]] = Field(default=None, description="Optional tags to organize and filter notification history.") diff --git a/activitysmith_openapi/openapi-source.json b/activitysmith_openapi/openapi-source.json index aeeecd6..a320213 100644 --- a/activitysmith_openapi/openapi-source.json +++ b/activitysmith_openapi/openapi-source.json @@ -1,6 +1,6 @@ { "repository": "ActivitySmithHQ/activitysmith-backend", - "commit": "6832df646de7de8a6aaf0110960c37074b513c48", + "commit": "ce15d69eeb25776a3ae395e77d8b907ff7ba0709", "path": "openapi.json", - "sha256": "9ae85ad6127cfa2d50c33261459f5f9e85e011939d01ac4a9d507fa6ed11aa61" + "sha256": "b113d50e06d6581707d9472dda6e371b996441f58dceba87a88f79cb96508a7d" } diff --git a/activitysmith_openapi/test/test_billing_blocked_error.py b/activitysmith_openapi/test/test_billing_blocked_error.py new file mode 100644 index 0000000..952c2a9 --- /dev/null +++ b/activitysmith_openapi/test/test_billing_blocked_error.py @@ -0,0 +1,59 @@ +# coding: utf-8 + +""" + ActivitySmith API + + Send push notifications and Live Activities to your own devices via a single API key. + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from activitysmith_openapi.models.billing_blocked_error import BillingBlockedError + +class TestBillingBlockedError(unittest.TestCase): + """BillingBlockedError unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> BillingBlockedError: + """Test BillingBlockedError + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `BillingBlockedError` + """ + model = BillingBlockedError() + if include_optional: + return BillingBlockedError( + error = 'trial_expired', + message = '', + trial_period = activitysmith_openapi.models.billing_blocked_error_trial_period.BillingBlockedError_trial_period( + started_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + ends_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ), + upgrade_url = '' + ) + else: + return BillingBlockedError( + error = 'trial_expired', + message = '', + upgrade_url = '', + ) + """ + + def testBillingBlockedError(self): + """Test BillingBlockedError""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/activitysmith_openapi/test/test_billing_blocked_error_trial_period.py b/activitysmith_openapi/test/test_billing_blocked_error_trial_period.py new file mode 100644 index 0000000..34d9d35 --- /dev/null +++ b/activitysmith_openapi/test/test_billing_blocked_error_trial_period.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + ActivitySmith API + + Send push notifications and Live Activities to your own devices via a single API key. + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from activitysmith_openapi.models.billing_blocked_error_trial_period import BillingBlockedErrorTrialPeriod + +class TestBillingBlockedErrorTrialPeriod(unittest.TestCase): + """BillingBlockedErrorTrialPeriod unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> BillingBlockedErrorTrialPeriod: + """Test BillingBlockedErrorTrialPeriod + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `BillingBlockedErrorTrialPeriod` + """ + model = BillingBlockedErrorTrialPeriod() + if include_optional: + return BillingBlockedErrorTrialPeriod( + started_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + ends_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f') + ) + else: + return BillingBlockedErrorTrialPeriod( + started_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + ends_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + ) + """ + + def testBillingBlockedErrorTrialPeriod(self): + """Test BillingBlockedErrorTrialPeriod""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/activitysmith_openapi/test/test_push_interruption_level.py b/activitysmith_openapi/test/test_push_interruption_level.py new file mode 100644 index 0000000..aa87199 --- /dev/null +++ b/activitysmith_openapi/test/test_push_interruption_level.py @@ -0,0 +1,33 @@ +# coding: utf-8 + +""" + ActivitySmith API + + Send push notifications and Live Activities to your own devices via a single API key. + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from activitysmith_openapi.models.push_interruption_level import PushInterruptionLevel + +class TestPushInterruptionLevel(unittest.TestCase): + """PushInterruptionLevel unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def testPushInterruptionLevel(self): + """Test PushInterruptionLevel""" + # inst = PushInterruptionLevel() + +if __name__ == '__main__': + unittest.main() diff --git a/activitysmith_openapi/test/test_push_notification_request.py b/activitysmith_openapi/test/test_push_notification_request.py index 803435e..8ed2458 100644 --- a/activitysmith_openapi/test/test_push_notification_request.py +++ b/activitysmith_openapi/test/test_push_notification_request.py @@ -41,6 +41,8 @@ def make_instance(self, include_optional) -> PushNotificationRequest: title = '', message = '', subtitle = '', + icon = 'https:/', + interruption_level = 'passive', media = 'https:/', redirection = 'A5bTTFjjMRwg.Zbs8YayHLrJdgMvb:', actions = [ diff --git a/pyproject.toml b/pyproject.toml index 4501f59..d390f77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "activitysmith" -version = "1.12.0" +version = "1.13.0" description = "Official ActivitySmith Python SDK" readme = "README.md" requires-python = ">=3.9" diff --git a/tests/test_value_serialization.py b/tests/test_value_serialization.py index 9c62212..55ed52d 100644 --- a/tests/test_value_serialization.py +++ b/tests/test_value_serialization.py @@ -164,3 +164,48 @@ def test_prominent_value_serializes_without_losing_format(requests, value, metho getattr(activitysmith.live_activities, method)(*args, request) assert requests[-1]["content_state"]["value"] == value assert isinstance(requests[-1]["content_state"]["value"], str) == isinstance(value, str) + + +@pytest.mark.parametrize("form", ["dict", "named"]) +def test_push_icon_serialization(requests, form): + client = ActivitySmith(api_key="test") + fields = {"title": "New Download", "icon": "https://cdn.example.com/avatar.png"} + with pytest.raises(RequestCaptured): + if form == "named": + client.notifications.send(**fields) + else: + client.notifications.send(fields) + assert requests[-1]["icon"] == "https://cdn.example.com/avatar.png" + + +@pytest.mark.parametrize("level", ["passive", "active", "time-sensitive"]) +@pytest.mark.parametrize("form", ["dict", "named", "constant"]) +def test_push_interruption_level_serialization(requests, level, form): + from activitysmith import PushInterruptionLevel + + client = ActivitySmith(api_key="test") + if form == "constant": + level = {"passive": PushInterruptionLevel.PASSIVE, "active": PushInterruptionLevel.ACTIVE, + "time-sensitive": PushInterruptionLevel.TIME_SENSITIVE}[level] + fields = {"title": "Deploy", "interruption_level": level} + with pytest.raises(RequestCaptured): + if form == "dict": + client.notifications.send(fields) + else: + client.notifications.send(**fields) + assert requests[-1]["interruption_level"] == level + + +def test_push_interruption_level_omitted_by_default(requests): + client = ActivitySmith(api_key="test") + with pytest.raises(RequestCaptured): + client.notifications.send(title="Deploy") + assert "interruption_level" not in requests[-1] + + +@pytest.mark.parametrize("level", ["critical", "timeSensitive", "default"]) +def test_push_rejects_unsupported_interruption_level(requests, level): + client = ActivitySmith(api_key="test") + with pytest.raises(ValueError, match="interruption_level must be passive, active, or time-sensitive"): + client.notifications.send(title="Outage", interruption_level=level) + assert requests == []