Skip to content

Commit 6d79fb9

Browse files
authored
Merge pull request #2261 from leggedrobotics/feat/add_trigger_functionality_cli-sdk
feat(cli+sdk): add triggers functionality to CLI+SDK
2 parents 420b1d4 + 7602f62 commit 6d79fb9

16 files changed

Lines changed: 1248 additions & 160 deletions

cli/kleinkram/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
from kleinkram.wrappers import create_project
66
from kleinkram.wrappers import create_template
77
from kleinkram.wrappers import create_template_version
8+
from kleinkram.wrappers import create_trigger
89
from kleinkram.wrappers import delete_execution
910
from kleinkram.wrappers import delete_file
1011
from kleinkram.wrappers import delete_files
1112
from kleinkram.wrappers import delete_mission
1213
from kleinkram.wrappers import delete_project
1314
from kleinkram.wrappers import delete_template
15+
from kleinkram.wrappers import delete_trigger
1416
from kleinkram.wrappers import download
1517
from kleinkram.wrappers import download_artifact
1618
from kleinkram.wrappers import get_execution
@@ -25,9 +27,11 @@
2527
from kleinkram.wrappers import list_missions
2628
from kleinkram.wrappers import list_projects
2729
from kleinkram.wrappers import list_templates
30+
from kleinkram.wrappers import list_triggers
2831
from kleinkram.wrappers import update_file
2932
from kleinkram.wrappers import update_mission
3033
from kleinkram.wrappers import update_project
34+
from kleinkram.wrappers import update_trigger
3135
from kleinkram.wrappers import upload
3236
from kleinkram.wrappers import verify
3337

@@ -62,4 +66,8 @@
6266
"create_template",
6367
"create_template_version",
6468
"launch_execution",
69+
"create_trigger",
70+
"update_trigger",
71+
"list_triggers",
72+
"delete_trigger",
6573
]

cli/kleinkram/api/deser.py

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,21 @@
1414

1515
from kleinkram.errors import ParsingError
1616
from kleinkram.models import ActionTemplate
17+
from kleinkram.models import ActionTrigger
1718
from kleinkram.models import ArtifactState
1819
from kleinkram.models import Execution
1920
from kleinkram.models import File
21+
from kleinkram.models import FileConfig
2022
from kleinkram.models import FileState
23+
from kleinkram.models import FileTriggerEvent
2124
from kleinkram.models import LogEntry
2225
from kleinkram.models import MetadataValue
2326
from kleinkram.models import Mission
2427
from kleinkram.models import Project
28+
from kleinkram.models import TimeConfig
29+
from kleinkram.models import TriggerConfig
30+
from kleinkram.models import TriggerType
31+
from kleinkram.models import WebhookConfig
2532

