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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
90 changes: 83 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions activitysmith/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .client import (
ActivitySmith,
LiveActivityColor,
PushInterruptionLevel,
action,
alert_badge,
alert_icon,
Expand All @@ -11,6 +12,7 @@
__all__ = [
"ActivitySmith",
"LiveActivityColor",
"PushInterruptionLevel",
"action",
"alert_badge",
"alert_icon",
Expand Down
26 changes: 25 additions & 1 deletion activitysmith/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"

Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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(
{
Expand All @@ -235,6 +245,8 @@ def _build_push_request(
"channels": channels,
"tags": tags,
"metadata": metadata,
"icon": icon,
"interruption_level": interruption_level,
}
)

Expand All @@ -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"
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down
5 changes: 4 additions & 1 deletion activitysmith_openapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions activitysmith_openapi/api/app_icon_badges_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
18 changes: 12 additions & 6 deletions activitysmith_openapi/api/live_activities_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
15 changes: 9 additions & 6 deletions activitysmith_openapi/api/push_notifications_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading