glaip-sdk 0.6.23__py3-none-any.whl → 0.6.25__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glaip_sdk/agents/base.py +31 -0
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agents.py +19 -1
- glaip_sdk/client/main.py +4 -0
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/models/__init__.py +17 -0
- glaip_sdk/models/agent_runs.py +2 -1
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/registry/tool.py +54 -5
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- {glaip_sdk-0.6.23.dist-info → glaip_sdk-0.6.25.dist-info}/METADATA +1 -1
- {glaip_sdk-0.6.23.dist-info → glaip_sdk-0.6.25.dist-info}/RECORD +17 -12
- {glaip_sdk-0.6.23.dist-info → glaip_sdk-0.6.25.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.6.23.dist-info → glaip_sdk-0.6.25.dist-info}/entry_points.txt +0 -0
- {glaip_sdk-0.6.23.dist-info → glaip_sdk-0.6.25.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Schedule DTO models for AIP SDK.
|
|
3
|
+
|
|
4
|
+
These models represent API payloads and responses. They are intentionally DTO-only
|
|
5
|
+
and do not contain runtime behavior.
|
|
6
|
+
|
|
7
|
+
Authors:
|
|
8
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
9
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, ConfigDict
|
|
16
|
+
|
|
17
|
+
from glaip_sdk.models.agent_runs import RunStatus
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ScheduleConfig(BaseModel):
|
|
21
|
+
"""Cron-like schedule configuration matching backend ScheduleConfig.
|
|
22
|
+
|
|
23
|
+
All fields accept cron-style values:
|
|
24
|
+
- Specific values: "0", "9", "1"
|
|
25
|
+
- Wildcards: "*"
|
|
26
|
+
- Intervals: "*/5", "*/2"
|
|
27
|
+
- Ranges: "1-5", "9-17"
|
|
28
|
+
- Lists: "1,3,5"
|
|
29
|
+
|
|
30
|
+
Note: day_of_week uses 0-6 where 0=Monday.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
minute: str = "*"
|
|
34
|
+
hour: str = "*"
|
|
35
|
+
day_of_month: str = "*"
|
|
36
|
+
month: str = "*"
|
|
37
|
+
day_of_week: str = "*"
|
|
38
|
+
|
|
39
|
+
model_config = ConfigDict(from_attributes=True)
|
|
40
|
+
|
|
41
|
+
def to_cron_string(self) -> str:
|
|
42
|
+
"""Convert to standard cron string format.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Cron string in format "minute hour day_of_month month day_of_week"
|
|
46
|
+
"""
|
|
47
|
+
return f"{self.minute} {self.hour} {self.day_of_month} {self.month} {self.day_of_week}"
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_cron_string(cls, cron: str) -> "ScheduleConfig":
|
|
51
|
+
"""Parse a cron string into ScheduleConfig.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
cron: Cron string in format "minute hour day_of_month month day_of_week"
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
ScheduleConfig instance
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
ValueError: If cron string doesn't have exactly 5 fields
|
|
61
|
+
"""
|
|
62
|
+
parts = cron.split()
|
|
63
|
+
if len(parts) != 5:
|
|
64
|
+
raise ValueError(f"Invalid cron string: expected 5 fields, got {len(parts)}")
|
|
65
|
+
return cls(
|
|
66
|
+
minute=parts[0],
|
|
67
|
+
hour=parts[1],
|
|
68
|
+
day_of_month=parts[2],
|
|
69
|
+
month=parts[3],
|
|
70
|
+
day_of_week=parts[4],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ScheduleMetadata(BaseModel):
|
|
75
|
+
"""Metadata embedded in schedule responses.
|
|
76
|
+
|
|
77
|
+
Contains the agent association, input text, and cron configuration.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
agent_id: str
|
|
81
|
+
input: str
|
|
82
|
+
schedule: ScheduleConfig
|
|
83
|
+
|
|
84
|
+
model_config = ConfigDict(from_attributes=True)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ScheduleResponse(BaseModel):
|
|
88
|
+
"""Schedule response DTO.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
id: Schedule ID.
|
|
92
|
+
next_run_time: Next run time as returned by the API.
|
|
93
|
+
time_until_next_run: Human-readable duration until next run.
|
|
94
|
+
metadata: Schedule metadata.
|
|
95
|
+
created_at: Creation timestamp.
|
|
96
|
+
updated_at: Update timestamp.
|
|
97
|
+
agent_id: Agent ID derived from metadata.
|
|
98
|
+
input: Input text derived from metadata.
|
|
99
|
+
schedule_config: ScheduleConfig derived from metadata.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
id: str
|
|
103
|
+
next_run_time: str | None = None
|
|
104
|
+
time_until_next_run: str | None = None
|
|
105
|
+
metadata: ScheduleMetadata | None = None
|
|
106
|
+
created_at: datetime | None = None
|
|
107
|
+
updated_at: datetime | None = None
|
|
108
|
+
|
|
109
|
+
model_config = ConfigDict(from_attributes=True)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def agent_id(self) -> str | None:
|
|
113
|
+
"""Get the agent ID from metadata."""
|
|
114
|
+
return self.metadata.agent_id if self.metadata else None
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def input(self) -> str | None:
|
|
118
|
+
"""Get the scheduled input text from metadata."""
|
|
119
|
+
return self.metadata.input if self.metadata else None
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def schedule_config(self) -> ScheduleConfig | None:
|
|
123
|
+
"""Get the schedule configuration from metadata."""
|
|
124
|
+
return self.metadata.schedule if self.metadata else None
|
|
125
|
+
|
|
126
|
+
def __repr__(self) -> str:
|
|
127
|
+
"""Return a readable representation of the schedule."""
|
|
128
|
+
parts = [f"ScheduleResponse(id={self.id!r}"]
|
|
129
|
+
if self.next_run_time:
|
|
130
|
+
parts.append(f"next_run_time={self.next_run_time!r}")
|
|
131
|
+
if self.time_until_next_run:
|
|
132
|
+
parts.append(f"time_until_next_run={self.time_until_next_run!r}")
|
|
133
|
+
if self.agent_id:
|
|
134
|
+
parts.append(f"agent_id={self.agent_id!r}")
|
|
135
|
+
if self.created_at:
|
|
136
|
+
parts.append(f"created_at={self.created_at!r}")
|
|
137
|
+
return ", ".join(parts) + ")"
|
|
138
|
+
|
|
139
|
+
def __str__(self) -> str:
|
|
140
|
+
"""Return a readable string representation."""
|
|
141
|
+
return self.__repr__()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Type alias for SSE event dictionaries
|
|
145
|
+
ScheduleRunOutputChunk = dict[str, Any]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ScheduleRunResponse(BaseModel):
|
|
149
|
+
"""Schedule run response DTO."""
|
|
150
|
+
|
|
151
|
+
id: str
|
|
152
|
+
agent_id: str
|
|
153
|
+
schedule_id: str | None = None # May be None for non-scheduled runs
|
|
154
|
+
status: RunStatus # Backend uses lowercase.
|
|
155
|
+
run_type: str | None = None # "schedule" for scheduled runs
|
|
156
|
+
started_at: datetime | None = None
|
|
157
|
+
completed_at: datetime | None = None
|
|
158
|
+
input: str | None = None # Input used for the execution.
|
|
159
|
+
config: ScheduleConfig | dict[str, str] | None = None # Schedule config used.
|
|
160
|
+
created_at: datetime | None = None
|
|
161
|
+
updated_at: datetime | None = None
|
|
162
|
+
|
|
163
|
+
model_config = ConfigDict(from_attributes=True, extra="ignore")
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def duration(self) -> str | None:
|
|
167
|
+
"""Calculate the duration of the run.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Formatted duration string (HH:MM:SS) or None if incomplete
|
|
171
|
+
"""
|
|
172
|
+
if self.started_at and self.completed_at:
|
|
173
|
+
delta = self.completed_at - self.started_at
|
|
174
|
+
total_seconds = int(delta.total_seconds())
|
|
175
|
+
hours, remainder = divmod(total_seconds, 3600)
|
|
176
|
+
minutes, seconds = divmod(remainder, 60)
|
|
177
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
def __repr__(self) -> str:
|
|
181
|
+
"""Return a readable representation of the run."""
|
|
182
|
+
parts = [f"ScheduleRunResponse(id={self.id!r}"]
|
|
183
|
+
parts.append(f"status={self.status!r}")
|
|
184
|
+
if self.started_at:
|
|
185
|
+
parts.append(f"started_at={self.started_at.isoformat()!r}")
|
|
186
|
+
if self.duration:
|
|
187
|
+
parts.append(f"duration={self.duration!r}")
|
|
188
|
+
return ", ".join(parts) + ")"
|
|
189
|
+
|
|
190
|
+
def __str__(self) -> str:
|
|
191
|
+
"""Return a readable string representation."""
|
|
192
|
+
return self.__repr__()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ScheduleRunResult(BaseModel):
|
|
196
|
+
"""Full output payload for a schedule run.
|
|
197
|
+
|
|
198
|
+
Maps to the backend's AgentRunWithOutputResponse which includes
|
|
199
|
+
run metadata plus the output stream.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
id: str
|
|
203
|
+
agent_id: str
|
|
204
|
+
schedule_id: str | None = None
|
|
205
|
+
status: RunStatus
|
|
206
|
+
run_type: str | None = None
|
|
207
|
+
started_at: datetime | None = None
|
|
208
|
+
completed_at: datetime | None = None
|
|
209
|
+
input: str | None = None # Input used for the execution.
|
|
210
|
+
config: ScheduleConfig | dict[str, str] | None = None # Schedule config used.
|
|
211
|
+
output: list[ScheduleRunOutputChunk] | None = None
|
|
212
|
+
created_at: datetime | None = None
|
|
213
|
+
updated_at: datetime | None = None
|
|
214
|
+
|
|
215
|
+
model_config = ConfigDict(from_attributes=True, extra="ignore")
|
|
216
|
+
|
|
217
|
+
def __repr__(self) -> str:
|
|
218
|
+
"""Return a readable representation of the result."""
|
|
219
|
+
output_count = len(self.output) if self.output else 0
|
|
220
|
+
return f"ScheduleRunResult(id={self.id!r}, status={self.status!r}, output_chunks={output_count})"
|
|
221
|
+
|
|
222
|
+
def __str__(self) -> str:
|
|
223
|
+
"""Return a readable string representation."""
|
|
224
|
+
return self.__repr__()
|
glaip_sdk/registry/tool.py
CHANGED
|
@@ -66,9 +66,16 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
66
66
|
Raises:
|
|
67
67
|
ValueError: If name cannot be extracted from the reference.
|
|
68
68
|
"""
|
|
69
|
+
# Lazy import to avoid circular dependency
|
|
70
|
+
from glaip_sdk.tools.base import Tool # noqa: PLC0415
|
|
71
|
+
|
|
69
72
|
if isinstance(ref, str):
|
|
70
73
|
return ref
|
|
71
74
|
|
|
75
|
+
# Tool instance (from Tool.from_langchain() or Tool.from_native())
|
|
76
|
+
if isinstance(ref, Tool):
|
|
77
|
+
return ref.get_name()
|
|
78
|
+
|
|
72
79
|
# Dict from API response - extract name or id
|
|
73
80
|
if isinstance(ref, dict):
|
|
74
81
|
return ref.get("name") or ref.get("id") or ""
|
|
@@ -101,17 +108,61 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
101
108
|
ValueError: If the tool cannot be resolved.
|
|
102
109
|
"""
|
|
103
110
|
# Lazy imports to avoid circular dependency
|
|
111
|
+
from glaip_sdk.tools.base import Tool as ToolClass # noqa: PLC0415
|
|
112
|
+
from glaip_sdk.tools.base import ToolType # noqa: PLC0415
|
|
104
113
|
from glaip_sdk.utils.discovery import find_tool # noqa: PLC0415
|
|
105
114
|
from glaip_sdk.utils.sync import update_or_create_tool # noqa: PLC0415
|
|
106
115
|
|
|
107
|
-
#
|
|
116
|
+
# Tool instance from Tool.from_native() or Tool.from_langchain()
|
|
117
|
+
# Use try/except to handle mocked Tool class in tests
|
|
118
|
+
try:
|
|
119
|
+
is_tool_instance = isinstance(ref, ToolClass)
|
|
120
|
+
except TypeError:
|
|
121
|
+
is_tool_instance = False
|
|
122
|
+
|
|
123
|
+
if is_tool_instance:
|
|
124
|
+
# If Tool has an ID, it's already deployed - return as-is
|
|
125
|
+
if ref.id is not None:
|
|
126
|
+
logger.debug("Caching already deployed tool: %s", name)
|
|
127
|
+
self._cache[name] = ref
|
|
128
|
+
# Also cache by id for consistency with other resolution branches
|
|
129
|
+
self._cache[ref.id] = ref
|
|
130
|
+
return ref
|
|
131
|
+
|
|
132
|
+
# Tool.from_native() - look up on platform
|
|
133
|
+
if ref.tool_type == ToolType.NATIVE:
|
|
134
|
+
logger.info("Looking up native tool: %s", name)
|
|
135
|
+
tool = find_tool(name)
|
|
136
|
+
if tool:
|
|
137
|
+
self._cache[name] = tool
|
|
138
|
+
return tool
|
|
139
|
+
raise ValueError(f"Native tool not found on platform: {name}")
|
|
140
|
+
|
|
141
|
+
# Tool.from_langchain() - upload the tool_class
|
|
142
|
+
if ref.tool_class is not None:
|
|
143
|
+
logger.info("Uploading custom tool: %s", name)
|
|
144
|
+
tool = update_or_create_tool(ref.tool_class)
|
|
145
|
+
self._cache[name] = tool
|
|
146
|
+
if tool.id:
|
|
147
|
+
self._cache[tool.id] = tool
|
|
148
|
+
return tool
|
|
149
|
+
|
|
150
|
+
# Unresolvable Tool instance - neither native nor has tool_class
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"Cannot resolve Tool instance: {ref}. "
|
|
153
|
+
f"Tool has no id, is not NATIVE type, and has no tool_class. "
|
|
154
|
+
f"Ensure Tool is created via Tool.from_native() or Tool.from_langchain()."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Already deployed tool (not a ToolClass, but has id/name) - just cache and return
|
|
158
|
+
# This handles API response objects and backward compatibility
|
|
108
159
|
if hasattr(ref, "id") and hasattr(ref, "name") and not isinstance(ref, type):
|
|
109
160
|
if ref.id is not None:
|
|
110
161
|
logger.debug("Caching already deployed tool: %s", name)
|
|
111
162
|
self._cache[name] = ref
|
|
112
163
|
return ref
|
|
113
164
|
|
|
114
|
-
# Tool without ID (
|
|
165
|
+
# Tool without ID (backward compatibility) - look up on platform
|
|
115
166
|
logger.info("Looking up native tool: %s", name)
|
|
116
167
|
tool = find_tool(name)
|
|
117
168
|
if tool:
|
|
@@ -132,9 +183,7 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
132
183
|
if isinstance(ref, dict):
|
|
133
184
|
tool_id = ref.get("id")
|
|
134
185
|
if tool_id:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
tool = Tool(id=tool_id, name=ref.get("name", ""))
|
|
186
|
+
tool = ToolClass(id=tool_id, name=ref.get("name", ""))
|
|
138
187
|
self._cache[name] = tool
|
|
139
188
|
return tool
|
|
140
189
|
raise ValueError(f"Tool dict missing 'id': {ref}")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Schedules runtime package.
|
|
2
|
+
|
|
3
|
+
This package contains runtime schedule resource objects (class-based) that
|
|
4
|
+
encapsulate behavior and API interactions via attached clients.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from glaip_sdk.schedules.base import (
|
|
11
|
+
Schedule,
|
|
12
|
+
ScheduleListResult,
|
|
13
|
+
ScheduleRun,
|
|
14
|
+
ScheduleRunListResult,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Schedule",
|
|
19
|
+
"ScheduleListResult",
|
|
20
|
+
"ScheduleRun",
|
|
21
|
+
"ScheduleRunListResult",
|
|
22
|
+
]
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Schedule runtime resources.
|
|
2
|
+
|
|
3
|
+
This module contains class-based runtime resources for schedules.
|
|
4
|
+
|
|
5
|
+
The runtime resources:
|
|
6
|
+
- Are not Pydantic models.
|
|
7
|
+
- Are returned from public client APIs.
|
|
8
|
+
- Delegate API operations to a bound ScheduleClient.
|
|
9
|
+
|
|
10
|
+
Authors:
|
|
11
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from glaip_sdk.models.agent_runs import RunStatus
|
|
21
|
+
from glaip_sdk.models.schedule import (
|
|
22
|
+
ScheduleConfig,
|
|
23
|
+
ScheduleMetadata,
|
|
24
|
+
ScheduleResponse,
|
|
25
|
+
ScheduleRunResponse,
|
|
26
|
+
ScheduleRunResult,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
30
|
+
from glaip_sdk.client.schedules import ScheduleClient
|
|
31
|
+
|
|
32
|
+
_SCHEDULE_CLIENT_REQUIRED_MSG = "No client available. Use client.schedules.get() to get a client-connected schedule."
|
|
33
|
+
_SCHEDULE_RUN_CLIENT_REQUIRED_MSG = (
|
|
34
|
+
"No client available. Use client.schedules.list_runs() to get a client-connected schedule run."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Schedule:
|
|
39
|
+
"""Runtime schedule resource.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
id (str): The schedule ID.
|
|
43
|
+
next_run_time (str | None): Next run time as returned by the API.
|
|
44
|
+
time_until_next_run (str | None): Human readable duration until next run.
|
|
45
|
+
metadata (ScheduleMetadata | None): Schedule metadata.
|
|
46
|
+
created_at (datetime | None): Creation timestamp.
|
|
47
|
+
updated_at (datetime | None): Update timestamp.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
id: str,
|
|
54
|
+
next_run_time: str | None = None,
|
|
55
|
+
time_until_next_run: str | None = None,
|
|
56
|
+
metadata: ScheduleMetadata | None = None,
|
|
57
|
+
created_at: datetime | None = None,
|
|
58
|
+
updated_at: datetime | None = None,
|
|
59
|
+
_client: ScheduleClient | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Initialize a runtime Schedule."""
|
|
62
|
+
self.id = id
|
|
63
|
+
self.next_run_time = next_run_time
|
|
64
|
+
self.time_until_next_run = time_until_next_run
|
|
65
|
+
self.metadata = metadata
|
|
66
|
+
self.created_at = created_at
|
|
67
|
+
self.updated_at = updated_at
|
|
68
|
+
self._client = _client
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_response(cls, response: ScheduleResponse, *, client: ScheduleClient) -> Schedule:
|
|
72
|
+
"""Build a runtime Schedule from a DTO response.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
response: Parsed schedule response DTO.
|
|
76
|
+
client: ScheduleClient to bind.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Runtime Schedule.
|
|
80
|
+
"""
|
|
81
|
+
return cls(
|
|
82
|
+
id=response.id,
|
|
83
|
+
next_run_time=response.next_run_time,
|
|
84
|
+
time_until_next_run=response.time_until_next_run,
|
|
85
|
+
metadata=response.metadata,
|
|
86
|
+
created_at=response.created_at,
|
|
87
|
+
updated_at=response.updated_at,
|
|
88
|
+
_client=client,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def agent_id(self) -> str | None:
|
|
93
|
+
"""Agent ID derived from metadata."""
|
|
94
|
+
return self.metadata.agent_id if self.metadata else None
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def input(self) -> str | None:
|
|
98
|
+
"""Input text derived from metadata."""
|
|
99
|
+
return self.metadata.input if self.metadata else None
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def schedule_config(self) -> ScheduleConfig | None:
|
|
103
|
+
"""Schedule configuration derived from metadata."""
|
|
104
|
+
return self.metadata.schedule if self.metadata else None
|
|
105
|
+
|
|
106
|
+
def update(
|
|
107
|
+
self,
|
|
108
|
+
*,
|
|
109
|
+
input: str | None = None,
|
|
110
|
+
schedule: ScheduleConfig | dict[str, str] | str | None = None,
|
|
111
|
+
) -> Schedule:
|
|
112
|
+
"""Update this schedule."""
|
|
113
|
+
if self._client is None:
|
|
114
|
+
raise RuntimeError(_SCHEDULE_CLIENT_REQUIRED_MSG)
|
|
115
|
+
return self._client.update(self.id, input=input, schedule=schedule)
|
|
116
|
+
|
|
117
|
+
def delete(self) -> None:
|
|
118
|
+
"""Delete this schedule."""
|
|
119
|
+
if self._client is None:
|
|
120
|
+
raise RuntimeError(_SCHEDULE_CLIENT_REQUIRED_MSG)
|
|
121
|
+
self._client.delete(self.id)
|
|
122
|
+
|
|
123
|
+
def list_runs(
|
|
124
|
+
self,
|
|
125
|
+
*,
|
|
126
|
+
status: RunStatus | None = None,
|
|
127
|
+
limit: int | None = None,
|
|
128
|
+
page: int | None = None,
|
|
129
|
+
) -> ScheduleRunListResult:
|
|
130
|
+
"""List runs for this schedule."""
|
|
131
|
+
if self._client is None:
|
|
132
|
+
raise RuntimeError(_SCHEDULE_CLIENT_REQUIRED_MSG)
|
|
133
|
+
if self.agent_id is None:
|
|
134
|
+
raise ValueError("Schedule has no agent_id")
|
|
135
|
+
return self._client.list_runs(
|
|
136
|
+
self.agent_id,
|
|
137
|
+
schedule_id=self.id,
|
|
138
|
+
status=status,
|
|
139
|
+
limit=limit,
|
|
140
|
+
page=page,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def __repr__(self) -> str:
|
|
144
|
+
"""Return a developer-friendly representation."""
|
|
145
|
+
parts: list[str] = [f"id={self.id!r}"]
|
|
146
|
+
if self.agent_id is not None:
|
|
147
|
+
parts.append(f"agent_id={self.agent_id!r}")
|
|
148
|
+
if self.next_run_time is not None:
|
|
149
|
+
parts.append(f"next_run_time={self.next_run_time!r}")
|
|
150
|
+
if self.time_until_next_run is not None:
|
|
151
|
+
parts.append(f"time_until_next_run={self.time_until_next_run!r}")
|
|
152
|
+
if self.created_at is not None:
|
|
153
|
+
parts.append(f"created_at={self.created_at!r}")
|
|
154
|
+
return f"Schedule({', '.join(parts)})"
|
|
155
|
+
|
|
156
|
+
def __str__(self) -> str:
|
|
157
|
+
"""Return a readable string representation."""
|
|
158
|
+
return self.__repr__()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class ScheduleRun:
|
|
162
|
+
"""Runtime schedule run resource."""
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
*,
|
|
167
|
+
id: str,
|
|
168
|
+
agent_id: str,
|
|
169
|
+
schedule_id: str | None = None,
|
|
170
|
+
status: RunStatus,
|
|
171
|
+
run_type: str | None = None,
|
|
172
|
+
started_at: datetime | None = None,
|
|
173
|
+
completed_at: datetime | None = None,
|
|
174
|
+
input: str | None = None,
|
|
175
|
+
config: ScheduleConfig | dict[str, str] | None = None,
|
|
176
|
+
created_at: datetime | None = None,
|
|
177
|
+
updated_at: datetime | None = None,
|
|
178
|
+
_client: ScheduleClient | None = None,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Initialize a runtime ScheduleRun."""
|
|
181
|
+
self.id = id
|
|
182
|
+
self.agent_id = agent_id
|
|
183
|
+
self.schedule_id = schedule_id
|
|
184
|
+
self.status = status
|
|
185
|
+
self.run_type = run_type
|
|
186
|
+
self.started_at = started_at
|
|
187
|
+
self.completed_at = completed_at
|
|
188
|
+
self.input = input
|
|
189
|
+
self.config = config
|
|
190
|
+
self.created_at = created_at
|
|
191
|
+
self.updated_at = updated_at
|
|
192
|
+
self._client = _client
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def from_response(cls, response: ScheduleRunResponse, *, client: ScheduleClient) -> ScheduleRun:
|
|
196
|
+
"""Build a runtime ScheduleRun from a DTO response."""
|
|
197
|
+
return cls(
|
|
198
|
+
id=response.id,
|
|
199
|
+
agent_id=response.agent_id,
|
|
200
|
+
schedule_id=response.schedule_id,
|
|
201
|
+
status=response.status,
|
|
202
|
+
run_type=response.run_type,
|
|
203
|
+
started_at=response.started_at,
|
|
204
|
+
completed_at=response.completed_at,
|
|
205
|
+
input=response.input,
|
|
206
|
+
config=response.config,
|
|
207
|
+
created_at=response.created_at,
|
|
208
|
+
updated_at=response.updated_at,
|
|
209
|
+
_client=client,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def get_result(self) -> ScheduleRunResult:
|
|
213
|
+
"""Retrieve the full output payload for this run."""
|
|
214
|
+
if self._client is None:
|
|
215
|
+
raise RuntimeError(_SCHEDULE_RUN_CLIENT_REQUIRED_MSG)
|
|
216
|
+
if self.agent_id is None:
|
|
217
|
+
raise ValueError("Schedule run has no agent_id")
|
|
218
|
+
return self._client.get_run_result(self.agent_id, self.id)
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def duration(self) -> str | None:
|
|
222
|
+
"""Formatted duration (HH:MM:SS) when both timestamps are available."""
|
|
223
|
+
if not self.started_at or not self.completed_at:
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
total_seconds = int((self.completed_at - self.started_at).total_seconds())
|
|
227
|
+
minutes, seconds = divmod(total_seconds, 60)
|
|
228
|
+
hours, minutes = divmod(minutes, 60)
|
|
229
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
230
|
+
|
|
231
|
+
def __repr__(self) -> str:
|
|
232
|
+
"""Return a developer-friendly representation."""
|
|
233
|
+
parts: list[str] = [f"id={self.id!r}", f"status={self.status!r}"]
|
|
234
|
+
if self.started_at is not None:
|
|
235
|
+
parts.append(f"started_at={self.started_at.isoformat()!r}")
|
|
236
|
+
duration = self.duration
|
|
237
|
+
if duration is not None:
|
|
238
|
+
parts.append(f"duration={duration!r}")
|
|
239
|
+
return f"ScheduleRun({', '.join(parts)})"
|
|
240
|
+
|
|
241
|
+
def __str__(self) -> str:
|
|
242
|
+
"""Return a readable string representation."""
|
|
243
|
+
return self.__repr__()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass
|
|
247
|
+
class ScheduleListResult:
|
|
248
|
+
"""Paginated list wrapper for runtime schedules."""
|
|
249
|
+
|
|
250
|
+
items: list[Schedule]
|
|
251
|
+
total: int | None = field(default=None)
|
|
252
|
+
page: int | None = field(default=None)
|
|
253
|
+
limit: int | None = field(default=None)
|
|
254
|
+
has_next: bool | None = field(default=None)
|
|
255
|
+
has_prev: bool | None = field(default=None)
|
|
256
|
+
|
|
257
|
+
def __iter__(self):
|
|
258
|
+
"""Iterate over schedules."""
|
|
259
|
+
yield from self.items
|
|
260
|
+
|
|
261
|
+
def __len__(self) -> int:
|
|
262
|
+
"""Return the number of schedules in this page."""
|
|
263
|
+
return self.items.__len__()
|
|
264
|
+
|
|
265
|
+
def __getitem__(self, index: int) -> Schedule:
|
|
266
|
+
"""Return the schedule at the given index."""
|
|
267
|
+
return self.items[index]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass
|
|
271
|
+
class ScheduleRunListResult:
|
|
272
|
+
"""Paginated list wrapper for runtime schedule runs."""
|
|
273
|
+
|
|
274
|
+
items: list[ScheduleRun]
|
|
275
|
+
total: int | None = field(default=None)
|
|
276
|
+
page: int | None = field(default=None)
|
|
277
|
+
limit: int | None = field(default=None)
|
|
278
|
+
has_next: bool | None = field(default=None)
|
|
279
|
+
has_prev: bool | None = field(default=None)
|
|
280
|
+
|
|
281
|
+
def __iter__(self):
|
|
282
|
+
"""Iterate over schedule runs."""
|
|
283
|
+
yield from self.items
|
|
284
|
+
|
|
285
|
+
def __len__(self) -> int:
|
|
286
|
+
"""Return the number of runs in this page."""
|
|
287
|
+
return self.items.__len__()
|
|
288
|
+
|
|
289
|
+
def __getitem__(self, index: int) -> ScheduleRun:
|
|
290
|
+
"""Return the run at the given index."""
|
|
291
|
+
return self.items[index]
|