2633
__all__ = [
2734
"_parse_project",
@@ -34,6 +41,8 @@
3441
MissionObject = NewType("MissionObject", Dict[str, Any])
3542
FileObject = NewType("FileObject", Dict[str, Any])
3643
ExecutionObject = NewType("ExecutionObject", Dict[str, Any])
44+
TemplateObject = NewType("TemplateObject", Dict[str, Any])
45+
TriggerObject = NewType("TriggerObject", Dict[str, Any])
3746

3847
MISSION = "mission"
3948
PROJECT = "project"
@@ -102,6 +111,19 @@ class TemplateObjectKeys(str, Enum):
102111
VERSION = "version"
103112

104113

114+
class ActionTriggerObjectKeys(str, Enum):
115+
UUID = "uuid"
116+
NAME = "name"
117+
DESCRIPTION = "description"
118+
MISSION_UUID = "missionUuid"
119+
TEMPLATE_NAME = "templateName"
120+
TEMPLATE_UUID = "templateUuid"
121+
TYPE = "type"
122+
CONFIG = "config"
123+
CREATOR_NAME = "creatorName"
124+
CREATOR_UUID = "creatorUuid"
125+
126+
105127
class LogEntryObjectKeys(str, Enum):
106128
TIMESTAMP = "timestamp"
107129
LEVEL = "type"
@@ -230,9 +252,9 @@ def _parse_file(file: FileObject) -> File:
230252
return parsed
231253

232254

233-
def _parse_action_template(template_object: Dict[str, Any]) -> ActionTemplate:
255+
def _parse_action_template(template_object: TemplateObject) -> ActionTemplate:
234256
try:
235-
uuid_ = UUID(template_object[TemplateObjectKeys.UUID], version=4)
257+
uuid = UUID(template_object[TemplateObjectKeys.UUID], version=4)
236258
access_rights = template_object[TemplateObjectKeys.ACCESS_RIGHTS]
237259
command = template_object[TemplateObjectKeys.COMMAND]
238260
cpu_cores = template_object[TemplateObjectKeys.CPU_CORES]
@@ -250,7 +272,7 @@ def _parse_action_template(template_object: Dict[str, Any]) -> ActionTemplate:
250272
raise ParsingError(f"error parsing action template: {template_object}") from e
251273

252274
return ActionTemplate(
253-
uuid=uuid_,
275+
uuid=uuid,
254276
access_rights=access_rights,
255277
command=command,
256278
cpu_cores=cpu_cores,
@@ -268,7 +290,7 @@ def _parse_action_template(template_object: Dict[str, Any]) -> ActionTemplate:
268290

269291
def _parse_execution(execution_object: ExecutionObject) -> Execution:
270292
try:
271-
uuid_ = UUID(execution_object[ExecutionObjectKeys.UUID], version=4)
293+
uuid = UUID(execution_object[ExecutionObjectKeys.UUID], version=4)
272294
state = execution_object[ExecutionObjectKeys.STATE]
273295
state_cause = execution_object[ExecutionObjectKeys.STATE_CAUSE]
274296
artifact_url = execution_object.get(ExecutionObjectKeys.ARTIFACT_URL)
@@ -309,7 +331,7 @@ def _parse_execution(execution_object: ExecutionObject) -> Execution:
309331
raise ParsingError(f"error parsing run: {execution_object}") from e
310332

311333
return Execution(
312-
uuid=uuid_,
334+
uuid=uuid,
313335
state=state,
314336
state_cause=state_cause,
315337
artifact_url=artifact_url,
@@ -324,3 +346,46 @@ def _parse_execution(execution_object: ExecutionObject) -> Execution:
324346
template_name=template_name,
325347
logs=logs,
326348
)
349+
350+
351+
def _parse_action_trigger(trigger_object: TriggerObject) -> ActionTrigger:
352+
try:
353+
uuid = UUID(trigger_object[ActionTriggerObjectKeys.UUID], version=4)
354+
name = trigger_object[ActionTriggerObjectKeys.NAME]
355+
description = trigger_object[ActionTriggerObjectKeys.DESCRIPTION]
356+
mission_uuid = UUID(trigger_object[ActionTriggerObjectKeys.MISSION_UUID], version=4)
357+
template_uuid = UUID(trigger_object[ActionTriggerObjectKeys.TEMPLATE_UUID], version=4)
358+
template_name = trigger_object[ActionTriggerObjectKeys.TEMPLATE_NAME]
359+
type_ = TriggerType(trigger_object[ActionTriggerObjectKeys.TYPE])
360+
creator_name = trigger_object[ActionTriggerObjectKeys.CREATOR_NAME]
361+
creator_uuid = UUID(trigger_object[ActionTriggerObjectKeys.CREATOR_UUID], version=4)
362+
363+
config: TriggerConfig
364+
if type_ is TriggerType.FILE:
365+
raw_config = trigger_object[ActionTriggerObjectKeys.CONFIG]
366+
config = FileConfig(
367+
patterns=tuple(raw_config.get("patterns") or ()),
368+
event=tuple(FileTriggerEvent(e) for e in raw_config.get("event")) if raw_config.get("event") else (),
369+
)
370+
elif type_ is TriggerType.TIME:
371+
config = TimeConfig(**trigger_object[ActionTriggerObjectKeys.CONFIG])
372+
elif type_ is TriggerType.WEBHOOK:
373+
config = WebhookConfig(**trigger_object[ActionTriggerObjectKeys.CONFIG])
374+
else:
375+
raise ParsingError(f"unknown trigger type: {type_}")
376+
377+
except Exception as e:
378+
raise ParsingError(f"error parsing action trigger: {trigger_object}") from e
379+
380+
return ActionTrigger(
381+
uuid=uuid,
382+
name=name,
383+
description=description,
384+
mission_uuid=mission_uuid,
385+
template_uuid=template_uuid,
386+
template_name=template_name,
387+
type=type_,
388+
creator_name=creator_name,
389+
creator_uuid=creator_uuid,
390+
config=config,
391+
)

cli/kleinkram/api/pagination.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def paginated_request(
4747
resp.raise_for_status()
4848

4949
paged_data = resp.json()
50+
5051
data_page = cast(List[DataPage], paged_data["data"])
5152

5253
for entry in data_page:

cli/kleinkram/api/query.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ class ExecutionQuery:
5555
template_name: Optional[str] = None
5656

5757

58+
@dataclass
59+
class TriggerQuery:
60+
"""
61+
This matches the parameters supported by the backend. The backend
62+
does not yet support filtering by list of ids as for
63+
other resources (e.g. projects and missions).
64+
"""
65+
66+
mission_uuid: Optional[UUID] = None
67+
68+
5869
def check_mission_query_is_creatable(query: MissionQuery) -> str:
5970
"""\
6071
check if a query is unique and can be used to create a mission

cli/kleinkram/api/routes.py

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
from kleinkram.api.deser import FileObject
2424
from kleinkram.api.deser import MissionObject
2525
from kleinkram.api.deser import ProjectObject
26+
from kleinkram.api.deser import TemplateObject
27+
from kleinkram.api.deser import TriggerObject
2628
from kleinkram.api.deser import _parse_action_template
29+
from kleinkram.api.deser import _parse_action_trigger
2730
from kleinkram.api.deser import _parse_execution
2831
from kleinkram.api.deser import _parse_file
2932
from kleinkram.api.deser import _parse_mission
@@ -33,6 +36,7 @@
3336
from kleinkram.api.query import FileQuery
3437
from kleinkram.api.query import MissionQuery
3538
from kleinkram.api.query import ProjectQuery
39+
from kleinkram.api.query import TriggerQuery
3640
from kleinkram.api.query import file_query_is_unique
3741
from kleinkram.api.query import mission_query_is_unique
3842
from kleinkram.api.query import project_query_is_unique
@@ -52,10 +56,13 @@
5256
from kleinkram.errors import TemplateNotFound
5357
from kleinkram.errors import TemplateValidationError
5458
from kleinkram.models import ActionTemplate
59+
from kleinkram.models import ActionTrigger
5560
from kleinkram.models import Execution
5661
from kleinkram.models import File
5762
from kleinkram.models import Mission
5863
from kleinkram.models import Project
64+
from kleinkram.models import TriggerConfig
65+
from kleinkram.models import TriggerType
5966
from kleinkram.utils import is_valid_uuid4
6067
from kleinkram.utils import parse_uuid_like
6168
from kleinkram.utils import split_args
@@ -236,7 +243,7 @@ def get_template_revisions(
236243
) -> Generator[ActionTemplate, None, None]:
237244
try:
238245
response_stream = paginated_request(client, f"/templates/{template_id}/revisions")
239-
yield from map(lambda p: _parse_action_template(p), response_stream)
246+
yield from map(lambda p: _parse_action_template(TemplateObject(p)), response_stream)
240247
except ValueError as e:
241248
raise kleinkram.errors.TemplateNotFound(f"Template not found: {template_id}") from e
242249
except httpx.HTTPStatusError:
@@ -260,7 +267,30 @@ def get_templates(
260267
client: AuthenticatedClient,
261268
) -> Generator[ActionTemplate, None, None]:
262269
response_stream = paginated_request(client, "/templates")
263-
yield from map(lambda p: _parse_action_template(p), response_stream)
270+
yield from map(lambda p: _parse_action_template(TemplateObject(p)), response_stream)
271+
272+
273+
LIST_ACTIONTRIGGERS_ENDPOINT = "/triggers"
274+
275+
276+
def get_triggers(client: AuthenticatedClient, query: Optional[TriggerQuery] = None) -> List[ActionTrigger]:
277+
params = {"missionUuid": str(query.mission_uuid)} if query and query.mission_uuid else None
278+
# the backend does not support pagination for triggers currently, so we do a single request
279+
resp = client.get(LIST_ACTIONTRIGGERS_ENDPOINT, params=params)
280+
resp.raise_for_status()
281+
payload = resp.json()
282+
return list(map(lambda p: _parse_action_trigger(TriggerObject(p)), payload))
283+
284+
285+
def get_trigger(
286+
client: AuthenticatedClient,
287+
trigger_uuid: UUID,
288+
) -> ActionTrigger:
289+
resp = client.patch(UPDATE_TRIGGER.format(trigger_uuid), json={})
290+
if resp.status_code == 404:
291+
raise kleinkram.errors.TriggerNotFound(f"Trigger not found: {trigger_uuid}")
292+
resp.raise_for_status()
293+
return _parse_action_trigger(TriggerObject(resp.json()))
264294

265295

266296
def get_project(client: AuthenticatedClient, query: ProjectQuery, exact_match: bool = False) -> Project:
@@ -299,6 +329,29 @@ def get_file(client: AuthenticatedClient, query: FileQuery) -> File:
299329
raise kleinkram.errors.FileNotFound(f"File not found: {query}")
300330

301331

332+
def _create_trigger(
333+
client: AuthenticatedClient,
334+
name: str,
335+
description: str,
336+
template_uuid: UUID,
337+
mission_uuid: UUID,
338+
type_: TriggerType,
339+
config: TriggerConfig,
340+
) -> UUID:
341+
payload = {
342+
"name": name,
343+
"description": description,
344+
"templateUuid": str(template_uuid),
345+
"missionUuid": str(mission_uuid),
346+
"type": type_.value,
347+
"config": config.__dict__,
348+
}
349+
resp = client.post("/triggers", json=payload)
350+
resp.raise_for_status()
351+
352+
return UUID(resp.json()["uuid"], version=4)
353+
354+
302355
def _launch_execution(client: AuthenticatedClient, mission_uuid: UUID, template_uuid: UUID) -> UUID:
303356
"""
304357
Submits a new action to the API and returns the action UUID.
@@ -460,6 +513,42 @@ def _update_project(
460513
resp.raise_for_status()
461514

462515

516+
UPDATE_TRIGGER = "/triggers/{}"
517+
518+
519+
def _update_trigger(
520+
client: AuthenticatedClient,
521+
trigger_uuid: UUID,
522+
*,
523+
name: Optional[str] = None,
524+
description: Optional[str] = None,
525+
template_uuid: Optional[UUID] = None,
526+
mission_uuid: Optional[UUID] = None,
527+
type_: Optional[TriggerType] = None,
528+
config: Optional[TriggerConfig] = None,
529+
) -> None:
530+
531+
if all(v is None for v in [name, description, template_uuid, mission_uuid, type_, config]):
532+
raise ValueError("at least one field must be updated")
533+
534+
body = {}
535+
if name is not None:
536+
body["name"] = name
537+
if description is not None:
538+
body["description"] = description
539+
if template_uuid is not None:
540+
body["templateUuid"] = str(template_uuid)
541+
if mission_uuid is not None:
542+
body["missionUuid"] = str(mission_uuid)
543+
if type_ is not None:
544+
body["type"] = type_.value
545+
if config is not None:
546+
body["config"] = config.__dict__
547+
548+
resp = client.patch(f"{UPDATE_TRIGGER.format(trigger_uuid)}", json=body)
549+
resp.raise_for_status()
550+
551+
463552
def _get_api_version() -> Tuple[int, int, int]:
464553
config = get_config()
465554
client = httpx.Client()
@@ -537,3 +626,13 @@ def _delete_execution(client: AuthenticatedClient, execution_id: UUID) -> None:
537626
if resp.status_code == 404:
538627
raise kleinkram.errors.ExecutionNotFound(f"Execution not found: {execution_id}")
539628
resp.raise_for_status()
629+
630+
631+
DELETE_TRIGGER_ONE = "/triggers/{}"
632+
633+
634+
def _delete_trigger(client: AuthenticatedClient, trigger_uuid: UUID) -> None:
635+
resp = client.delete(DELETE_TRIGGER_ONE.format(trigger_uuid))
636+
if resp.status_code == 404:
637+
raise kleinkram.errors.TriggerNotFound(f"Trigger not found: {trigger_uuid}")
638+
resp.raise_for_status()

0 commit comments

Comments
 (0)