dagster-docker 0.17.19__py3-none-any.whl → 0.28.5__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.
@@ -1,11 +1,15 @@
1
- from dagster._core.utils import check_dagster_package_version
1
+ from dagster_shared.libraries import DagsterLibraryRegistry
2
2
 
3
- from .docker_executor import docker_executor as docker_executor
4
- from .docker_run_launcher import DockerRunLauncher as DockerRunLauncher
5
- from .ops import (
3
+ from dagster_docker.docker_executor import docker_executor as docker_executor
4
+ from dagster_docker.docker_run_launcher import DockerRunLauncher as DockerRunLauncher
5
+ from dagster_docker.ops import (
6
6
  docker_container_op as docker_container_op,
7
7
  execute_docker_container as execute_docker_container,
8
8
  )
9
- from .version import __version__
9
+ from dagster_docker.pipes import (
10
+ PipesDockerClient as PipesDockerClient,
11
+ PipesDockerLogsMessageReader as PipesDockerLogsMessageReader,
12
+ )
13
+ from dagster_docker.version import __version__
10
14
 
11
- check_dagster_package_version("dagster-docker", __version__)
15
+ DagsterLibraryRegistry.register("dagster-docker", __version__)
@@ -1,4 +1,5 @@
1
- from typing import TYPE_CHECKING, Any, Mapping, NamedTuple, Optional, Sequence, cast
1
+ from collections.abc import Mapping, Sequence
2
+ from typing import TYPE_CHECKING, Any, NamedTuple, Optional, cast
2
3
 
3
4
  from dagster import (
4
5
  Array,
@@ -10,10 +11,10 @@ from dagster import (
10
11
  from dagster._config import process_config
11
12
  from dagster._core.container_context import process_shared_container_context_config
12
13
  from dagster._core.errors import DagsterInvalidConfigError
13
- from dagster._core.storage.pipeline_run import DagsterRun
14
+ from dagster._core.storage.dagster_run import DagsterRun
14
15
 
15
16
  if TYPE_CHECKING:
16
- from . import DockerRunLauncher
17
+ from dagster_docker import DockerRunLauncher
17
18
 
18
19
  DOCKER_CONTAINER_CONTEXT_SCHEMA = {
19
20
  "registry": Field(
@@ -79,7 +80,7 @@ class DockerContainerContext(
79
80
  networks: Optional[Sequence[str]] = None,
80
81
  container_kwargs: Optional[Mapping[str, Any]] = None,
81
82
  ):
82
- return super(DockerContainerContext, cls).__new__(
83
+ return super().__new__(
83
84
  cls,
84
85
  registry=check.opt_nullable_mapping_param(registry, "registry"),
85
86
  env_vars=check.opt_sequence_param(env_vars, "env_vars", of_type=str),
@@ -102,7 +103,7 @@ class DockerContainerContext(
102
103
  )
103
104
 
104
105
  @staticmethod
105
- def create_for_run(pipeline_run: DagsterRun, run_launcher: Optional["DockerRunLauncher"]):
106
+ def create_for_run(dagster_run: DagsterRun, run_launcher: Optional["DockerRunLauncher"]):
106
107
  context = DockerContainerContext()
107
108
 
108
109
  # First apply the instance / run_launcher-level context
@@ -117,8 +118,8 @@ class DockerContainerContext(
117
118
  )
118
119
 
119
120
  run_container_context = (
120
- pipeline_run.pipeline_code_origin.repository_origin.container_context
121
- if pipeline_run.pipeline_code_origin
121
+ dagster_run.job_code_origin.repository_origin.container_context
122
+ if dagster_run.job_code_origin
122
123
  else None
123
124
  )
124
125
 
@@ -154,7 +155,7 @@ class DockerContainerContext(
154
155
  run_docker_container_context,
155
156
  )
156
157
 
157
- processed_context_value = cast(Mapping[str, Any], processed_container_context.value)
158
+ processed_context_value = cast("Mapping[str, Any]", processed_container_context.value)
158
159
 
159
160
  return shared_container_context.merge(
160
161
  DockerContainerContext(
@@ -1,13 +1,18 @@
1
- from typing import Iterator, Optional, cast
1
+ from collections.abc import Iterator
2
+ from typing import TYPE_CHECKING, Optional, cast
2
3
 
3
4
  import dagster._check as check
4
5
  import docker
5
6
  import docker.errors
6
7
  from dagster import Field, IntSource, executor
7
- from dagster._annotations import experimental
8
+ from dagster._annotations import beta
8
9
  from dagster._core.definitions.executor_definition import multiple_process_executor_requirements
9
- from dagster._core.events import DagsterEvent, EngineEventData, MetadataEntry
10
+ from dagster._core.events import DagsterEvent, EngineEventData
10
11
  from dagster._core.execution.retries import RetryMode, get_retries_config
12
+ from dagster._core.execution.step_dependency_config import (
13
+ StepDependencyConfig,
14
+ get_step_dependency_config_field,
15
+ )
11
16
  from dagster._core.execution.tags import get_tag_concurrency_limits_config
12
17
  from dagster._core.executor.base import Executor
13
18
  from dagster._core.executor.init import InitExecutorContext
@@ -17,15 +22,15 @@ from dagster._core.executor.step_delegating.step_handler.base import (
17
22
  StepHandler,
18
23
  StepHandlerContext,
19
24
  )
20
- from dagster._core.origin import PipelinePythonOrigin
21
25
  from dagster._core.utils import parse_env_var
22
- from dagster._grpc.types import ExecuteStepArgs
23
- from dagster._serdes.utils import hash_str
24
26
  from dagster._utils.merger import merge_dicts
27
+ from dagster_shared.serdes.utils import hash_str
25
28
 
29
+ from dagster_docker.container_context import DockerContainerContext
26
30
  from dagster_docker.utils import DOCKER_CONFIG_SCHEMA, validate_docker_config, validate_docker_image
27
31
 
28
- from .container_context import DockerContainerContext
32
+ if TYPE_CHECKING:
33
+ from dagster._core.origin import JobPythonOrigin
29
34
 
30
35
 
31
36
  @executor(
@@ -43,14 +48,14 @@ from .container_context import DockerContainerContext
43
48
  ),
44
49
  ),
45
50
  "tag_concurrency_limits": get_tag_concurrency_limits_config(),
51
+ "step_dependency_config": get_step_dependency_config_field(),
46
52
  },
47
53
  ),
48
54
  requirements=multiple_process_executor_requirements(),
49
55
  )
50
- @experimental
56
+ @beta
51
57
  def docker_executor(init_context: InitExecutorContext) -> Executor:
52
- """
53
- Executor which launches steps as Docker containers.
58
+ """Executor which launches steps as Docker containers.
54
59
 
55
60
  To use the `docker_executor`, set it as the `executor_def` when defining a job:
56
61
 
@@ -101,6 +106,9 @@ def docker_executor(init_context: InitExecutorContext) -> Executor:
101
106
  retries=check.not_none(RetryMode.from_config(retries)),
102
107
  max_concurrent=max_concurrent,
103
108
  tag_concurrency_limits=tag_concurrency_limits,
109
+ step_dependency_config=StepDependencyConfig.from_config(
110
+ config.get("step_dependency_config") # type: ignore
111
+ ),
104
112
  )
105
113
 
106
114
 
@@ -118,10 +126,10 @@ class DockerStepHandler(StepHandler):
118
126
  )
119
127
 
120
128
  def _get_image(self, step_handler_context: StepHandlerContext):
121
- from . import DockerRunLauncher
129
+ from dagster_docker import DockerRunLauncher
122
130
 
123
131
  image = cast(
124
- PipelinePythonOrigin, step_handler_context.pipeline_run.pipeline_code_origin
132
+ "JobPythonOrigin", step_handler_context.dagster_run.job_code_origin
125
133
  ).repository_origin.container_image
126
134
  if not image:
127
135
  image = self._image
@@ -138,13 +146,13 @@ class DockerStepHandler(StepHandler):
138
146
 
139
147
  def _get_docker_container_context(self, step_handler_context: StepHandlerContext):
140
148
  # This doesn't vary per step: would be good to have a hook where it can be set once
141
- # for the whole StepHandler but we need access to the PipelineRun for that
149
+ # for the whole StepHandler but we need access to the DagsterRun for that
142
150
 
143
- from .docker_run_launcher import DockerRunLauncher
151
+ from dagster_docker.docker_run_launcher import DockerRunLauncher
144
152
 
145
153
  run_launcher = step_handler_context.instance.run_launcher
146
154
  run_target = DockerContainerContext.create_for_run(
147
- step_handler_context.pipeline_run,
155
+ step_handler_context.dagster_run,
148
156
  run_launcher if isinstance(run_launcher, DockerRunLauncher) else None,
149
157
  )
150
158
 
@@ -172,11 +180,17 @@ class DockerStepHandler(StepHandler):
172
180
  )
173
181
  return client
174
182
 
175
- def _get_container_name(self, execute_step_args: ExecuteStepArgs):
176
- run_id = execute_step_args.pipeline_run_id
177
- step_keys_to_execute = check.not_none(execute_step_args.step_keys_to_execute)
183
+ def _get_step_key(self, step_handler_context: StepHandlerContext) -> str:
184
+ step_keys_to_execute = cast(
185
+ "list[str]", step_handler_context.execute_step_args.step_keys_to_execute
186
+ )
178
187
  assert len(step_keys_to_execute) == 1, "Launching multiple steps is not currently supported"
179
- step_key = step_keys_to_execute[0]
188
+ return step_keys_to_execute[0]
189
+
190
+ def _get_container_name(self, step_handler_context: StepHandlerContext):
191
+ execute_step_args = step_handler_context.execute_step_args
192
+ run_id = execute_step_args.run_id
193
+ step_key = self._get_step_key(step_handler_context)
180
194
 
181
195
  step_name = f"dagster-step-{hash_str(run_id + step_key)}"
182
196
 
@@ -200,17 +214,20 @@ class DockerStepHandler(StepHandler):
200
214
  assert len(step_keys_to_execute) == 1, "Launching multiple steps is not currently supported"
201
215
  step_key = step_keys_to_execute[0]
202
216
 
217
+ container_kwargs = {**container_context.container_kwargs}
218
+ container_kwargs.pop("stop_timeout", None)
219
+
203
220
  env_vars = dict([parse_env_var(env_var) for env_var in container_context.env_vars])
204
- env_vars["DAGSTER_RUN_JOB_NAME"] = step_handler_context.pipeline_run.job_name
221
+ env_vars["DAGSTER_RUN_JOB_NAME"] = step_handler_context.dagster_run.job_name
205
222
  env_vars["DAGSTER_RUN_STEP_KEY"] = step_key
206
223
  return client.containers.create(
207
224
  step_image,
208
- name=self._get_container_name(execute_step_args),
225
+ name=self._get_container_name(step_handler_context),
209
226
  detach=True,
210
227
  network=container_context.networks[0] if len(container_context.networks) else None,
211
228
  command=execute_step_args.get_command_args(),
212
229
  environment=env_vars,
213
- **container_context.container_kwargs,
230
+ **container_kwargs,
214
231
  )
215
232
 
216
233
  def launch_step(self, step_handler_context: StepHandlerContext) -> Iterator[DagsterEvent]:
@@ -245,9 +262,9 @@ class DockerStepHandler(StepHandler):
245
262
  yield DagsterEvent.step_worker_starting(
246
263
  step_handler_context.get_step_context(step_key),
247
264
  message="Launching step in Docker container.",
248
- metadata_entries=[
249
- MetadataEntry("Docker container id", value=step_container.id),
250
- ],
265
+ metadata={
266
+ "Docker container id": step_container.id,
267
+ },
251
268
  )
252
269
  step_container.start()
253
270
 
@@ -256,9 +273,15 @@ class DockerStepHandler(StepHandler):
256
273
 
257
274
  client = self._get_client(container_context)
258
275
 
259
- container_name = self._get_container_name(step_handler_context.execute_step_args)
276
+ container_name = self._get_container_name(step_handler_context)
277
+ step_key = self._get_step_key(step_handler_context)
260
278
 
261
- container = client.containers.get(container_name)
279
+ try:
280
+ container = client.containers.get(container_name)
281
+ except docker.errors.NotFound:
282
+ return CheckStepHealthResult.unhealthy(
283
+ reason=f"Docker container {container_name} for step {step_key} could not be found."
284
+ )
262
285
 
263
286
  if container.status == "running":
264
287
  return CheckStepHealthResult.healthy()
@@ -276,7 +299,7 @@ class DockerStepHandler(StepHandler):
276
299
  return CheckStepHealthResult.healthy()
277
300
 
278
301
  return CheckStepHealthResult.unhealthy(
279
- reason=f"Container status is {container.status}. Return code is {str(ret_code)}."
302
+ reason=f"Container status is {container.status}. Return code is {ret_code}."
280
303
  )
281
304
 
282
305
  def terminate_step(self, step_handler_context: StepHandlerContext) -> Iterator[DagsterEvent]:
@@ -285,12 +308,12 @@ class DockerStepHandler(StepHandler):
285
308
  step_keys_to_execute = check.not_none(
286
309
  step_handler_context.execute_step_args.step_keys_to_execute
287
310
  )
288
- assert (
289
- len(step_keys_to_execute) == 1
290
- ), "Terminating multiple steps is not currently supported"
311
+ assert len(step_keys_to_execute) == 1, (
312
+ "Terminating multiple steps is not currently supported"
313
+ )
291
314
  step_key = step_keys_to_execute[0]
292
315
 
293
- container_name = self._get_container_name(step_handler_context.execute_step_args)
316
+ container_name = self._get_container_name(step_handler_context)
294
317
 
295
318
  yield DagsterEvent.engine_event(
296
319
  step_handler_context.get_step_context(step_key),
@@ -302,4 +325,6 @@ class DockerStepHandler(StepHandler):
302
325
 
303
326
  container = client.containers.get(container_name)
304
327
 
305
- container.stop()
328
+ stop_timeout = container_context.container_kwargs.get("stop_timeout")
329
+
330
+ container.stop(timeout=stop_timeout)
@@ -1,3 +1,7 @@
1
+ import json
2
+ from collections.abc import Mapping
3
+ from typing import Any, Optional
4
+
1
5
  import dagster._check as check
2
6
  import docker
3
7
  from dagster._core.launcher.base import (
@@ -7,16 +11,17 @@ from dagster._core.launcher.base import (
7
11
  RunLauncher,
8
12
  WorkerStatus,
9
13
  )
10
- from dagster._core.storage.pipeline_run import DagsterRun
14
+ from dagster._core.storage.dagster_run import DagsterRun
11
15
  from dagster._core.storage.tags import DOCKER_IMAGE_TAG
12
16
  from dagster._core.utils import parse_env_var
13
17
  from dagster._grpc.types import ExecuteRunArgs, ResumeRunArgs
14
18
  from dagster._serdes import ConfigurableClass
19
+ from dagster._serdes.config_class import ConfigurableClassData
20
+ from typing_extensions import Self
15
21
 
22
+ from dagster_docker.container_context import DockerContainerContext
16
23
  from dagster_docker.utils import DOCKER_CONFIG_SCHEMA, validate_docker_config, validate_docker_image
17
24
 
18
- from .container_context import DockerContainerContext
19
-
20
25
  DOCKER_CONTAINER_ID_TAG = "docker/container_id"
21
26
 
22
27
 
@@ -25,7 +30,7 @@ class DockerRunLauncher(RunLauncher, ConfigurableClass):
25
30
 
26
31
  def __init__(
27
32
  self,
28
- inst_data=None,
33
+ inst_data: Optional[ConfigurableClassData] = None,
29
34
  image=None,
30
35
  registry=None,
31
36
  env_vars=None,
@@ -61,12 +66,14 @@ class DockerRunLauncher(RunLauncher, ConfigurableClass):
61
66
  def config_type(cls):
62
67
  return DOCKER_CONFIG_SCHEMA
63
68
 
64
- @staticmethod
65
- def from_config_value(inst_data, config_value):
66
- return DockerRunLauncher(inst_data=inst_data, **config_value)
69
+ @classmethod
70
+ def from_config_value(
71
+ cls, inst_data: ConfigurableClassData, config_value: Mapping[str, Any]
72
+ ) -> Self:
73
+ return cls(inst_data=inst_data, **config_value)
67
74
 
68
- def get_container_context(self, pipeline_run: DagsterRun) -> DockerContainerContext:
69
- return DockerContainerContext.create_for_run(pipeline_run, self)
75
+ def get_container_context(self, dagster_run: DagsterRun) -> DockerContainerContext:
76
+ return DockerContainerContext.create_for_run(dagster_run, self)
70
77
 
71
78
  def _get_client(self, container_context: DockerContainerContext):
72
79
  client = docker.client.from_env()
@@ -78,8 +85,8 @@ class DockerRunLauncher(RunLauncher, ConfigurableClass):
78
85
  )
79
86
  return client
80
87
 
81
- def _get_docker_image(self, pipeline_code_origin):
82
- docker_image = pipeline_code_origin.repository_origin.container_image
88
+ def _get_docker_image(self, job_code_origin):
89
+ docker_image = job_code_origin.repository_origin.container_image
83
90
 
84
91
  if not docker_image:
85
92
  docker_image = self.image
@@ -97,6 +104,17 @@ class DockerRunLauncher(RunLauncher, ConfigurableClass):
97
104
 
98
105
  client = self._get_client(container_context)
99
106
 
107
+ container_kwargs = {**container_context.container_kwargs}
108
+ labels = container_kwargs.pop("labels", {})
109
+
110
+ container_kwargs.pop("stop_timeout", None)
111
+
112
+ if isinstance(labels, list):
113
+ labels = {key: "" for key in labels}
114
+
115
+ labels["dagster/run_id"] = run.run_id
116
+ labels["dagster/job_name"] = run.job_name
117
+
100
118
  try:
101
119
  container = client.containers.create(
102
120
  image=docker_image,
@@ -104,10 +122,11 @@ class DockerRunLauncher(RunLauncher, ConfigurableClass):
104
122
  detach=True,
105
123
  environment=docker_env,
106
124
  network=container_context.networks[0] if len(container_context.networks) else None,
107
- **container_context.container_kwargs,
125
+ labels=labels,
126
+ **container_kwargs,
108
127
  )
109
128
 
110
- except docker.errors.ImageNotFound:
129
+ except docker.errors.ImageNotFound: # pyright: ignore[reportAttributeAccessIssue]
111
130
  client.images.pull(docker_image)
112
131
  container = client.containers.create(
113
132
  image=docker_image,
@@ -115,7 +134,8 @@ class DockerRunLauncher(RunLauncher, ConfigurableClass):
115
134
  detach=True,
116
135
  environment=docker_env,
117
136
  network=container_context.networks[0] if len(container_context.networks) else None,
118
- **container_context.container_kwargs,
137
+ labels=labels,
138
+ **container_kwargs,
119
139
  )
120
140
 
121
141
  if len(container_context.networks) > 1:
@@ -124,31 +144,26 @@ class DockerRunLauncher(RunLauncher, ConfigurableClass):
124
144
  network.connect(container)
125
145
 
126
146
  self._instance.report_engine_event(
127
- message=(
128
- "Launching run in a new container {container_id} with image {docker_image}".format(
129
- container_id=container.id,
130
- docker_image=docker_image,
131
- )
132
- ),
133
- pipeline_run=run,
147
+ message=f"Launching run in a new container {container.id} with image {docker_image}",
148
+ dagster_run=run,
134
149
  cls=self.__class__,
135
150
  )
136
151
 
137
152
  self._instance.add_run_tags(
138
153
  run.run_id,
139
- {DOCKER_CONTAINER_ID_TAG: container.id, DOCKER_IMAGE_TAG: docker_image},
154
+ {DOCKER_CONTAINER_ID_TAG: container.id, DOCKER_IMAGE_TAG: docker_image}, # pyright: ignore[reportArgumentType]
140
155
  )
141
156
 
142
157
  container.start()
143
158
 
144
159
  def launch_run(self, context: LaunchRunContext) -> None:
145
- run = context.pipeline_run
146
- pipeline_code_origin = check.not_none(context.pipeline_code_origin)
147
- docker_image = self._get_docker_image(pipeline_code_origin)
160
+ run = context.dagster_run
161
+ job_code_origin = check.not_none(context.job_code_origin)
162
+ docker_image = self._get_docker_image(job_code_origin)
148
163
 
149
164
  command = ExecuteRunArgs(
150
- pipeline_origin=pipeline_code_origin,
151
- pipeline_run_id=run.run_id,
165
+ job_origin=job_code_origin,
166
+ run_id=run.run_id,
152
167
  instance_ref=self._instance.get_ref(),
153
168
  ).get_command_args()
154
169
 
@@ -159,20 +174,20 @@ class DockerRunLauncher(RunLauncher, ConfigurableClass):
159
174
  return True
160
175
 
161
176
  def resume_run(self, context: ResumeRunContext) -> None:
162
- run = context.pipeline_run
163
- pipeline_code_origin = check.not_none(context.pipeline_code_origin)
164
- docker_image = self._get_docker_image(pipeline_code_origin)
177
+ run = context.dagster_run
178
+ job_code_origin = check.not_none(context.job_code_origin)
179
+ docker_image = self._get_docker_image(job_code_origin)
165
180
 
166
181
  command = ResumeRunArgs(
167
- pipeline_origin=pipeline_code_origin,
168
- pipeline_run_id=run.run_id,
182
+ job_origin=job_code_origin,
183
+ run_id=run.run_id,
169
184
  instance_ref=self._instance.get_ref(),
170
185
  ).get_command_args()
171
186
 
172
187
  self._launch_container_with_command(run, docker_image, command)
173
188
 
174
189
  def _get_container(self, run):
175
- if not run or run.is_finished:
190
+ if not run:
176
191
  return None
177
192
 
178
193
  container_id = run.tags.get(DOCKER_CONTAINER_ID_TAG)
@@ -184,24 +199,31 @@ class DockerRunLauncher(RunLauncher, ConfigurableClass):
184
199
 
185
200
  try:
186
201
  return self._get_client(container_context).containers.get(container_id)
187
- except Exception:
202
+ except docker.errors.NotFound: # pyright: ignore[reportAttributeAccessIssue]
188
203
  return None
189
204
 
190
205
  def terminate(self, run_id):
191
206
  run = self._instance.get_run_by_id(run_id)
207
+
208
+ if not run or run.is_finished:
209
+ return False
210
+
211
+ self._instance.report_run_canceling(run)
212
+
192
213
  container = self._get_container(run)
193
214
 
215
+ container_context = self.get_container_context(run)
216
+ stop_timeout = container_context.container_kwargs.get("stop_timeout")
217
+
194
218
  if not container:
195
219
  self._instance.report_engine_event(
196
220
  message="Unable to get docker container to send termination request to.",
197
- pipeline_run=run,
221
+ dagster_run=run,
198
222
  cls=self.__class__,
199
223
  )
200
224
  return False
201
225
 
202
- self._instance.report_run_canceling(run)
203
-
204
- container.stop()
226
+ container.stop(timeout=stop_timeout)
205
227
 
206
228
  return True
207
229
 
@@ -210,11 +232,22 @@ class DockerRunLauncher(RunLauncher, ConfigurableClass):
210
232
  return True
211
233
 
212
234
  def check_run_worker_health(self, run: DagsterRun):
235
+ container_id = run.tags.get(DOCKER_CONTAINER_ID_TAG)
236
+
237
+ if not container_id:
238
+ return CheckRunHealthResult(WorkerStatus.NOT_FOUND, msg="No container ID tag for run.")
239
+
213
240
  container = self._get_container(run)
214
241
  if container is None:
215
- return CheckRunHealthResult(WorkerStatus.NOT_FOUND)
242
+ return CheckRunHealthResult(
243
+ WorkerStatus.NOT_FOUND, msg=f"Could not find container with ID {container_id}."
244
+ )
216
245
  if container.status == "running":
217
246
  return CheckRunHealthResult(WorkerStatus.RUNNING)
218
- return CheckRunHealthResult(
219
- WorkerStatus.FAILED, msg=f"Container status is {container.status}"
247
+
248
+ container_state = container.attrs.get("State")
249
+ failure_string = f"Container status is {container.status}." + (
250
+ f" Container state: {json.dumps(container_state)}" if container_state else ""
220
251
  )
252
+
253
+ return CheckRunHealthResult(WorkerStatus.FAILED, msg=failure_string)
@@ -1,4 +1,4 @@
1
- from .docker_container_op import (
1
+ from dagster_docker.ops.docker_container_op import (
2
2
  docker_container_op as docker_container_op,
3
3
  execute_docker_container as execute_docker_container,
4
4
  )
@@ -1,14 +1,16 @@
1
- from typing import Any, Mapping, Optional, Sequence
1
+ from collections.abc import Mapping, Sequence
2
+ from typing import Any, Optional
2
3
 
3
4
  import docker
5
+ import docker.errors
4
6
  from dagster import Field, In, Nothing, OpExecutionContext, StringSource, op
5
- from dagster._annotations import experimental
7
+ from dagster._annotations import beta
6
8
  from dagster._core.utils import parse_env_var
7
- from dagster._serdes.utils import hash_str
9
+ from dagster_shared.serdes.utils import hash_str
8
10
 
9
- from ..container_context import DockerContainerContext
10
- from ..docker_run_launcher import DockerRunLauncher
11
- from ..utils import DOCKER_CONFIG_SCHEMA, validate_docker_image
11
+ from dagster_docker.container_context import DockerContainerContext
12
+ from dagster_docker.docker_run_launcher import DockerRunLauncher
13
+ from dagster_docker.utils import DOCKER_CONFIG_SCHEMA, validate_docker_image
12
14
 
13
15
  DOCKER_CONTAINER_OP_CONFIG = {
14
16
  **DOCKER_CONFIG_SCHEMA,
@@ -44,7 +46,6 @@ def _get_client(docker_container_context: DockerContainerContext):
44
46
  def _get_container_name(run_id, op_name, retry_number):
45
47
  container_name = hash_str(run_id + op_name)
46
48
 
47
- retry_number = retry_number
48
49
  if retry_number > 0:
49
50
  container_name = f"{container_name}-{retry_number}"
50
51
 
@@ -72,7 +73,7 @@ def _create_container(
72
73
  )
73
74
 
74
75
 
75
- @experimental
76
+ @beta
76
77
  def execute_docker_container(
77
78
  context: OpExecutionContext,
78
79
  image: str,
@@ -83,8 +84,7 @@ def execute_docker_container(
83
84
  env_vars: Optional[Sequence[str]] = None,
84
85
  container_kwargs: Optional[Mapping[str, Any]] = None,
85
86
  ):
86
- """
87
- This function is a utility for executing a Docker container from within a Dagster op.
87
+ """This function is a utility for executing a Docker container from within a Dagster op.
88
88
 
89
89
  Args:
90
90
  image (str): The image to use for the launched Docker container.
@@ -105,10 +105,12 @@ def execute_docker_container(
105
105
  of available options.
106
106
  """
107
107
  run_container_context = DockerContainerContext.create_for_run(
108
- context.pipeline_run,
109
- context.instance.run_launcher
110
- if isinstance(context.instance.run_launcher, DockerRunLauncher)
111
- else None,
108
+ context.dagster_run,
109
+ (
110
+ context.instance.run_launcher
111
+ if isinstance(context.instance.run_launcher, DockerRunLauncher)
112
+ else None
113
+ ),
112
114
  )
113
115
 
114
116
  validate_docker_image(image)
@@ -139,7 +141,7 @@ def execute_docker_container(
139
141
  container.start()
140
142
 
141
143
  for line in container.logs(stdout=True, stderr=True, stream=True, follow=True):
142
- print(line)
144
+ print(line) # noqa: T201
143
145
 
144
146
  exit_status = container.wait()["StatusCode"]
145
147
 
@@ -148,10 +150,9 @@ def execute_docker_container(
148
150
 
149
151
 
150
152
  @op(ins={"start_after": In(Nothing)}, config_schema=DOCKER_CONTAINER_OP_CONFIG)
151
- @experimental
153
+ @beta
152
154
  def docker_container_op(context):
153
- """
154
- An op that runs a Docker container using the docker Python API.
155
+ """An op that runs a Docker container using the docker Python API.
155
156
 
156
157
  Contrast with the `docker_executor`, which runs each Dagster op in a Dagster job in its
157
158
  own Docker container.
@@ -0,0 +1,208 @@
1
+ from collections.abc import Iterator, Mapping, Sequence
2
+ from contextlib import contextmanager
3
+ from typing import Any, Optional, Union
4
+
5
+ import docker
6
+ import docker.errors
7
+ from dagster import (
8
+ OpExecutionContext,
9
+ _check as check,
10
+ )
11
+ from dagster._core.definitions.resource_annotation import TreatAsResourceParam
12
+ from dagster._core.execution.context.asset_execution_context import AssetExecutionContext
13
+ from dagster._core.pipes.client import (
14
+ PipesClient,
15
+ PipesClientCompletedInvocation,
16
+ PipesContextInjector,
17
+ PipesMessageReader,
18
+ )
19
+ from dagster._core.pipes.context import PipesMessageHandler
20
+ from dagster._core.pipes.utils import (
21
+ PipesEnvContextInjector,
22
+ extract_message_or_forward_to_stdout,
23
+ open_pipes_session,
24
+ )
25
+ from dagster_pipes import DagsterPipesError, PipesDefaultMessageWriter, PipesExtras, PipesParams
26
+
27
+
28
+ class PipesDockerLogsMessageReader(PipesMessageReader):
29
+ @contextmanager
30
+ def read_messages(
31
+ self,
32
+ handler: PipesMessageHandler,
33
+ ) -> Iterator[PipesParams]:
34
+ self._handler = handler
35
+ try:
36
+ yield {PipesDefaultMessageWriter.STDIO_KEY: PipesDefaultMessageWriter.STDERR}
37
+ finally:
38
+ self._handler = None
39
+
40
+ def consume_docker_logs(self, container) -> None:
41
+ handler = check.not_none(
42
+ self._handler, "Can only consume logs within context manager scope."
43
+ )
44
+ for log_line in container.logs(stdout=True, stderr=True, stream=True, follow=True):
45
+ if isinstance(log_line, bytes):
46
+ log_entry = log_line.decode("utf-8")
47
+ elif isinstance(log_line, str):
48
+ log_entry = log_line
49
+ else:
50
+ continue
51
+
52
+ extract_message_or_forward_to_stdout(handler, log_entry)
53
+
54
+ def no_messages_debug_text(self) -> str:
55
+ return "Attempted to read messages by extracting them from docker logs directly."
56
+
57
+
58
+ class PipesDockerClient(PipesClient, TreatAsResourceParam):
59
+ """A pipes client that runs external processes in docker containers.
60
+
61
+ By default context is injected via environment variables and messages are parsed out of the
62
+ log stream, with other logs forwarded to stdout of the orchestration process.
63
+
64
+ Args:
65
+ env (Optional[Mapping[str, str]]): An optional dict of environment variables to pass to the
66
+ container.
67
+ register (Optional[Mapping[str, str]]): An optional dict of registry credentials to login to
68
+ the docker client.
69
+ context_injector (Optional[PipesContextInjector]): A context injector to use to inject
70
+ context into the docker container process. Defaults to :py:class:`PipesEnvContextInjector`.
71
+ message_reader (Optional[PipesMessageReader]): A message reader to use to read messages
72
+ from the docker container process. Defaults to :py:class:`DockerLogsMessageReader`.
73
+ """
74
+
75
+ def __init__(
76
+ self,
77
+ env: Optional[Mapping[str, str]] = None,
78
+ registry: Optional[Mapping[str, str]] = None,
79
+ context_injector: Optional[PipesContextInjector] = None,
80
+ message_reader: Optional[PipesMessageReader] = None,
81
+ ):
82
+ self.env = check.opt_mapping_param(env, "env", key_type=str, value_type=str)
83
+ self.registry = check.opt_mapping_param(registry, "registry", key_type=str, value_type=str)
84
+ self.context_injector = (
85
+ check.opt_inst_param(
86
+ context_injector,
87
+ "context_injector",
88
+ PipesContextInjector,
89
+ )
90
+ or PipesEnvContextInjector()
91
+ )
92
+
93
+ self.message_reader = (
94
+ check.opt_inst_param(message_reader, "message_reader", PipesMessageReader)
95
+ or PipesDockerLogsMessageReader()
96
+ )
97
+
98
+ @classmethod
99
+ def _is_dagster_maintained(cls) -> bool:
100
+ return True
101
+
102
+ def run( # pyright: ignore[reportIncompatibleMethodOverride]
103
+ self,
104
+ *,
105
+ context: Union[OpExecutionContext, AssetExecutionContext],
106
+ image: str,
107
+ extras: Optional[PipesExtras] = None,
108
+ command: Optional[Union[str, Sequence[str]]] = None,
109
+ env: Optional[Mapping[str, str]] = None,
110
+ registry: Optional[Mapping[str, str]] = None,
111
+ container_kwargs: Optional[Mapping[str, Any]] = None,
112
+ ) -> PipesClientCompletedInvocation:
113
+ """Create a docker container and run it to completion, enriched with the pipes protocol.
114
+
115
+ Args:
116
+ image (str):
117
+ The image for the container to use.
118
+ command (Optional[Union[str, Sequence[str]]]):
119
+ The command for the container use.
120
+ env (Optional[Mapping[str,str]]):
121
+ A mapping of environment variable names to values to set on the first
122
+ container in the pod spec, on top of those configured on resource.
123
+ registry (Optional[Mapping[str, str]]:
124
+ A mapping containing url, username, and password to be used
125
+ with docker client login.
126
+ container_kwargs (Optional[Mapping[str, Any]]:
127
+ Arguments to be forwarded to docker client containers.create.
128
+ extras (Optional[PipesExtras]):
129
+ Extra values to pass along as part of the ext protocol.
130
+ context_injector (Optional[PipesContextInjector]):
131
+ Override the default ext protocol context injection.
132
+ message_reader (Optional[PipesMessageReader]):
133
+ Override the default ext protocol message reader.
134
+
135
+ Returns:
136
+ PipesClientCompletedInvocation: Wrapper containing results reported by the external
137
+ process.
138
+ """
139
+ with open_pipes_session(
140
+ context=context,
141
+ context_injector=self.context_injector,
142
+ message_reader=self.message_reader,
143
+ extras=extras,
144
+ ) as pipes_session:
145
+ client = docker.client.from_env()
146
+ registry = registry or self.registry
147
+ if registry:
148
+ client.login(
149
+ registry=registry["url"],
150
+ username=registry["username"],
151
+ password=registry["password"],
152
+ )
153
+
154
+ try:
155
+ container = self._create_container(
156
+ client=client,
157
+ image=image,
158
+ command=command,
159
+ env=env,
160
+ open_pipes_session_env=pipes_session.get_bootstrap_env_vars(),
161
+ container_kwargs=container_kwargs,
162
+ )
163
+ except docker.errors.ImageNotFound:
164
+ client.images.pull(image)
165
+ container = self._create_container(
166
+ client=client,
167
+ image=image,
168
+ command=command,
169
+ env=env,
170
+ open_pipes_session_env=pipes_session.get_bootstrap_env_vars(),
171
+ container_kwargs=container_kwargs,
172
+ )
173
+
174
+ result = container.start()
175
+ try:
176
+ if isinstance(self.message_reader, PipesDockerLogsMessageReader):
177
+ self.message_reader.consume_docker_logs(container)
178
+
179
+ result = container.wait()
180
+ if result["StatusCode"] != 0:
181
+ raise DagsterPipesError(f"Container exited with non-zero status code: {result}")
182
+ finally:
183
+ container.stop()
184
+ return PipesClientCompletedInvocation(pipes_session)
185
+
186
+ def _create_container(
187
+ self,
188
+ client,
189
+ image: str,
190
+ command: Optional[Union[str, Sequence[str]]],
191
+ env: Optional[Mapping[str, str]],
192
+ container_kwargs: Optional[Mapping[str, Any]],
193
+ open_pipes_session_env: Mapping[str, str],
194
+ ):
195
+ kwargs = dict(container_kwargs or {})
196
+ kwargs_env = kwargs.pop("environment", {})
197
+ return client.containers.create(
198
+ image=image,
199
+ command=command,
200
+ detach=True,
201
+ environment={
202
+ **open_pipes_session_env,
203
+ **(self.env or {}),
204
+ **(env or {}),
205
+ **kwargs_env,
206
+ },
207
+ **kwargs,
208
+ )
@@ -0,0 +1 @@
1
+ partial
dagster_docker/utils.py CHANGED
@@ -6,7 +6,7 @@ from dagster import (
6
6
  from dagster._utils.merger import merge_dicts
7
7
  from docker_image import reference
8
8
 
9
- from .container_context import DOCKER_CONTAINER_CONTEXT_SCHEMA
9
+ from dagster_docker.container_context import DOCKER_CONTAINER_CONTEXT_SCHEMA
10
10
 
11
11
  DOCKER_CONFIG_SCHEMA = merge_dicts(
12
12
  {
@@ -55,8 +55,4 @@ def validate_docker_image(docker_image):
55
55
  # validate that the docker image name is valid
56
56
  reference.Reference.parse(docker_image)
57
57
  except Exception as e:
58
- raise Exception(
59
- "Docker image name {docker_image} is not correctly formatted".format(
60
- docker_image=docker_image
61
- )
62
- ) from e
58
+ raise Exception(f"Docker image name {docker_image} is not correctly formatted") from e
dagster_docker/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.17.19"
1
+ __version__ = "0.28.5"
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: dagster-docker
3
+ Version: 0.28.5
4
+ Summary: A Dagster integration for docker
5
+ Home-page: https://github.com/dagster-io/dagster/tree/master/python_modules/libraries/dagster-docker
6
+ Author: Dagster Labs
7
+ Author-email: hello@dagsterlabs.com
8
+ License: Apache-2.0
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.10,<3.14
16
+ License-File: LICENSE
17
+ Requires-Dist: dagster==1.12.5
18
+ Requires-Dist: docker
19
+ Requires-Dist: docker-image-py
20
+ Provides-Extra: test
21
+ Requires-Dist: flaky; extra == "test"
22
+ Requires-Dist: botocore>=1.21.49; extra == "test"
23
+ Dynamic: author
24
+ Dynamic: author-email
25
+ Dynamic: classifier
26
+ Dynamic: home-page
27
+ Dynamic: license
28
+ Dynamic: license-file
29
+ Dynamic: provides-extra
30
+ Dynamic: requires-dist
31
+ Dynamic: requires-python
32
+ Dynamic: summary
@@ -0,0 +1,15 @@
1
+ dagster_docker/__init__.py,sha256=rnJ8jarKzaUQl3kIXVFR9h2kNinX0Enjx_mIROBgHdY,624
2
+ dagster_docker/container_context.py,sha256=pbM6btkjXppI5mXaU-2YhRSUTkCQfhzvPHz72WzM3Ec,6485
3
+ dagster_docker/docker_executor.py,sha256=i9wszCPICeduvseI4OPfYOob8NShlBnG0ZLlD0p8EKY,12826
4
+ dagster_docker/docker_run_launcher.py,sha256=3rGXZ1wp9gbPyn1iP5XLiTxF-JzazCVpt50qvvfTSm8,8458
5
+ dagster_docker/pipes.py,sha256=Ibp5ZqYmISxHZBaOwVwed2phCZaRfimmDSG87lVrRj0,8242
6
+ dagster_docker/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
7
+ dagster_docker/utils.py,sha256=oo3JziSJL0CrHMtvQL9KA3OUEWTBEHA1PdY4sqotZy0,1821
8
+ dagster_docker/version.py,sha256=SdGElACDuKJX1c_iqPcfu_quaKOShee-DePM8qfbbJU,23
9
+ dagster_docker/ops/__init__.py,sha256=-86lzmRMHY6zwn33K7hP1_CI0Z14c4-8urBKl7tlIA4,161
10
+ dagster_docker/ops/docker_container_op.py,sha256=zzhdaKlAhBWn1bAHWpy-9ym6evxK1eZniSfACuR7E_E,6434
11
+ dagster_docker-0.28.5.dist-info/licenses/LICENSE,sha256=4lsMW-RCvfVD4_F57wrmpe3vX1xwUk_OAKKmV_XT7Z0,11348
12
+ dagster_docker-0.28.5.dist-info/METADATA,sha256=ZhrMexywE8Dz17ILRMTY-fzxDhrlr1MRYm-5yfqBA8c,1043
13
+ dagster_docker-0.28.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ dagster_docker-0.28.5.dist-info/top_level.txt,sha256=FvMtaf9uYIb-jIVJgoiNi1tJ31JZmvh55GxzszLahKs,15
15
+ dagster_docker-0.28.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.33.6)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright 2023 Elementl, Inc.
189
+ Copyright 2025 Dagster Labs, Inc.
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
@@ -1,22 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: dagster-docker
3
- Version: 0.17.19
4
- Summary: A Dagster integration for docker
5
- Home-page: https://github.com/dagster-io/dagster/tree/master/python_modules/libraries/dagster-docker
6
- Author: Elementl
7
- Author-email: hello@elementl.com
8
- License: Apache-2.0
9
- Platform: UNKNOWN
10
- Classifier: Programming Language :: Python :: 3.7
11
- Classifier: Programming Language :: Python :: 3.8
12
- Classifier: Programming Language :: Python :: 3.9
13
- Classifier: Programming Language :: Python :: 3.10
14
- Classifier: License :: OSI Approved :: Apache Software License
15
- Classifier: Operating System :: OS Independent
16
- License-File: LICENSE
17
- Requires-Dist: dagster (==1.1.19)
18
- Requires-Dist: docker
19
- Requires-Dist: docker-image-py
20
-
21
- UNKNOWN
22
-
@@ -1,13 +0,0 @@
1
- dagster_docker/__init__.py,sha256=FVXqcMHKY8GXFaqIl5aTpX5fHHTsRu4dGJ6EYkusfdM,421
2
- dagster_docker/container_context.py,sha256=ytE70Wj2emHOopviwZ6aE7bkc3sywCvJTo4T9WVJVmo,6484
3
- dagster_docker/docker_executor.py,sha256=gMbZLVOGJvMuWIED7Lmhf4iT5JivW2fxtRX79B24Eqw,11815
4
- dagster_docker/docker_run_launcher.py,sha256=S5_EFHOOo4q82BipDN1mRHX0GJa1H3wucLz44wqnrdo,7220
5
- dagster_docker/utils.py,sha256=pviR2koL8w_UQq14UY2wquT1zcy5VDEg9sOteybURsY,1892
6
- dagster_docker/version.py,sha256=LKtJnPhS5GRuhOh9obHGnDlncpF9oAEamdbIKdiL3Tw,24
7
- dagster_docker/ops/__init__.py,sha256=ZGKgOE1FC-vGfR4bk2kg4FXSg4LVsV2PzIK_XyAUqOk,143
8
- dagster_docker/ops/docker_container_op.py,sha256=jJFwgFnbNlCyq2g6AiUI41Rd_0WSMhhtyBQeez9Ay1k,6362
9
- dagster_docker-0.17.19.dist-info/LICENSE,sha256=-gtoVIAZYUHYmNHISZg982FI4Oh19mV1nxgTVW8eCB8,11344
10
- dagster_docker-0.17.19.dist-info/METADATA,sha256=FHEWIB02zH3sDvYolvv0XWAM3YdHwhuWjY6f-ipc7fk,721
11
- dagster_docker-0.17.19.dist-info/WHEEL,sha256=p46_5Uhzqz6AzeSosiOnxK-zmFja1i22CrQCjmYe8ec,92
12
- dagster_docker-0.17.19.dist-info/top_level.txt,sha256=FvMtaf9uYIb-jIVJgoiNi1tJ31JZmvh55GxzszLahKs,15
13
- dagster_docker-0.17.19.dist-info/RECORD,,