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
44 changes: 42 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@ python -m pip install .

```python
import os
from activitysmith import ActivitySmith, action, content_state, metric
from activitysmith import (
ActivitySmith,
action,
alert_badge,
alert_icon,
content_state,
metric,
)

activitysmith = ActivitySmith(
api_key=os.environ["ACTIVITYSMITH_API_KEY"],
Expand Down Expand Up @@ -125,12 +132,13 @@ activitysmith.notifications.send(

## Live Activities

There are four types of Live Activities:
There are five types of Live Activities:

- `stats`: best for showing business numbers side by side, such as revenue, sales, new users, conversion, refunds, or any other value you want visible at a glance
- `metrics`: best for live percentage values that change often, like server CPU, memory usage, disk usage, or error rate
- `segmented_progress`: best for anything that moves through clear stages, like deployments, onboarding flows, backups, ETL pipelines, migrations, and AI agent runs
- `progress`: best for tracking real-time progress with percentage, like tasks, backups, migrations, syncs, or uploads
- `alert`: best for status updates, such as feature adoption, reactivation, onboarding blockers, incidents, escalations, and other operational states

### Start & Update Live Activity

Expand Down Expand Up @@ -235,6 +243,37 @@ activitysmith.live_activities.stream(
)
```

#### Alert

<p align="center">
<img
src="https://cdn.activitysmith.com/features/alert-live-activity.png"
alt="Alert Live Activity stream example"
width="680"
/>
</p>

```python
activitysmith.live_activities.stream(
"customer-ops",
content_state=content_state(
title="Reactivation",
message="Lumen came back after 2 weeks",
type="alert",
icon=alert_icon("cloud.sun", color="yellow"),
badge=alert_badge("Customer", color="magenta"),
),
)
```

The `icon.symbol` value is an Apple SF Symbol name. Browse the catalog with one of these tools:

- [ActivitySmith app](https://apps.apple.com/us/app/activitysmith/id6752254835) - Open Settings -> SF Symbols to browse 45 hand-picked icons ready to use
- [SF Symbols](https://developer.apple.com/sf-symbols/) - Apple's official macOS app
- [Interactful](https://apps.apple.com/app/interactful/id1528095640) - free third-party iOS app listing all SF Symbols under Foundations -> Iconography

`icon` and `badge` are optional. If you omit either one, that element is not shown in the Live Activity.

### End Live Activity

Call `end_stream(...)` with the same `stream_key` to dismiss the Live Activity. You can include final values before it is removed. By default, iOS removes the Live Activity after two minutes. Set `auto_dismiss_minutes` to choose a different dismissal time, including `0` for immediate dismissal.
Expand All @@ -258,6 +297,7 @@ activitysmith.live_activities.end_stream(
### Live Activity Action

Live Activities can include one optional action button. Use it to open a URL from the Live Activity or trigger a backend webhook.
For Alert Live Activities, set `content_state.color` to tint the action button. `icon.color` and `badge.color` only affect the icon and badge.

<p align="center">
<img
Expand Down
20 changes: 18 additions & 2 deletions activitysmith/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
from .client import ActivitySmith, LiveActivityColor, action, content_state, metric
from .client import (
ActivitySmith,
LiveActivityColor,
action,
alert_badge,
alert_icon,
content_state,
metric,
)

__all__ = ["ActivitySmith", "LiveActivityColor", "action", "content_state", "metric"]
__all__ = [
"ActivitySmith",
"LiveActivityColor",
"action",
"alert_badge",
"alert_icon",
"content_state",
"metric",
]
100 changes: 98 additions & 2 deletions activitysmith/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from activitysmith_openapi.api.metrics_api import MetricsApi
from activitysmith_openapi.api.push_notifications_api import PushNotificationsApi

SDK_VERSION = "1.4.1"
SDK_VERSION = "1.5.0"
SDK_HEADER_NAME = "X-ActivitySmith-SDK"
SDK_HEADER_VALUE = f"python-v{SDK_VERSION}"

Expand Down Expand Up @@ -122,11 +122,44 @@ def action(
)


def alert_icon(
symbol: str,
*,
color: str | None = None,
) -> dict[str, Any]:
return _compact_dict(
{
"symbol": symbol,
"color": color,
}
)


def alert_badge(
title: str,
*,
color: str | None = None,
) -> dict[str, Any]:
return _compact_dict(
{
"title": title,
"color": color,
}
)


def _normalize_live_activity_content_state(content_state: Any) -> Any:
return content_state


def content_state(
title: str,
*,
type: str | None = None,
subtitle: str | None = None,
message: str | None = None,
icon: Any | None = None,
badge: Any | None = None,
metrics: Any | None = None,
number_of_steps: int | None = None,
current_step: int | None = None,
Expand All @@ -138,11 +171,14 @@ def content_state(
auto_dismiss_seconds: int | None = None,
auto_dismiss_minutes: int | None = None,
) -> dict[str, Any]:
return _compact_dict(
state = _compact_dict(
{
"title": title,
"subtitle": subtitle,
"type": type,
"message": message,
"icon": icon,
"badge": badge,
"metrics": metrics,
"number_of_steps": number_of_steps,
"current_step": current_step,
Expand All @@ -155,6 +191,7 @@ def content_state(
"auto_dismiss_minutes": auto_dismiss_minutes,
}
)
return _normalize_live_activity_content_state(state)


def _build_push_request(
Expand Down Expand Up @@ -208,6 +245,7 @@ class LiveActivityColor:
RED = "red"
ORANGE = "orange"
YELLOW = "yellow"
GRAY = "gray"


def _build_live_activity_request(
Expand All @@ -218,6 +256,9 @@ def _build_live_activity_request(
title: Any | None = None,
subtitle: Any | None = None,
type: Any | None = None,
message: Any | None = None,
icon: Any | None = None,
badge: Any | None = None,
metrics: Any | None = None,
number_of_steps: Any | None = None,
current_step: Any | None = None,
Expand All @@ -238,6 +279,9 @@ def _build_live_activity_request(
"title": title,
"subtitle": subtitle,
"type": type,
"message": message,
"icon": icon,
"badge": badge,
"metrics": metrics,
"number_of_steps": number_of_steps,
"current_step": current_step,
Expand All @@ -250,6 +294,7 @@ def _build_live_activity_request(
"auto_dismiss_minutes": auto_dismiss_minutes,
}
)
content_state_fields = _normalize_live_activity_content_state(content_state_fields)
request_fields = _compact_dict(
{
"activity_id": activity_id,
Expand All @@ -273,6 +318,7 @@ def _build_live_activity_request(
)

if content_state is not None:
content_state = _normalize_live_activity_content_state(content_state)
existing_content_state = normalized.get("content_state")
if existing_content_state is None:
normalized["content_state"] = content_state
Expand All @@ -289,6 +335,9 @@ def _build_live_activity_request(
normalized["content_state"] = {**existing_content_state, **content_state_fields}
else:
raise TypeError("ActivitySmith: content_state must be a dict")
normalized["content_state"] = _normalize_live_activity_content_state(
normalized["content_state"]
)

normalized.update(request_fields)
return normalized
Expand Down Expand Up @@ -337,6 +386,7 @@ class LiveActivitiesResource:
TYPE_PROGRESS = "progress"
TYPE_METRICS = "metrics"
TYPE_STATS = "stats"
TYPE_ALERT = "alert"

def __init__(self, api: LiveActivitiesApi) -> None:
self._api = api
Expand All @@ -351,6 +401,22 @@ def metric(
) -> dict[str, Any]:
return metric(label, value, unit=unit, color=color)

@staticmethod
def alert_icon(
symbol: str,
*,
color: str | None = None,
) -> dict[str, Any]:
return alert_icon(symbol, color=color)

@staticmethod
def alert_badge(
title: str,
*,
color: str | None = None,
) -> dict[str, Any]:
return alert_badge(title, color=color)

def start(
self,
request: Any | None = None,
Expand All @@ -359,6 +425,9 @@ def start(
title: Any | None = None,
subtitle: Any | None = None,
type: Any | None = None,
message: Any | None = None,
icon: Any | None = None,
badge: Any | None = None,
metrics: Any | None = None,
number_of_steps: Any | None = None,
current_step: Any | None = None,
Expand All @@ -378,6 +447,9 @@ def start(
title=title,
subtitle=subtitle,
type=type,
message=message,
icon=icon,
badge=badge,
metrics=metrics,
number_of_steps=number_of_steps,
current_step=current_step,
Expand All @@ -404,6 +476,9 @@ def update(
title: Any | None = None,
subtitle: Any | None = None,
type: Any | None = None,
message: Any | None = None,
icon: Any | None = None,
badge: Any | None = None,
metrics: Any | None = None,
number_of_steps: Any | None = None,
current_step: Any | None = None,
Expand All @@ -421,6 +496,9 @@ def update(
title=title,
subtitle=subtitle,
type=type,
message=message,
icon=icon,
badge=badge,
metrics=metrics,
number_of_steps=number_of_steps,
current_step=current_step,
Expand All @@ -442,6 +520,9 @@ def end(
title: Any | None = None,
subtitle: Any | None = None,
type: Any | None = None,
message: Any | None = None,
icon: Any | None = None,
badge: Any | None = None,
metrics: Any | None = None,
number_of_steps: Any | None = None,
current_step: Any | None = None,
Expand All @@ -460,6 +541,9 @@ def end(
title=title,
subtitle=subtitle,
type=type,
message=message,
icon=icon,
badge=badge,
metrics=metrics,
number_of_steps=number_of_steps,
current_step=current_step,
Expand All @@ -482,6 +566,9 @@ def stream(
title: Any | None = None,
subtitle: Any | None = None,
type: Any | None = None,
message: Any | None = None,
icon: Any | None = None,
badge: Any | None = None,
metrics: Any | None = None,
number_of_steps: Any | None = None,
current_step: Any | None = None,
Expand All @@ -501,6 +588,9 @@ def stream(
title=title,
subtitle=subtitle,
type=type,
message=message,
icon=icon,
badge=badge,
metrics=metrics,
number_of_steps=number_of_steps,
current_step=current_step,
Expand Down Expand Up @@ -528,6 +618,9 @@ def end_stream(
title: Any | None = None,
subtitle: Any | None = None,
type: Any | None = None,
message: Any | None = None,
icon: Any | None = None,
badge: Any | None = None,
metrics: Any | None = None,
number_of_steps: Any | None = None,
current_step: Any | None = None,
Expand All @@ -546,6 +639,9 @@ def end_stream(
title=title,
subtitle=subtitle,
type=type,
message=message,
icon=icon,
badge=badge,
metrics=metrics,
number_of_steps=number_of_steps,
current_step=current_step,
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.3.0"
__version__ = "1.4.2"

# import apis into sdk package
from activitysmith_openapi.api.live_activities_api import LiveActivitiesApi
Expand Down Expand Up @@ -44,6 +44,9 @@
from activitysmith_openapi.models.forbidden_error import ForbiddenError
from activitysmith_openapi.models.live_activity_action import LiveActivityAction
from activitysmith_openapi.models.live_activity_action_type import LiveActivityActionType
from activitysmith_openapi.models.live_activity_alert_badge import LiveActivityAlertBadge
from activitysmith_openapi.models.live_activity_alert_icon import LiveActivityAlertIcon
from activitysmith_openapi.models.live_activity_color import LiveActivityColor
from activitysmith_openapi.models.live_activity_end_request import LiveActivityEndRequest
from activitysmith_openapi.models.live_activity_end_response import LiveActivityEndResponse
from activitysmith_openapi.models.live_activity_limit_error import LiveActivityLimitError
Expand Down
Loading