Skip to content

Commit 09ec3ca

Browse files
feat: add monitor.activity() endpoint for polling monitor results
- Add MonitorActivityRequest, MonitorActivityResponse, MonitorTickEntry schemas - Add activity() method to MonitorResource (sync and async) - Update monitor examples to use activity() and show diffs nicely - Delete monitor on Ctrl+C cleanup Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent bac1751 commit 09ec3ca

8 files changed

Lines changed: 231 additions & 46 deletions

File tree

examples/monitor/monitor_basic.py

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,57 @@
11
from dotenv import load_dotenv
22
load_dotenv()
33

4-
from scrapegraph_py import ScrapeGraphAI, MonitorCreateRequest, MarkdownFormatConfig
4+
import json
5+
import signal
6+
import time
7+
from scrapegraph_py import ScrapeGraphAI, MonitorCreateRequest, JsonFormatConfig
58

69
sgai = ScrapeGraphAI()
710

811
res = sgai.monitor.create(MonitorCreateRequest(
9-
url="https://example.com",
10-
name="Example Monitor",
11-
interval="0 * * * *",
12-
formats=[MarkdownFormatConfig()],
12+
url="https://time.is/",
13+
name="Time Monitor",
14+
interval="*/10 * * * *",
15+
formats=[JsonFormatConfig(
16+
prompt="Extract the current time",
17+
schema={
18+
"type": "object",
19+
"properties": {
20+
"time": {"type": "string"},
21+
},
22+
"required": ["time"],
23+
},
24+
)],
1325
))
1426

15-
if res.status == "success":
16-
print("Monitor created:", res.data.cron_id)
17-
print("Status:", res.data.status)
18-
print("Interval:", res.data.interval)
19-
else:
20-
print("Failed:", res.error)
27+
if res.status != "success" or not res.data:
28+
print("Failed to create monitor:", res.error)
29+
exit(1)
30+
31+
monitor_id = res.data.cron_id
32+
print(f"Monitor created: {monitor_id}")
33+
print(f"Interval: {res.data.interval}")
34+
print("\nPolling for activity (Ctrl+C to stop)...\n")
35+
36+
running = True
37+
38+
def cleanup(_sig, _frame):
39+
global running
40+
running = False
41+
42+
signal.signal(signal.SIGINT, cleanup)
43+
44+
while running:
45+
activity = sgai.monitor.activity(monitor_id)
46+
if activity.status == "success" and activity.data:
47+
for tick in activity.data.ticks:
48+
changes = "CHANGED" if tick.changed else "no change"
49+
print(f"[{tick.created_at}] {tick.status} - {changes} ({tick.elapsed_ms}ms)")
50+
diffs = tick.diffs.model_dump(exclude_none=True)
51+
if diffs:
52+
print(f" Diffs: {json.dumps(diffs, indent=2)}")
53+
time.sleep(30)
54+
55+
print("\nStopping monitor...")
56+
sgai.monitor.delete(monitor_id)
57+
print("Monitor deleted")

examples/monitor/monitor_basic_async.py

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,52 @@
22
load_dotenv()
33

44
import asyncio
5-
from scrapegraph_py import AsyncScrapeGraphAI, MonitorCreateRequest, MarkdownFormatConfig
5+
import json
6+
from scrapegraph_py import AsyncScrapeGraphAI, MonitorCreateRequest, JsonFormatConfig
67

78
async def main():
89
async with AsyncScrapeGraphAI() as sgai:
910
res = await sgai.monitor.create(MonitorCreateRequest(
10-
url="https://example.com",
11-
name="Example Monitor",
12-
interval="0 * * * *",
13-
formats=[MarkdownFormatConfig()],
11+
url="https://time.is/",
12+
name="Time Monitor",
13+
interval="*/10 * * * *",
14+
formats=[JsonFormatConfig(
15+
prompt="Extract the current time",
16+
schema={
17+
"type": "object",
18+
"properties": {
19+
"time": {"type": "string"},
20+
},
21+
"required": ["time"],
22+
},
23+
)],
1424
))
1525

16-
if res.status == "success":
17-
print("Monitor created:", res.data.cron_id)
18-
print("Status:", res.data.status)
19-
print("Interval:", res.data.interval)
20-
else:
21-
print("Failed:", res.error)
26+
if res.status != "success" or not res.data:
27+
print("Failed to create monitor:", res.error)
28+
return
29+
30+
monitor_id = res.data.cron_id
31+
print(f"Monitor created: {monitor_id}")
32+
print(f"Interval: {res.data.interval}")
33+
print("\nPolling for activity (Ctrl+C to stop)...\n")
34+
35+
try:
36+
while True:
37+
activity = await sgai.monitor.activity(monitor_id)
38+
if activity.status == "success" and activity.data:
39+
for tick in activity.data.ticks:
40+
changes = "CHANGED" if tick.changed else "no change"
41+
print(f"[{tick.created_at}] {tick.status} - {changes} ({tick.elapsed_ms}ms)")
42+
diffs = tick.diffs.model_dump(exclude_none=True)
43+
if diffs:
44+
print(f" Diffs: {json.dumps(diffs, indent=2)}")
45+
await asyncio.sleep(30)
46+
except asyncio.CancelledError:
47+
pass
48+
49+
print("\nStopping monitor...")
50+
await sgai.monitor.delete(monitor_id)
51+
print("Monitor deleted")
2252

