Skip to content

Commit 8a3dc97

Browse files
bardonadamactivitysmith-bot
andauthored
feat: add stream helpers and docs (#26)
* chore: regenerate SDK * feat: add stream helpers and docs --------- Co-authored-by: activitysmith-bot <activitysmith-bot@users.noreply.github.com>
1 parent 058622e commit 8a3dc97

40 files changed

+2803
-64
lines changed

README.md

Lines changed: 238 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -116,27 +116,230 @@ activitysmith.notifications.send(
116116

117117
## Live Activities
118118

119-
Live Activities come in two UI types, but the lifecycle stays the same:
120-
start the activity, keep the returned `activity_id`, update it as state
121-
changes, then end it when the work is done.
119+
<p align="center">
120+
<img src="https://cdn.activitysmith.com/features/metrics-live-activity-action.png" alt="Live Activities example" width="680" />
121+
</p>
122+
123+
ActivitySmith supports two ways to drive Live Activities:
124+
125+
- Recommended: stream updates with `activitysmith.live_activities.stream(...)`
126+
- Advanced: manual lifecycle control with `start`, `update`, and `end`
127+
128+
Use stream updates when you want the easiest, stateless flow. You don't need to
129+
store `activity_id` or manage lifecycle state yourself. Send the latest state
130+
for a stable `stream_key` and ActivitySmith will start or update the Live
131+
Activity for you. When the tracked process is over, call `end_stream(...)`.
132+
133+
Use the manual lifecycle methods when you need direct control over a specific
134+
Live Activity instance.
135+
136+
Live Activity UI types:
137+
138+
- `metrics`: best for live operational stats like server CPU and memory, queue depth, or replica lag
139+
- `segmented_progress`: best for step-based workflows like deployments, backups, and ETL pipelines
140+
- `progress`: best for continuous jobs like uploads, reindexes, and long-running migrations tracked as a percentage
141+
142+
### Recommended: Stream updates
143+
144+
Use a stable `stream_key` to identify the system or workflow you are tracking,
145+
such as a server, deployment, build pipeline, cron job, or charging session.
146+
This is especially useful for cron jobs and other scheduled tasks where you do
147+
not want to store `activity_id` between runs.
148+
149+
#### Metrics
150+
151+
<p align="center">
152+
<img src="https://cdn.activitysmith.com/features/metrics-live-activity-start.png" alt="Metrics stream example" width="680" />
153+
</p>
154+
155+
```python
156+
status = activitysmith.live_activities.stream(
157+
"prod-web-1",
158+
{
159+
"content_state": {
160+
"title": "Server Health",
161+
"subtitle": "prod-web-1",
162+
"type": "metrics",
163+
"metrics": [
164+
{"label": "CPU", "value": 9, "unit": "%"},
165+
{"label": "MEM", "value": 45, "unit": "%"},
166+
],
167+
},
168+
},
169+
)
170+
```
171+
172+
#### Segmented progress
173+
174+
<p align="center">
175+
<img src="https://cdn.activitysmith.com/features/update-live-activity.png" alt="Segmented progress stream example" width="680" />
176+
</p>
177+
178+
```python
179+
activitysmith.live_activities.stream(
180+
"nightly-backup",
181+
{
182+
"content_state": {
183+
"title": "Nightly Backup",
184+
"subtitle": "upload archive",
185+
"type": "segmented_progress",
186+
"number_of_steps": 3,
187+
"current_step": 2,
188+
},
189+
},
190+
)
191+
```
192+
193+
#### Progress
194+
195+
<p align="center">
196+
<img src="https://cdn.activitysmith.com/features/progress-live-activity.png" alt="Progress stream example" width="680" />
197+
</p>
198+
199+
```python
200+
activitysmith.live_activities.stream(
201+
"search-reindex",
202+
{
203+
"content_state": {
204+
"title": "Search Reindex",
205+
"subtitle": "catalog-v2",
206+
"type": "progress",
207+
"percentage": 42,
208+
},
209+
},
210+
)
211+
```
212+
213+
Call `stream(...)` again with the same `stream_key` whenever the state changes.
214+
215+
#### End a stream
216+
217+
Use this when the tracked process is finished and you no longer want the Live
218+
Activity on devices. `content_state` is optional here; include it if you want
219+
to end the stream with a final state.
220+
221+
```python
222+
activitysmith.live_activities.end_stream(
223+
"prod-web-1",
224+
{
225+
"content_state": {
226+
"title": "Server Health",
227+
"subtitle": "prod-web-1",
228+
"type": "metrics",
229+
"metrics": [
230+
{"label": "CPU", "value": 7, "unit": "%"},
231+
{"label": "MEM", "value": 38, "unit": "%"},
232+
],
233+
},
234+
},
235+
)
236+
```
237+
238+
If you later send another `stream(...)` request with the same `stream_key`,
239+
ActivitySmith starts a new Live Activity for that stream again.
240+
241+
Stream responses include an `operation` field:
122242

123-
- `segmented_progress`: best for jobs tracked in steps
124-
- `progress`: best for jobs tracked as a percentage or numeric range
243+
- `started`: ActivitySmith started a new Live Activity for this `stream_key`
244+
- `updated`: ActivitySmith updated the current Live Activity
245+
- `rotated`: ActivitySmith ended the previous Live Activity and started a new one
246+
- `noop`: the incoming state matched the current state, so no update was sent
247+
- `paused`: the stream is paused, so no Live Activity was started or updated
248+
- `ended`: returned by `end_stream(...)` after the stream is ended
125249

126-
### Shared flow
250+
### Advanced: Manual lifecycle control
251+
252+
Use these methods when you want to manage the Live Activity lifecycle yourself.
253+
254+
#### Shared flow
127255

128256
1. Call `activitysmith.live_activities.start(...)`.
129257
2. Save the returned `activity_id`.
130258
3. Call `activitysmith.live_activities.update(...)` as progress changes.
131259
4. Call `activitysmith.live_activities.end(...)` when the work is finished.
132260

261+
### Metrics Type
262+
263+
Use `metrics` when you want to keep a small set of live stats visible, such as
264+
server health, queue pressure, or database load.
265+
266+
#### Start
267+
268+
<p align="center">
269+
<img src="https://cdn.activitysmith.com/features/metrics-live-activity-start.png" alt="Metrics start example" width="680" />
270+
</p>
271+
272+
```python
273+
start = activitysmith.live_activities.start(
274+
{
275+
"content_state": {
276+
"title": "Server Health",
277+
"subtitle": "prod-web-1",
278+
"type": "metrics",
279+
"metrics": [
280+
{"label": "CPU", "value": 9, "unit": "%"},
281+
{"label": "MEM", "value": 45, "unit": "%"},
282+
],
283+
},
284+
}
285+
)
286+
287+
activity_id = start.activity_id
288+
```
289+
290+
#### Update
291+
292+
<p align="center">
293+
<img src="https://cdn.activitysmith.com/features/metrics-live-activity-update.png" alt="Metrics update example" width="680" />
294+
</p>
295+
296+
```python
297+
activitysmith.live_activities.update(
298+
{
299+
"activity_id": activity_id,
300+
"content_state": {
301+
"title": "Server Health",
302+
"subtitle": "prod-web-1",
303+
"type": "metrics",
304+
"metrics": [
305+
{"label": "CPU", "value": 76, "unit": "%"},
306+
{"label": "MEM", "value": 52, "unit": "%"},
307+
],
308+
},
309+
}
310+
)
311+
```
312+
313+
#### End
314+
315+
<p align="center">
316+
<img src="https://cdn.activitysmith.com/features/metrics-live-activity-end.png" alt="Metrics end example" width="680" />
317+
</p>
318+
319+
```python
320+
activitysmith.live_activities.end(
321+
{
322+
"activity_id": activity_id,
323+
"content_state": {
324+
"title": "Server Health",
325+
"subtitle": "prod-web-1",
326+
"type": "metrics",
327+
"metrics": [
328+
{"label": "CPU", "value": 7, "unit": "%"},
329+
{"label": "MEM", "value": 38, "unit": "%"},
330+
],
331+
"auto_dismiss_minutes": 2,
332+
},
333+
}
334+
)
335+
```
336+
133337
### Segmented Progress Type
134338

135339
Use `segmented_progress` when progress is easier to follow as steps instead of a
136340
raw percentage. It fits jobs like backups, deployments, ETL pipelines, and
137-
checklists where "step 2 of 3" is more useful than "67%".
138-
`number_of_steps` is dynamic, so you can increase or decrease it later if the
139-
workflow changes.
341+
checklists where "step 2 of 3" is more useful than "67%". `number_of_steps` is
342+
dynamic, so you can increase or decrease it later if the workflow changes.
140343

141344
#### Start
142345

@@ -155,7 +358,6 @@ start = activitysmith.live_activities.start(
155358
"type": "segmented_progress",
156359
"color": "yellow",
157360
},
158-
"channels": ["devs", "ops"], # Optional
159361
}
160362
)
161363

