datatorch 0.4.9.2__tar.gz → 0.6.2__tar.gz
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.
- {datatorch-0.4.9.2 → datatorch-0.6.2}/PKG-INFO +12 -2
- {datatorch-0.4.9.2 → datatorch-0.6.2}/README.md +11 -1
- datatorch-0.6.2/datatorch/agent/agent.py +147 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/client.py +120 -107
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/pipelines/action/action.py +0 -4
- datatorch-0.6.2/datatorch/agent/pipelines/job/job.py +53 -0
- datatorch-0.6.2/datatorch/agent/pipelines/resolver.py +156 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/pipelines/runner/command.py +5 -2
- datatorch-0.6.2/datatorch/agent/pipelines/runner/docker.py +34 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/pipelines/runner/factory.py +7 -0
- datatorch-0.6.2/datatorch/agent/pipelines/runner/runner.py +122 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/pipelines/runner/shell.py +5 -2
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/pipelines/step/step.py +42 -50
- datatorch-0.6.2/datatorch/agent/pipelines/template.py +167 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/action/create.py +3 -1
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/action/run.py +2 -3
- datatorch-0.6.2/datatorch/cli/pipeline/run.py +22 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch.egg-info/PKG-INFO +12 -2
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch.egg-info/SOURCES.txt +1 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/setup.py +1 -1
- datatorch-0.4.9.2/datatorch/agent/agent.py +0 -80
- datatorch-0.4.9.2/datatorch/agent/pipelines/job/job.py +0 -61
- datatorch-0.4.9.2/datatorch/agent/pipelines/runner/docker.py +0 -22
- datatorch-0.4.9.2/datatorch/agent/pipelines/runner/runner.py +0 -80
- datatorch-0.4.9.2/datatorch/agent/pipelines/template.py +0 -157
- datatorch-0.4.9.2/datatorch/cli/pipeline/run.py +0 -30
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/directory.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/log_handler.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/logging.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/monitoring.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/pipelines/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/pipelines/action/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/pipelines/action/cache.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/pipelines/action/config.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/pipelines/job/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/pipelines/pipeline.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/pipelines/runner/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/pipelines/runner/node.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/pipelines/runner/python.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/pipelines/step/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/services/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/utils/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/utils/docker.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/agent/utils/process.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/api.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/client.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/entity/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/entity/annotation.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/entity/base.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/entity/dataset.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/entity/file.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/entity/label.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/entity/project.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/entity/settings.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/entity/sources/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/entity/sources/image/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/entity/sources/image/bounding_box.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/entity/sources/image/segmentations.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/entity/sources/image/typings.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/entity/sources/source.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/entity/storage_link.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/entity/user.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/scripts/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/scripts/import_coco.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/scripts/import_pixmask.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/scripts/utils/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/scripts/utils/simplify.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/utils.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/api/where.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/action/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/action/pull.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/agent/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/agent/create.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/agent/dir.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/agent/start.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/groups.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/import_cmds/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/import_cmds/coco.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/import_cmds/pixmask.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/main/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/main/login.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/main/logout.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/main/upgrade.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/main/version.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/pipeline/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/pipeline/generate.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/pipeline/upload.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/spinner.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/upload/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/upload/folder.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/cli/utils.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/core/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/core/env.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/core/folder.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/core/settings.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/utils/__init__.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/utils/converters.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/utils/files.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/utils/hash_table.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/utils/objects.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/utils/package.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/utils/string_style.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch/utils/url.py +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch.egg-info/dependency_links.txt +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch.egg-info/entry_points.txt +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch.egg-info/not-zip-safe +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch.egg-info/requires.txt +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/datatorch.egg-info/top_level.txt +0 -0
- {datatorch-0.4.9.2 → datatorch-0.6.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datatorch
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: A CLI and library for interacting with DataTorch.
|
|
5
5
|
Home-page: https://github.com/datatorch/python
|
|
6
6
|
Author: DataTorch
|
|
@@ -65,7 +65,7 @@ Dynamic: summary
|
|
|
65
65
|
|
|
66
66
|
## Installation
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
_Note_: Newer versions (>=0.4.8.2) of DataTorch client use `libmagic` for more
|
|
69
69
|
accurate mimetype guessing. This falls back to guessing via extension. Consider
|
|
70
70
|
installing `libmagic` if you have issues with file types uploaded to DataTorch.
|
|
71
71
|
|
|
@@ -86,3 +86,13 @@ pip3 install -r requirements.txt
|
|
|
86
86
|
|
|
87
87
|
Open your VSCode settings, by going `Code -> Preferences -> Settings`. Search
|
|
88
88
|
for "python formatting provider" and select "black" from the dropdown menu.
|
|
89
|
+
|
|
90
|
+
#### Testing
|
|
91
|
+
|
|
92
|
+
python3 -m build
|
|
93
|
+
|
|
94
|
+
python3 -m venv env
|
|
95
|
+
|
|
96
|
+
rm -rf env
|
|
97
|
+
|
|
98
|
+
source env/bin/activate && pip3 install dist/datatorch-0.6.1-py3-none-any.whl && pip3 install pycocotools
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
_Note_: Newer versions (>=0.4.8.2) of DataTorch client use `libmagic` for more
|
|
19
19
|
accurate mimetype guessing. This falls back to guessing via extension. Consider
|
|
20
20
|
installing `libmagic` if you have issues with file types uploaded to DataTorch.
|
|
21
21
|
|
|
@@ -36,3 +36,13 @@ pip3 install -r requirements.txt
|
|
|
36
36
|
|
|
37
37
|
Open your VSCode settings, by going `Code -> Preferences -> Settings`. Search
|
|
38
38
|
for "python formatting provider" and select "black" from the dropdown menu.
|
|
39
|
+
|
|
40
|
+
#### Testing
|
|
41
|
+
|
|
42
|
+
python3 -m build
|
|
43
|
+
|
|
44
|
+
python3 -m venv env
|
|
45
|
+
|
|
46
|
+
rm -rf env
|
|
47
|
+
|
|
48
|
+
source env/bin/activate && pip3 install dist/datatorch-0.6.1-py3-none-any.whl && pip3 install pycocotools
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List
|
|
6
|
+
from .client import AgentApiClient, AgentStepDispatch
|
|
7
|
+
from .pipelines.template import Variables
|
|
8
|
+
from .log_handler import AgentAPIHandler
|
|
9
|
+
from .monitoring import AgentSystemStats
|
|
10
|
+
from .directory import agent_directory
|
|
11
|
+
|
|
12
|
+
from gql.client import AsyncClientSession
|
|
13
|
+
from datatorch.agent.pipelines.step import Step
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
tasks: List[asyncio.Task] = []
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Agent(object):
|
|
20
|
+
"""
|
|
21
|
+
Agent protocol v2: the server sequences steps. The agent receives one
|
|
22
|
+
ready step at a time — its inputs already resolved server-side —
|
|
23
|
+
claims it, executes it, and reports the result. Job-level state and
|
|
24
|
+
cross-step dataflow live entirely on the server.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
async def run(cls, session):
|
|
29
|
+
agent = cls(session)
|
|
30
|
+
await agent.process_loop()
|
|
31
|
+
|
|
32
|
+
def __init__(self, session: AsyncClientSession):
|
|
33
|
+
self.api = AgentApiClient(session)
|
|
34
|
+
self.directory = agent_directory
|
|
35
|
+
# Running step tasks, keyed by stepId, so a server cancel/timeout
|
|
36
|
+
# signal can abort the right one. Entries are cleaned up when the
|
|
37
|
+
# step finishes.
|
|
38
|
+
self._step_tasks: Dict[str, asyncio.Task] = {}
|
|
39
|
+
logger.debug(f"Switch to agent directory: {self.directory.dir}")
|
|
40
|
+
os.chdir(self.directory.dir)
|
|
41
|
+
|
|
42
|
+
self._init_logger()
|
|
43
|
+
self._init_threads()
|
|
44
|
+
|
|
45
|
+
def _init_logger(self):
|
|
46
|
+
self.logger = logging.getLogger("datatorch.agent")
|
|
47
|
+
self.logger_api_handler = AgentAPIHandler(self.api)
|
|
48
|
+
self.logger.addHandler(self.logger_api_handler)
|
|
49
|
+
self.logger.debug("Agent logger has been initalized.")
|
|
50
|
+
|
|
51
|
+
def _init_threads(self):
|
|
52
|
+
self.system_stats = AgentSystemStats(self)
|
|
53
|
+
loop = asyncio.get_running_loop()
|
|
54
|
+
task = loop.create_task(self.system_stats.start())
|
|
55
|
+
tasks.append(task)
|
|
56
|
+
|
|
57
|
+
async def process_loop(self):
|
|
58
|
+
"""Drains steps missed while offline, then waits for pushes.
|
|
59
|
+
|
|
60
|
+
Runs the dispatch stream alongside a cancel-signal listener: the
|
|
61
|
+
server pushes a stop signal when a running step is canceled or
|
|
62
|
+
times out, and the agent aborts that step's task.
|
|
63
|
+
"""
|
|
64
|
+
loop = asyncio.get_running_loop()
|
|
65
|
+
|
|
66
|
+
pending = await self.api.pending_steps()
|
|
67
|
+
if pending:
|
|
68
|
+
logger.info(f"Recovered {len(pending)} pending step(s) on connect.")
|
|
69
|
+
for dispatch in pending:
|
|
70
|
+
self._start_step(dispatch)
|
|
71
|
+
|
|
72
|
+
logger.info("Waiting for steps.")
|
|
73
|
+
cancel_task = loop.create_task(self._cancel_loop())
|
|
74
|
+
tasks.append(cancel_task)
|
|
75
|
+
try:
|
|
76
|
+
async for request in self.api.agent_steps():
|
|
77
|
+
self._start_step(request.get("step"))
|
|
78
|
+
finally:
|
|
79
|
+
cancel_task.cancel()
|
|
80
|
+
|
|
81
|
+
async def _cancel_loop(self):
|
|
82
|
+
"""Aborts a running step's task when the server signals a stop."""
|
|
83
|
+
async for request in self.api.agent_step_cancels():
|
|
84
|
+
cancel = request.get("cancel") or {}
|
|
85
|
+
step_id = cancel.get("stepId")
|
|
86
|
+
task = self._step_tasks.get(step_id)
|
|
87
|
+
if task and not task.done():
|
|
88
|
+
logger.info(f"Received stop signal for step {step_id}.")
|
|
89
|
+
task.cancel()
|
|
90
|
+
else:
|
|
91
|
+
# Unknown/finished step — the signal is best-effort and the
|
|
92
|
+
# server has already moved the step terminal.
|
|
93
|
+
logger.debug(f"Stop signal for step {step_id} ignored (not running).")
|
|
94
|
+
|
|
95
|
+
def _start_step(self, dispatch: AgentStepDispatch):
|
|
96
|
+
"""Schedules a step and tracks its task for cancellation."""
|
|
97
|
+
loop = asyncio.get_running_loop()
|
|
98
|
+
step_id = dispatch.get("stepId")
|
|
99
|
+
task = loop.create_task(self._run_step(dispatch))
|
|
100
|
+
self._step_tasks[step_id] = task
|
|
101
|
+
tasks.append(task)
|
|
102
|
+
|
|
103
|
+
async def _run_step(self, dispatch: AgentStepDispatch):
|
|
104
|
+
"""Claims and executes a single dispatched step."""
|
|
105
|
+
step_id = dispatch.get("stepId")
|
|
106
|
+
step_name = dispatch.get("stepName") or dispatch.get("action")
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
claimed = await self.api.claim_step(step_id)
|
|
110
|
+
if not claimed:
|
|
111
|
+
# Another process (or an earlier duplicate dispatch) owns it.
|
|
112
|
+
logger.debug(f"Step {step_id} already claimed; skipping.")
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
logger.info(f"Step received (name: {step_name}, id: {step_id})")
|
|
116
|
+
step = Step.from_dispatch(dispatch, api=self.api)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
outputs = await step.run(Variables())
|
|
120
|
+
except asyncio.CancelledError:
|
|
121
|
+
# Either a disconnect mid-step (the reconciler fails the
|
|
122
|
+
# orphaned RUNNING step once the agent stays offline) or a
|
|
123
|
+
# server stop signal (the step is already terminal
|
|
124
|
+
# server-side). Either way the child process was killed by
|
|
125
|
+
# the runner; don't report a result — a late completion
|
|
126
|
+
# would be absorbed by the RUNNING guard anyway.
|
|
127
|
+
logger.info(f"Step {step_id} aborted.")
|
|
128
|
+
await step.upload_logs()
|
|
129
|
+
raise
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.error(f"Step {step_id} failed: {e}")
|
|
132
|
+
step.log(f"Step failed: {e}.")
|
|
133
|
+
await step.upload_logs()
|
|
134
|
+
await self.api.complete_step(
|
|
135
|
+
step_id, success=False, error_message=str(e)
|
|
136
|
+
)
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
await self.api.complete_step(
|
|
140
|
+
step_id,
|
|
141
|
+
success=True,
|
|
142
|
+
output=outputs,
|
|
143
|
+
rendered_input=step.rendered_inputs,
|
|
144
|
+
)
|
|
145
|
+
logger.info(f"Step finished (name: {step_name}, id: {step_id})")
|
|
146
|
+
finally:
|
|
147
|
+
self._step_tasks.pop(step_id, None)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
from
|
|
2
|
-
from typing import AsyncGenerator, List, cast
|
|
1
|
+
from typing import AsyncGenerator, List, Optional, cast
|
|
3
2
|
from gql import gql
|
|
4
3
|
from .directory import agent_directory
|
|
5
4
|
from gql.client import AsyncClientSession
|
|
@@ -12,95 +11,152 @@ class Log(TypedDict):
|
|
|
12
11
|
message: str
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
class
|
|
16
|
-
|
|
17
|
-
projectId: str
|
|
18
|
-
creatorId: str
|
|
19
|
-
lastRunNumber: int
|
|
14
|
+
class AgentStepDispatch(TypedDict):
|
|
15
|
+
"""A ready step pushed to this agent (agent protocol v2).
|
|
20
16
|
|
|
17
|
+
``input`` arrives server-resolved: ``steps.*`` and ``input.*``
|
|
18
|
+
references are already substituted with concrete values.
|
|
19
|
+
"""
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
name: str
|
|
21
|
+
stepId: str
|
|
22
|
+
agentId: str
|
|
25
23
|
action: str
|
|
26
|
-
|
|
24
|
+
stepName: Optional[str]
|
|
25
|
+
stepIndex: int
|
|
26
|
+
input: dict
|
|
27
|
+
jobId: str
|
|
28
|
+
jobName: str
|
|
29
|
+
runId: str
|
|
30
|
+
runNumber: int
|
|
31
|
+
pipelineId: str
|
|
32
|
+
projectId: str
|
|
27
33
|
|
|
28
34
|
|
|
29
|
-
class
|
|
30
|
-
|
|
31
|
-
type: str
|
|
32
|
-
event: dict
|
|
35
|
+
class AgentStepRequest(TypedDict):
|
|
36
|
+
step: AgentStepDispatch
|
|
33
37
|
|
|
34
38
|
|
|
35
|
-
class
|
|
36
|
-
|
|
37
|
-
name: str
|
|
38
|
-
text: str
|
|
39
|
-
config: dict
|
|
40
|
-
runNumber: int
|
|
41
|
-
pipeline: AgentPipelineConfig
|
|
42
|
-
trigger: AgentTriggerConfig
|
|
39
|
+
class AgentStepCancel(TypedDict):
|
|
40
|
+
"""A stop signal for a step this agent is currently running."""
|
|
43
41
|
|
|
42
|
+
stepId: str
|
|
43
|
+
agentId: str
|
|
44
44
|
|
|
45
|
-
class AgentJobConfig(TypedDict):
|
|
46
|
-
id: str
|
|
47
|
-
name: str
|
|
48
|
-
run: AgentRunConfig
|
|
49
|
-
steps: List[AgentStepsConfig]
|
|
50
45
|
|
|
46
|
+
class AgentStepCancelRequest(TypedDict):
|
|
47
|
+
cancel: AgentStepCancel
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
|
|
50
|
+
STEP_DISPATCH_FIELDS = """
|
|
51
|
+
stepId
|
|
52
|
+
agentId
|
|
53
|
+
action
|
|
54
|
+
stepName
|
|
55
|
+
stepIndex
|
|
56
|
+
input
|
|
57
|
+
jobId
|
|
58
|
+
jobName
|
|
59
|
+
runId
|
|
60
|
+
runNumber
|
|
61
|
+
pipelineId
|
|
62
|
+
projectId
|
|
63
|
+
"""
|
|
54
64
|
|
|
55
65
|
|
|
56
66
|
class AgentApiClient(object):
|
|
57
67
|
def __init__(self, session: AsyncClientSession):
|
|
58
68
|
self.session = session
|
|
59
69
|
|
|
60
|
-
def
|
|
61
|
-
"""
|
|
70
|
+
def agent_steps(self):
|
|
71
|
+
"""Subscribe to ready steps dispatched to this agent."""
|
|
62
72
|
# fmt: off
|
|
63
73
|
sub = gql("""
|
|
64
74
|
subscription {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
config
|
|
86
|
-
}
|
|
87
|
-
event
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
steps {
|
|
91
|
-
id
|
|
92
|
-
name
|
|
93
|
-
index
|
|
94
|
-
action
|
|
95
|
-
}
|
|
75
|
+
step: agentSteps {
|
|
76
|
+
%s
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
""" % STEP_DISPATCH_FIELDS)
|
|
80
|
+
# fmt: on
|
|
81
|
+
return cast(AsyncGenerator[AgentStepRequest, None], self.session.subscribe(sub))
|
|
82
|
+
|
|
83
|
+
def agent_step_cancels(self):
|
|
84
|
+
"""Subscribe to stop signals for steps this agent is running.
|
|
85
|
+
|
|
86
|
+
Best effort: the server has already moved the step terminal, so a
|
|
87
|
+
missed signal only means the step runs to harmless completion.
|
|
88
|
+
"""
|
|
89
|
+
# fmt: off
|
|
90
|
+
sub = gql("""
|
|
91
|
+
subscription {
|
|
92
|
+
cancel: agentStepCancels {
|
|
93
|
+
stepId
|
|
94
|
+
agentId
|
|
96
95
|
}
|
|
97
96
|
}
|
|
98
97
|
""")
|
|
99
98
|
# fmt: on
|
|
100
99
|
return cast(
|
|
101
|
-
AsyncGenerator[
|
|
100
|
+
AsyncGenerator[AgentStepCancelRequest, None],
|
|
101
|
+
self.session.subscribe(sub),
|
|
102
102
|
)
|
|
103
103
|
|
|
104
|
+
async def pending_steps(self) -> List[AgentStepDispatch]:
|
|
105
|
+
"""Steps dispatched to this agent but not yet claimed.
|
|
106
|
+
|
|
107
|
+
Called on (re)connect: pushes missed while offline are recovered
|
|
108
|
+
here.
|
|
109
|
+
"""
|
|
110
|
+
# fmt: off
|
|
111
|
+
query = """
|
|
112
|
+
query PendingSteps {
|
|
113
|
+
steps: agentPendingSteps {
|
|
114
|
+
%s
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
""" % STEP_DISPATCH_FIELDS
|
|
118
|
+
# fmt: on
|
|
119
|
+
result = await self.execute(query)
|
|
120
|
+
return cast(List[AgentStepDispatch], result.get("steps", []))
|
|
121
|
+
|
|
122
|
+
async def claim_step(self, step_id: str) -> bool:
|
|
123
|
+
"""Atomically claim a dispatched step. False = already claimed."""
|
|
124
|
+
# fmt: off
|
|
125
|
+
mutate = """
|
|
126
|
+
mutation ClaimStep($id: ID!) {
|
|
127
|
+
claimed: claimPipelineStep(id: $id)
|
|
128
|
+
}
|
|
129
|
+
"""
|
|
130
|
+
# fmt: on
|
|
131
|
+
result = await self.execute(mutate, params={"id": step_id})
|
|
132
|
+
return bool(result.get("claimed"))
|
|
133
|
+
|
|
134
|
+
async def complete_step(
|
|
135
|
+
self,
|
|
136
|
+
step_id: str,
|
|
137
|
+
success: bool,
|
|
138
|
+
output: Optional[dict] = None,
|
|
139
|
+
rendered_input: Optional[dict] = None,
|
|
140
|
+
error_message: Optional[str] = None,
|
|
141
|
+
) -> bool:
|
|
142
|
+
"""Report the result of a claimed step."""
|
|
143
|
+
# fmt: off
|
|
144
|
+
mutate = """
|
|
145
|
+
mutation CompleteStep($id: ID!, $input: CompletePipelineStepInput!) {
|
|
146
|
+
completed: completePipelineStep(id: $id, input: $input)
|
|
147
|
+
}
|
|
148
|
+
"""
|
|
149
|
+
# fmt: on
|
|
150
|
+
step_input = {"success": success}
|
|
151
|
+
if output is not None:
|
|
152
|
+
step_input["output"] = output
|
|
153
|
+
if rendered_input is not None:
|
|
154
|
+
step_input["renderedInput"] = rendered_input
|
|
155
|
+
if error_message is not None:
|
|
156
|
+
step_input["errorMessage"] = error_message
|
|
157
|
+
result = await self.execute(mutate, params={"id": step_id, "input": step_input})
|
|
158
|
+
return bool(result.get("completed"))
|
|
159
|
+
|
|
104
160
|
def initial_metrics(self, metrics):
|
|
105
161
|
# fmt: off
|
|
106
162
|
mutate = """
|
|
@@ -170,45 +226,6 @@ class AgentApiClient(object):
|
|
|
170
226
|
params = {"agentId": agent_directory.settings.agent_id, **metrics}
|
|
171
227
|
return self.execute(mutate, params=params)
|
|
172
228
|
|
|
173
|
-
def update_step(self, values: dict):
|
|
174
|
-
# fmt: off
|
|
175
|
-
mutate = """
|
|
176
|
-
mutation UpdateStepRun(
|
|
177
|
-
$id: ID!
|
|
178
|
-
$inputs: JSON
|
|
179
|
-
$outputs: JSON
|
|
180
|
-
$status: PipelineStepStatus
|
|
181
|
-
$startedAt: DateTime
|
|
182
|
-
$finishedAt: DateTime
|
|
183
|
-
) {
|
|
184
|
-
updatePipelineStep(
|
|
185
|
-
id: $id
|
|
186
|
-
input: {
|
|
187
|
-
status: $status
|
|
188
|
-
output: $outputs
|
|
189
|
-
input: $inputs
|
|
190
|
-
startedAt: $startedAt
|
|
191
|
-
finishedAt: $finishedAt
|
|
192
|
-
}
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
"""
|
|
196
|
-
# fmt: on
|
|
197
|
-
return self.execute(mutate, params=values)
|
|
198
|
-
|
|
199
|
-
async def update_job(self, values: dict):
|
|
200
|
-
# fmt: off
|
|
201
|
-
mutate = """
|
|
202
|
-
mutation UpdateJob($id: ID!, $status: PipelineJobStatus) {
|
|
203
|
-
updatePipelineJobRun(
|
|
204
|
-
id: $id,
|
|
205
|
-
input: { status: $status }
|
|
206
|
-
)
|
|
207
|
-
}
|
|
208
|
-
"""
|
|
209
|
-
# fmt: on
|
|
210
|
-
return await self.execute(mutate, params=values)
|
|
211
|
-
|
|
212
229
|
def upload_step_logs(self, step_id: str, logs: List[Log]):
|
|
213
230
|
# fmt: off
|
|
214
231
|
mutate = """
|
|
@@ -227,7 +244,3 @@ class AgentApiClient(object):
|
|
|
227
244
|
return await self.session.execute(
|
|
228
245
|
query, *args, variable_values=removed_none, **kwargs
|
|
229
246
|
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def create_client():
|
|
233
|
-
ApiClient
|
|
@@ -117,10 +117,6 @@ class Action(object):
|
|
|
117
117
|
variable_value = json.loads(variable_value)
|
|
118
118
|
variables.add_input(k, variable_value)
|
|
119
119
|
|
|
120
|
-
if self.step is not None:
|
|
121
|
-
# Update steps output after casting.
|
|
122
|
-
await self.step.update(inputs=variables.inputs)
|
|
123
|
-
|
|
124
120
|
logger.debug(f"Inputs for '{self.full_name}': {json.dumps(variables.inputs)}")
|
|
125
121
|
|
|
126
122
|
output = self.get_cached(variables)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from ..step import Step
|
|
4
|
+
from ..template import Variables
|
|
5
|
+
from ..resolver import UnresolvedReferenceError, resolve_step_input
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("datatorch.agent.job")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Job(object):
|
|
11
|
+
"""
|
|
12
|
+
LOCAL-MODE step sequencer (``datatorch pipeline run <yaml>``).
|
|
13
|
+
|
|
14
|
+
Against the server, jobs are sequenced by the server-side orchestrator
|
|
15
|
+
and this class is not involved — the agent executes one dispatched
|
|
16
|
+
step at a time. Local mode replicates the server's behavior so the
|
|
17
|
+
same yaml works in both places: steps run sequentially, each step's
|
|
18
|
+
``${{ steps.<name>.output.<key> }}`` / ``${{ input.<key> }}``
|
|
19
|
+
references are resolved (strictly) from earlier outputs before it
|
|
20
|
+
runs, and the first failure skips the rest.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, config: dict, trigger_input: dict = None):
|
|
24
|
+
self.config = config
|
|
25
|
+
self.name = config.get("name")
|
|
26
|
+
self.trigger_input = trigger_input or {}
|
|
27
|
+
|
|
28
|
+
async def run(self):
|
|
29
|
+
"""Runs each step of the job, resolving references in between."""
|
|
30
|
+
outputs_by_name = {}
|
|
31
|
+
|
|
32
|
+
for step_config in self.config.get("steps", []):
|
|
33
|
+
name = step_config.get("name")
|
|
34
|
+
try:
|
|
35
|
+
resolved, unresolved = resolve_step_input(
|
|
36
|
+
step_config.get("inputs", {}),
|
|
37
|
+
outputs_by_name,
|
|
38
|
+
self.trigger_input,
|
|
39
|
+
)
|
|
40
|
+
if unresolved:
|
|
41
|
+
raise UnresolvedReferenceError(unresolved)
|
|
42
|
+
|
|
43
|
+
step = Step.from_dict({**step_config, "inputs": resolved})
|
|
44
|
+
outputs = await step.run(Variables())
|
|
45
|
+
if name:
|
|
46
|
+
outputs_by_name[name] = outputs or {}
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.error(f"Job {self.name} failed on step '{name}': {e}")
|
|
49
|
+
logger.error("Skipping remaining steps.")
|
|
50
|
+
raise
|
|
51
|
+
|
|
52
|
+
logger.info(f"Successfully completed job '{self.name}'.")
|
|
53
|
+
return outputs_by_name
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local-mode port of the server's step input resolver.
|
|
3
|
+
|
|
4
|
+
When a pipeline runs against the DataTorch server, the server resolves
|
|
5
|
+
``${{ steps.<name>.output.<key> }}``, ``${{ input.<key> }}`` and
|
|
6
|
+
``${{ jobs.<name>.outputs.<key> }}`` references before dispatching each
|
|
7
|
+
step. Local mode (``datatorch pipeline run``) replicates those exact
|
|
8
|
+
semantics so a pipeline yaml behaves identically in both places:
|
|
9
|
+
|
|
10
|
+
- A reference spanning the whole string resolves to the raw value
|
|
11
|
+
(numbers/objects preserved); embedded references interpolate as strings.
|
|
12
|
+
- ``outputs``/``inputs`` are accepted as aliases of ``output``/``input``.
|
|
13
|
+
- Unresolvable references are an error (strict — matching the server,
|
|
14
|
+
which fails the step at dispatch).
|
|
15
|
+
|
|
16
|
+
Cross-job note: ``${{ jobs.<name>.outputs.<key> }}`` is recognized as a
|
|
17
|
+
server namespace here for parity, but local mode runs a single job at a
|
|
18
|
+
time (there is no cross-job sequencer — see ``job.Job``), so no job
|
|
19
|
+
outputs are supplied and such a reference resolves as unresolvable, i.e.
|
|
20
|
+
raises rather than passing a literal ``${{ }}`` to the action. A yaml that
|
|
21
|
+
uses job dependencies is a server-executed workflow.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
from typing import Any, Dict, List, Set, Tuple
|
|
27
|
+
|
|
28
|
+
# Named groups so the combined alternation reads by name, not by positional
|
|
29
|
+
# index that shifts when a namespace is added — mirrors the server resolver.
|
|
30
|
+
# The name group is ``[^}]+?`` (not ``.+?``) so it can't backtrack across a
|
|
31
|
+
# ``}}`` boundary — a string that both starts and ends with a ref must not be
|
|
32
|
+
# mistaken for one spanning ref.
|
|
33
|
+
STEP_REF = (
|
|
34
|
+
r"\$\{\{\s*steps\.(?P<stepName>[^}]+?)\.outputs?\.(?P<stepKey>[\w.\-]+)\s*\}\}"
|
|
35
|
+
)
|
|
36
|
+
INPUT_REF = r"\$\{\{\s*inputs?\.(?P<inputKey>[\w.\-]+)\s*\}\}"
|
|
37
|
+
JOB_REF = r"\$\{\{\s*jobs\.(?P<jobName>[^}]+?)\.outputs?\.(?P<jobKey>[\w.\-]+)\s*\}\}"
|
|
38
|
+
|
|
39
|
+
SERVER_REF = re.compile(f"{STEP_REF}|{INPUT_REF}|{JOB_REF}")
|
|
40
|
+
WHOLE_STRING_REF = re.compile(f"^(?:{STEP_REF}|{INPUT_REF}|{JOB_REF})$")
|
|
41
|
+
|
|
42
|
+
_MISSING = object()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class UnresolvedReferenceError(Exception):
|
|
46
|
+
def __init__(self, refs: List[str]):
|
|
47
|
+
self.refs = refs
|
|
48
|
+
super().__init__(
|
|
49
|
+
"Unresolvable input reference(s): "
|
|
50
|
+
+ ", ".join(refs)
|
|
51
|
+
+ ". References must point to a named earlier step output, "
|
|
52
|
+
+ "a trigger input, or a needed job output."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _lookup_mapping(mapping: Dict[str, dict], name, key):
|
|
57
|
+
if name is None or key is None:
|
|
58
|
+
return _MISSING
|
|
59
|
+
values = mapping.get(name.strip(), _MISSING)
|
|
60
|
+
if values is _MISSING or not isinstance(values, dict):
|
|
61
|
+
return _MISSING
|
|
62
|
+
return values.get(key, _MISSING)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _lookup(
|
|
66
|
+
outputs_by_name: Dict[str, dict],
|
|
67
|
+
trigger_input: dict,
|
|
68
|
+
job_outputs_by_name: Dict[str, dict],
|
|
69
|
+
groups: Dict[str, Any],
|
|
70
|
+
):
|
|
71
|
+
step = _lookup_mapping(
|
|
72
|
+
outputs_by_name, groups.get("stepName"), groups.get("stepKey")
|
|
73
|
+
)
|
|
74
|
+
if groups.get("stepName") is not None:
|
|
75
|
+
return step
|
|
76
|
+
job = _lookup_mapping(
|
|
77
|
+
job_outputs_by_name, groups.get("jobName"), groups.get("jobKey")
|
|
78
|
+
)
|
|
79
|
+
if groups.get("jobName") is not None:
|
|
80
|
+
return job
|
|
81
|
+
input_key = groups.get("inputKey")
|
|
82
|
+
if input_key is not None:
|
|
83
|
+
return trigger_input.get(input_key, _MISSING)
|
|
84
|
+
return _MISSING
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _resolve(
|
|
88
|
+
value, outputs_by_name, trigger_input, job_outputs_by_name, misses: Set[str]
|
|
89
|
+
):
|
|
90
|
+
if isinstance(value, str):
|
|
91
|
+
whole = WHOLE_STRING_REF.match(value)
|
|
92
|
+
if whole:
|
|
93
|
+
resolved = _lookup(
|
|
94
|
+
outputs_by_name,
|
|
95
|
+
trigger_input,
|
|
96
|
+
job_outputs_by_name,
|
|
97
|
+
whole.groupdict(),
|
|
98
|
+
)
|
|
99
|
+
if resolved is _MISSING:
|
|
100
|
+
misses.add(value.strip())
|
|
101
|
+
return value
|
|
102
|
+
return resolved
|
|
103
|
+
|
|
104
|
+
def replace(match: "re.Match") -> str:
|
|
105
|
+
resolved = _lookup(
|
|
106
|
+
outputs_by_name,
|
|
107
|
+
trigger_input,
|
|
108
|
+
job_outputs_by_name,
|
|
109
|
+
match.groupdict(),
|
|
110
|
+
)
|
|
111
|
+
if resolved is _MISSING:
|
|
112
|
+
misses.add(match.group(0))
|
|
113
|
+
return match.group(0)
|
|
114
|
+
if isinstance(resolved, (dict, list)):
|
|
115
|
+
return json.dumps(resolved)
|
|
116
|
+
if isinstance(resolved, bool):
|
|
117
|
+
# Match the server's String(true|false)
|
|
118
|
+
return "true" if resolved else "false"
|
|
119
|
+
return str(resolved)
|
|
120
|
+
|
|
121
|
+
return SERVER_REF.sub(replace, value)
|
|
122
|
+
if isinstance(value, list):
|
|
123
|
+
return [
|
|
124
|
+
_resolve(v, outputs_by_name, trigger_input, job_outputs_by_name, misses)
|
|
125
|
+
for v in value
|
|
126
|
+
]
|
|
127
|
+
if isinstance(value, dict):
|
|
128
|
+
return {
|
|
129
|
+
k: _resolve(v, outputs_by_name, trigger_input, job_outputs_by_name, misses)
|
|
130
|
+
for k, v in value.items()
|
|
131
|
+
}
|
|
132
|
+
return value
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def resolve_step_input(
|
|
136
|
+
inputs: Any,
|
|
137
|
+
outputs_by_name: Dict[str, dict],
|
|
138
|
+
trigger_input: dict = {},
|
|
139
|
+
job_outputs_by_name: Dict[str, dict] = {},
|
|
140
|
+
) -> Tuple[Any, List[str]]:
|
|
141
|
+
"""Deep-resolve server-namespace references in a step's inputs.
|
|
142
|
+
|
|
143
|
+
Returns ``(resolved, unresolved)`` where ``unresolved`` lists the
|
|
144
|
+
references (as authored) that could not be resolved. ``job_outputs_by_name``
|
|
145
|
+
is empty in local mode (single-job execution), so ``${{ jobs.* }}``
|
|
146
|
+
references resolve as unresolvable.
|
|
147
|
+
"""
|
|
148
|
+
misses: Set[str] = set()
|
|
149
|
+
resolved = _resolve(
|
|
150
|
+
inputs,
|
|
151
|
+
outputs_by_name,
|
|
152
|
+
trigger_input or {},
|
|
153
|
+
job_outputs_by_name or {},
|
|
154
|
+
misses,
|
|
155
|
+
)
|
|
156
|
+
return resolved, sorted(misses)
|