2353
asyncio.run(main())
Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,59 @@
11
from dotenv import load_dotenv
22
load_dotenv()
33

4-
from scrapegraph_py import ScrapeGraphAI, MonitorCreateRequest, MarkdownFormatConfig
4+
import json
5+
import signal
6+
import time
7+
from scrapegraph_py import ScrapeGraphAI, MonitorCreateRequest, JsonFormatConfig
58

69
sgai = ScrapeGraphAI()
710

811
res = sgai.monitor.create(MonitorCreateRequest(
9-
url="https://example.com",
10-
name="Example Monitor with Webhook",
11-
interval="0 */6 * * *",
12+
url="https://time.is/",
13+
name="Time Monitor with Webhook",
14+
interval="*/10 * * * *",
1215
webhook_url="https://your-webhook-endpoint.com/hook",
13-
formats=[MarkdownFormatConfig()],
16+
formats=[JsonFormatConfig(
17+
prompt="Extract the current time",
18+
schema={
19+
"type": "object",
20+
"properties": {
21+
"time": {"type": "string"},
22+
},
23+
"required": ["time"],
24+
},
25+
)],
1426
))
1527

16-
if res.status == "success":
17-
print("Monitor created:", res.data.cron_id)
18-
print("Status:", res.data.status)
19-
print("Interval:", res.data.interval)
20-
print("Webhook configured")
21-
else:
22-
print("Failed:", res.error)
28+
if res.status != "success" or not res.data:
29+
print("Failed to create monitor:", res.error)
30+
exit(1)
31+
32+
monitor_id = res.data.cron_id
33+
print(f"Monitor created: {monitor_id}")
34+
print(f"Interval: {res.data.interval}")
35+
print("Webhook configured")
36+
print("\nPolling for activity (Ctrl+C to stop)...\n")
37+
38+
running = True
39+
40+
def cleanup(_sig, _frame):
41+
global running
42+
running = False
43+
44+
signal.signal(signal.SIGINT, cleanup)
45+
46+
while running:
47+
activity = sgai.monitor.activity(monitor_id)
48+
if activity.status == "success" and activity.data:
49+
for tick in activity.data.ticks:
50+
changes = "CHANGED" if tick.changed else "no change"
51+
print(f"[{tick.created_at}] {tick.status} - {changes} ({tick.elapsed_ms}ms)")
52+
diffs = tick.diffs.model_dump(exclude_none=True)
53+
if diffs:
54+
print(f" Diffs: {json.dumps(diffs, indent=2)}")
55+
time.sleep(30)
56+
57+
print("\nStopping monitor...")
58+
sgai.monitor.delete(monitor_id)
59+
print("Monitor deleted")

examples/monitor/monitor_with_webhook_async.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,54 @@
22
load_dotenv()
33

44
import asyncio
5-
from scrapegraph_py import AsyncScrapeGraphAI, MonitorCreateRequest, MarkdownFormatConfig
5+
import json
6+
from scrapegraph_py import AsyncScrapeGraphAI, MonitorCreateRequest, JsonFormatConfig
67

78
async def main():
89
async with AsyncScrapeGraphAI() as sgai:
910
res = await sgai.monitor.create(MonitorCreateRequest(
10-
url="https://example.com",
11-
name="Example Monitor with Webhook",
12-
interval="0 */6 * * *",
11+
url="https://time.is/",
12+
name="Time Monitor with Webhook",
13+
interval="*/10 * * * *",
1314
webhook_url="https://your-webhook-endpoint.com/hook",
14-
formats=[MarkdownFormatConfig()],
15+
formats=[JsonFormatConfig(
16+
prompt="Extract the current time",
17+
schema={
18+
"type": "object",
19+
"properties": {
20+
"time": {"type": "string"},
21+
},
22+
"required": ["time"],
23+
},
24+
)],
1525
))
1626