@@ -175,9 +377,9 @@ activitysmith.live_activities.update(
175377
"content_state": {
176378
"title": "Nightly database backup",
177379
"subtitle": "upload archive",
178-
"number_of_steps": 4,
380+
"number_of_steps": 3,
179381
"current_step": 2,
180-
}
382+
},
181383
}
182384
)
183385
```
@@ -195,10 +397,10 @@ activitysmith.live_activities.end(
195397
"content_state": {
196398
"title": "Nightly database backup",
197399
"subtitle": "verify restore",
198-
"number_of_steps": 4,
199-
"current_step": 4,
400+
"number_of_steps": 3,
401+
"current_step": 3,
200402
"auto_dismiss_minutes": 2,
201-
}
403+
},
202404
}
203405
)
204406
```
@@ -223,7 +425,6 @@ start = activitysmith.live_activities.start(
223425
"subtitle": "Added 30 mi range",
224426
"type": "progress",
225427
"percentage": 15,
226-
"color": "lime",
227428
}
228429
}
229430
)
@@ -272,10 +473,10 @@ activitysmith.live_activities.end(
272473

273474
### Live Activity Action
274475

275-
Just like Actionable Push Notifications, Live Activities can have a button that opens provided URL in a browser or triggers a webhook. Webhooks are executed by the ActivitySmith backend.
476+
Just like Actionable Push Notifications, Live Activities can have a button that opens a URL in a browser or triggers a webhook. Webhooks are executed by the ActivitySmith backend.
276477

277478
<p align="center">
278-
<img src="https://cdn.activitysmith.com/features/live-activity-with-action.png?v=20260319-1" alt="Live Activity with action" width="680" />
479+
<img src="https://cdn.activitysmith.com/features/metrics-live-activity-action.png" alt="Metrics Live Activity with action" width="680" />
279480
</p>
280481

281482
#### Open URL action
@@ -284,16 +485,18 @@ Just like Actionable Push Notifications, Live Activities can have a button that
284485
start = activitysmith.live_activities.start(
285486
{
286487
"content_state": {
287-
"title": "Deploying payments-api",
288-
"subtitle": "Running database migrations",
289-
"number_of_steps": 5,
290-
"current_step": 3,
291-
"type": "segmented_progress",
488+
"title": "Server Health",
489+
"subtitle": "prod-web-1",
490+
"type": "metrics",
491+
"metrics": [
492+
{"label": "CPU", "value": 76, "unit": "%"},
493+
{"label": "MEM", "value": 52, "unit": "%"},
494+
],
292495
},
293496
"action": {
294-
"title": "Open Workflow",
497+
"title": "Open Dashboard",
295498
"type": "open_url",
296-
"url": "https://github.com/acme/payments-api/actions/runs/1234567890",
499+
"url": "https://ops.example.com/servers/prod-web-1",
297500
},
298501
}
299502
)
@@ -308,18 +511,21 @@ activitysmith.live_activities.update(
308511
{
309512
"activity_id": activity_id,
310513
"content_state": {
311-
"title": "Reindexing product search",
312-
"subtitle": "Shard 7 of 12",
313-
"number_of_steps": 12,
314-
"current_step": 7,
514+
"title": "Server Health",
515+
"subtitle": "prod-web-1",
516+
"type": "metrics",
517+
"metrics": [
518+
{"label": "CPU", "value": 91, "unit": "%"},
519+
{"label": "MEM", "value": 57, "unit": "%"},
520+
],
315521
},
316522
"action": {
317-
"title": "Pause Reindex",
523+
"title": "Restart Service",
318524
"type": "webhook",
319-
"url": "https://ops.example.com/hooks/search/reindex/pause",
525+
"url": "https://ops.example.com/hooks/servers/prod-web-1/restart",
320526
"method": "POST",
321527
"body": {
322-
"job_id": "reindex-2026-03-19",
528+
"server_id": "prod-web-1",
323529
"requested_by": "activitysmith-python",
324530
},
325531
},

activitysmith/client.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,18 @@ def update(self, request: Any):
9191
def end(self, request: Any):
9292
return self._api.end_live_activity(live_activity_end_request=request)
9393

94+
def stream(self, stream_key: str, request: Any):
95+
return self._api.reconcile_live_activity_stream(
96+
stream_key=stream_key,
97+
live_activity_stream_request=_normalize_channels_target(request),
98+
)
99+
100+
def end_stream(self, stream_key: str, request: Any | None = None):
101+
return self._api.end_live_activity_stream(
102+
stream_key=stream_key,
103+
live_activity_stream_delete_request=request,
104+
)
105+
94106
# Backward-compatible aliases.
95107
def start_live_activity(self, live_activity_start_request: Any):
96108
return self.start(live_activity_start_request)
@@ -101,6 +113,12 @@ def update_live_activity(self, live_activity_update_request: Any):
101113
def end_live_activity(self, live_activity_end_request: Any):
102114
return self.end(live_activity_end_request)
103115

116+
def reconcile_live_activity_stream(self, stream_key: str, live_activity_stream_request: Any):
117+
return self.stream(stream_key, live_activity_stream_request)
118+
119+
def end_live_activity_stream(self, stream_key: str, live_activity_stream_delete_request: Any | None = None):
120+
return self.end_stream(stream_key, live_activity_stream_delete_request)
121+
104122

105123
@dataclass
106124
class ActivitySmith:

0 commit comments

Comments
 (0)