17-
if res.status == "success":
18-
print("Monitor created:", res.data.cron_id)
19-
print("Status:", res.data.status)
20-
print("Interval:", res.data.interval)
21-
print("Webhook configured")
22-
else:
23-
print("Failed:", res.error)
27+
if res.status != "success" or not res.data:
28+
print("Failed to create monitor:", res.error)
29+
return
30+
31+
monitor_id = res.data.cron_id
32+
print(f"Monitor created: {monitor_id}")
33+
print(f"Interval: {res.data.interval}")
34+
print("Webhook configured")
35+
print("\nPolling for activity (Ctrl+C to stop)...\n")
36+
37+
try:
38+
while True:
39+
activity = await sgai.monitor.activity(monitor_id)
40+
if activity.status == "success" and activity.data:
41+
for tick in activity.data.ticks:
42+
changes = "CHANGED" if tick.changed else "no change"
43+
print(f"[{tick.created_at}] {tick.status} - {changes} ({tick.elapsed_ms}ms)")
44+
diffs = tick.diffs.model_dump(exclude_none=True)
45+
if diffs:
46+
print(f" Diffs: {json.dumps(diffs, indent=2)}")
47+
await asyncio.sleep(30)
48+
except asyncio.CancelledError:
49+
pass
50+
51+
print("\nStopping monitor...")
52+
await sgai.monitor.delete(monitor_id)
53+
print("Monitor deleted")
2454

2555
asyncio.run(main())

src/scrapegraph_py/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@
1919
JsonFormatConfig,
2020
LinksFormatConfig,
2121
MarkdownFormatConfig,
22+
MonitorActivityRequest,
23+
MonitorActivityResponse,
2224
MonitorCreateRequest,
2325
MonitorDiffs,
2426
MonitorResponse,
2527
MonitorResult,
28+
MonitorTickEntry,
2629
MonitorUpdateRequest,
2730
ScrapeRequest,
2831
ScrapeResponse,
@@ -53,6 +56,9 @@
5356
"MonitorResponse",
5457
"MonitorResult",
5558
"MonitorDiffs",
59+
"MonitorActivityRequest",
60+
"MonitorActivityResponse",
61+
"MonitorTickEntry",
5662
"HistoryFilter",
5763
"HistoryPage",
5864
"HistoryEntry",

src/scrapegraph_py/async_client.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
HistoryEntry,
2323
HistoryFilter,
2424
HistoryPage,
25+
MonitorActivityRequest,
26+
MonitorActivityResponse,
2527
MonitorCreateRequest,
2628
MonitorResponse,
2729
MonitorUpdateRequest,
@@ -109,6 +111,14 @@ async def pause(self, id: str) -> ApiResult[MonitorResponse]:
109111
async def resume(self, id: str) -> ApiResult[MonitorResponse]:
110112
return await self._client._post_empty(f"/monitor/{id}/resume", MonitorResponse)
111113

114+
async def activity(
115+
self, id: str, params: MonitorActivityRequest | None = None
116+
) -> ApiResult[MonitorActivityResponse]:
117+
p = params.model_dump(by_alias=True, exclude_none=True) if params else None
118+
return await self._client._get(
119+
f"/monitor/{id}/activity", MonitorActivityResponse, params=p
120+
)
121+
112122

113123
class AsyncHistoryResource:
114124
def __init__(self, client: AsyncScrapeGraphAI):

src/scrapegraph_py/client.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
HistoryEntry,
2323
HistoryFilter,
2424
HistoryPage,
25+
MonitorActivityRequest,
26+
MonitorActivityResponse,
2527
MonitorCreateRequest,
2628
MonitorResponse,
2729
MonitorUpdateRequest,
@@ -109,6 +111,12 @@ def pause(self, id: str) -> ApiResult[MonitorResponse]:
109111
def resume(self, id: str) -> ApiResult[MonitorResponse]:
110112
return self._client._post_empty(f"/monitor/{id}/resume", MonitorResponse)
111113

114+
def activity(
115+
self, id: str, params: MonitorActivityRequest | None = None
116+
) -> ApiResult[MonitorActivityResponse]:
117+
p = params.model_dump(by_alias=True, exclude_none=True) if params else None
118+
return self._client._get(f"/monitor/{id}/activity", MonitorActivityResponse, params=p)
119+
112120

113121
class HistoryResource:
114122
def __init__(self, client: ScrapeGraphAI):

src/scrapegraph_py/schemas.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,33 @@ class MonitorResponse(ResponseModel):
403403
model_config = ConfigDict(extra="allow")
404404

405405

406+
ApiMonitorTickStatus = Literal["completed", "failed", "paused", "running"]
407+
408+
409+
class MonitorTickEntry(ResponseModel):
410+
id: str
411+
status: ApiMonitorTickStatus
412+
created_at: str
413+
elapsed_ms: int
414+
changed: bool
415+
diffs: MonitorDiffs
416+
error: str | None = None
417+
418+
model_config = ConfigDict(extra="allow")
419+
420+
421+
class MonitorActivityResponse(ResponseModel):
422+
ticks: list[MonitorTickEntry]
423+
next_cursor: str | None
424+
425+
model_config = ConfigDict(extra="allow")
426+
427+
428+
class MonitorActivityRequest(CamelModel):
429+
limit: int = Field(default=20, ge=1, le=100)
430+
cursor: str | None = None
431+
432+
406433
class HistoryEntry(ResponseModel):
407434
id: str
408435
service: ApiHistoryService

0 commit comments

Comments
 (0)