1from __future__ import annotations 2 3import logging 4import time 5import traceback 6from collections.abc import Iterator, Mapping, Sequence 7from contextlib import contextmanager 8from contextvars import ContextVar 9from dataclasses import dataclass, field 10from importlib import metadata 11from tempfile import TemporaryDirectory 12from threading import RLock 13from typing import Any, Optional, Protocol, Union, cast, get_args, overload 14 15from daggerml._core import CancellationError, Dml, DmlRepoError, Error, Ref, Runnable, Uri 16from daggerml.util import BackoffWithJitter, current_time_millis 17 18logger = logging.getLogger(__name__) 19 20Scalar = Union[str, int, float, bool, type(None), Uri, Runnable] 21Collection = Union[list, dict] 22ProjectionStep = Union[str, int, list[int]] 23 24_NO_DEFAULT_DML = object() 25_SCOPED_DEFAULT_DML: ContextVar[object] = ContextVar("daggerml_scoped_default_dml", default=_NO_DEFAULT_DML) 26_PROCESS_DEFAULT_DML: Optional["Dml"] = None 27 28 29def _resolve_default_dml(*, create: bool = True) -> tuple["Dml", str]: 30 scoped = _SCOPED_DEFAULT_DML.get() 31 if scoped is not _NO_DEFAULT_DML: 32 return cast("Dml", scoped), "scoped" 33 34 global _PROCESS_DEFAULT_DML 35 if _PROCESS_DEFAULT_DML is not None: 36 return _PROCESS_DEFAULT_DML, "process" 37 38 if not create: 39 raise DmlRepoError("No default Dml is configured") 40 41 _PROCESS_DEFAULT_DML = Dml() 42 return _PROCESS_DEFAULT_DML, "implicit" 43 44 45def get_default_dml() -> "Dml": 46 """Return the active default Dml runtime.""" 47 dml, _source = _resolve_default_dml(create=True) 48 return dml 49 50 51def set_default_dml(dml: "Dml") -> None: 52 """Set the process-default Dml runtime.""" 53 global _PROCESS_DEFAULT_DML 54 _PROCESS_DEFAULT_DML = dml 55 56 57def clear_default_dml() -> None: 58 """Clear the process-default Dml runtime.""" 59 global _PROCESS_DEFAULT_DML 60 _PROCESS_DEFAULT_DML = None 61 62 63@contextmanager 64def use_default_dml(dml: "Dml"): 65 """Temporarily override the default Dml runtime for the active context.""" 66 token = _SCOPED_DEFAULT_DML.set(dml) 67 try: 68 yield dml 69 finally: 70 _SCOPED_DEFAULT_DML.reset(token) 71 72 73def new( 74 name="", 75 message="", 76 cache_key: str | None = None, 77 execution_id: str | None = None, 78 tags: list[str] | None = None, 79 dml: Dml | None = None, 80) -> "Dag": 81 """Create a new DAG using the active or provided Dml runtime.""" 82 runtime = dml or get_default_dml() 83 execution = Ref(f"index:{execution_id}") if execution_id is not None else None 84 create_kwargs = {"cache_key": cache_key, "execution": execution} 85 if tags is not None: 86 create_kwargs["tags"] = tags 87 index_id = runtime.runtime.create(**create_kwargs) 88 return Dag(dml=runtime, token=index_id, name=name, message=message) 89 90 91def load( 92 name: str, 93 dml: Dml | None = None, 94 *, 95 revision: Ref | str = "HEAD", 96 remote: bool = False, 97 dep: str | None = None, 98) -> "Dag": 99 """Load a DAG using the active default Dml runtime.""" 100 dml = dml or get_default_dml() 101 dag_ref = dml.show(revision, remote=remote, dep=dep)["dags"].get(name) 102 if dag_ref is None: 103 raise DmlRepoError(f"DAG not found: {name}") 104 return Dag(dml=dml, ref=dag_ref, name=name) 105 106 107def resume( 108 frozen: Ref, 109 *, 110 name: str, 111 message: str, 112 dml: Dml | None = None, 113) -> "Dag": 114 """Resume a frozen DAG runtime with explicitly supplied commit metadata.""" 115 runtime = dml or get_default_dml() 116 index_id = runtime.runtime.unfreeze(frozen) 117 return Dag(dml=runtime, token=index_id, name=name, message=message) 118 119 120@contextmanager 121def temporary(prefix="dml-tmp-", **kw): 122 """Create a temporary Dml runtime with an unborn attached HEAD.""" 123 with TemporaryDirectory(prefix=prefix) as tmpdir: 124 yield Dml.init(project_home=tmpdir, **kw) 125 126 127def status() -> dict[str, object]: 128 """Return status for the active default Dml runtime.""" 129 dml, source = _resolve_default_dml(create=True) 130 return { 131 "default": { 132 "source": source, 133 "has_scoped_override": _SCOPED_DEFAULT_DML.get() is not _NO_DEFAULT_DML, 134 "has_process_default": _PROCESS_DEFAULT_DML is not None, 135 }, 136 "status": dml.status(), 137 } 138 139 140def _make_node(dag: "Dag", ref: Ref) -> "Node": 141 """ 142 Create a Node from a Dag and Ref. 143 144 Parameters 145 ---------- 146 dag : Dag 147 The parent DAG. 148 ref : Ref 149 The reference to the node. 150 Returns 151 ------- 152 Node 153 A Node instance representing the reference in the DAG. 154 """ 155 node_value = dag.dml.dag.get_node(ref) 156 if isinstance(node_value, Error): 157 raise NodeError(node_value, node_ref=ref, dag=dag) 158 info: dict[str, Any] = {"data_type": type(node_value).__name__.lower()} 159 # Determine node type based on value and populate info 160 if isinstance(node_value, list): 161 info["length"] = len(node_value) 162 node = ListNode(dag, ref, _info=info) 163 elif isinstance(node_value, dict): 164 info["length"] = len(node_value) 165 info["keys"] = sorted(node_value.keys()) 166 node = DictNode(dag, ref, _info=info) 167 elif isinstance(node_value, Runnable): 168 node = RunnableNode(dag, ref, _info=info) 169 else: 170 node = ScalarNode(dag, ref, _info=info) 171 return node 172 173 174def _info_for_value(value: Any) -> dict[str, Any]: 175 info: dict[str, Any] = {"data_type": type(value).__name__.lower()} 176 if isinstance(value, list): 177 info["length"] = len(value) 178 elif isinstance(value, dict): 179 info["length"] = len(value) 180 info["keys"] = sorted(value.keys()) 181 return info 182 183 184def _normalize_projection_step(key: ProjectionStep | slice, *, length: int | None = None) -> ProjectionStep: 185 if isinstance(key, slice): 186 if key.step is not None: 187 raise ValueError("Slice step is not supported") 188 start = key.start if key.start is not None else 0 189 if key.stop is None: 190 if length is None: 191 raise DmlRepoError("Slice stop requires known collection length") 192 stop = length 193 else: 194 stop = key.stop 195 return [start, stop] 196 return key 197 198 199def _apply_projection_step(value: Any, step: ProjectionStep) -> Any: 200 if isinstance(step, list): 201 if len(step) != 2: 202 raise DmlRepoError("Slice projection requires exactly [start, stop]") 203 return value[slice(*step)] 204 return value[step] 205 206 207def _apply_projection_path(value: Any, path: tuple[ProjectionStep, ...]) -> Any: 208 for step in path: 209 value = _apply_projection_step(value, step) 210 return value 211 212 213def _describe_node(node: "Node") -> Mapping[str, Any]: 214 return node.dag.dml.dag.describe_node(node.ref) 215 216 217def _builtin_name_for_argv(dag: "Dag", argv_refs: list[Ref]) -> str | None: 218 if not argv_refs: 219 return None 220 runnable = dag.dml.dag.get_node(argv_refs[0], recursive=True) 221 if not isinstance(runnable, Runnable): 222 return None 223 if runnable.adapter != "": 224 return None 225 if not runnable.target.uri.startswith("daggerml:"): 226 return None 227 return runnable.target.uri.split(":", 1)[1] 228 229 230def _prepend_get_path_step(path: tuple[ProjectionStep, ...], key: ProjectionStep) -> tuple[ProjectionStep, ...] | None: 231 if isinstance(key, list): 232 if len(key) != 2: 233 return None 234 if not path: 235 return None 236 first, *rest = path 237 if not isinstance(first, int): 238 return None 239 return (key[0] + first, *rest) 240 return (key, *path) 241 242 243def _backtrack_builtin( 244 node: "Node", argv_refs: list[Ref], path: tuple[ProjectionStep, ...] 245) -> tuple["Node", tuple[ProjectionStep, ...]] | None: 246 builtin = _builtin_name_for_argv(node.dag, argv_refs) 247 if builtin is None: 248 return None 249 arg_nodes = [_make_node(node.dag, ref) for ref in argv_refs[1:]] 250 if builtin == "get": 251 if len(arg_nodes) < 2: 252 return None 253 key = cast(ProjectionStep, arg_nodes[1].value()) 254 next_path = _prepend_get_path_step(path, key) 255 if next_path is None: 256 return None 257 return arg_nodes[0], next_path 258 if builtin == "list": 259 if not path: 260 return None 261 index, *rest = path 262 if not isinstance(index, int): 263 return None 264 if index < 0: 265 index += len(arg_nodes) 266 if index < 0 or index >= len(arg_nodes): 267 return None 268 return arg_nodes[index], tuple(rest) 269 if builtin == "dict": 270 if not path: 271 return None 272 key, *rest = path 273 if not isinstance(key, str): 274 return None 275 for idx in range(0, len(arg_nodes), 2): 276 if idx + 1 >= len(arg_nodes): 277 break 278 if arg_nodes[idx].value() == key: 279 return arg_nodes[idx + 1], tuple(rest) 280 return None 281 if builtin == "assoc": 282 if len(arg_nodes) < 3 or not path: 283 return None 284 selected_key, *rest = path 285 assoc_key = arg_nodes[1].value() 286 if selected_key == assoc_key: 287 return arg_nodes[2], tuple(rest) 288 return arg_nodes[0], path 289 if builtin == "conj": 290 if len(arg_nodes) < 2 or not path: 291 return None 292 index, *rest = path 293 if not isinstance(index, int): 294 return None 295 base_len = len(arg_nodes[0].value()) 296 if index < 0: 297 index += base_len + 1 298 if index == base_len: 299 return arg_nodes[1], tuple(rest) 300 return arg_nodes[0], path 301 return None 302 303 304def _nearest_context_state( 305 node: "Node", path: tuple[ProjectionStep, ...] 306) -> tuple[Dag, Node, tuple[ProjectionStep, ...], bool]: 307 current = node 308 current_path = path 309 while True: 310 node_info = _describe_node(current) 311 node_type = node_info["type"] 312 if node_type == "ImportNode": 313 source_dag = Dag(dml=current.dag.dml, ref=cast(Ref, node_info["dag"])) 314 source_node = _make_node(source_dag, cast(Ref, node_info["node"])) 315 if current_path: 316 current = source_node 317 continue 318 return source_dag, source_node, current_path, True 319 if node_type == "FnNode": 320 argv_refs = cast(list[Ref], node_info["argv"]) 321 source = _backtrack_builtin(current, argv_refs, current_path) 322 if source is not None: 323 current, current_path = source 324 continue 325 fn_dag = Dag(dml=current.dag.dml, ref=cast(Ref, node_info["dag"])) 326 return fn_dag, fn_dag.result, current_path, True 327 return current.dag, current, current_path, False 328 329 330def _resolve_context(node: "Node", path: tuple[ProjectionStep, ...], *, root: bool) -> Dag: 331 context_dag, next_node, next_path, can_recurse = _nearest_context_state(node, path) 332 if not root: 333 return context_dag 334 if not can_recurse: 335 return context_dag 336 return _resolve_context(next_node, next_path, root=True) 337 338 339@dataclass 340class Dag: 341 dml: Dml 342 token: Optional[Ref] = None # Working index id 343 ref: Optional[Ref] = None 344 name: str = "" # DAG name for commit 345 message: str = "" # Commit message 346 347 def __repr__(self): 348 to = self.ref.to if self.ref else (self.token.to if self.token is not None else "NA") 349 return f"Dag({to})" 350 351 def __hash__(self): 352 "Useful only for tests." 353 return 42 354 355 def __eq__(self, other): 356 "DAG equality is based on identity, not content." 357 if not isinstance(other, Dag): 358 return False 359 return self.ref == other.ref and self.token == other.token and self.dml == other.dml 360 361 def __enter__(self): 362 "Catch exceptions and commit an Error" 363 assert not self.ref 364 return self 365 366 def __exit__(self, exc_type, exc_value, tb): 367 if exc_value is not None: 368 # Convert exception to Error and commit it 369 traceback.print_exception(exc_type, exc_value, tb) 370 err = Error.from_ex(exc_value) if not isinstance(exc_value, Error) else exc_value 371 self.commit(err) 372 373 def _require_index_ref(self) -> Ref: 374 if self.token is None: 375 raise DmlRepoError("No active index") 376 return self.token 377 378 def _read_dag_ref(self) -> Ref: 379 """Return the completed or partial DAG backing this wrapper's reads.""" 380 if self.ref is not None: 381 return self.ref 382 return cast(Ref, self.dml.runtime.describe(self._require_index_ref())["dag"]) 383 384 @property 385 def tags(self) -> list[str]: 386 """Return the normalized tags stored on this DAG.""" 387 return list(self.dml.dag.describe(self._read_dag_ref())["tags"]) 388 389 def _put_literal(self, value: Any, *, name: Optional[str] = None) -> Ref: 390 index_id = self._require_index_ref() 391 value = apply_codecs(value, dag=self) 392 return self.dml.runtime.put_literal(index_id, value, name=name) 393 394 def _start_fn(self, argv: list[Ref], *, name: Optional[str] = None) -> Optional[Ref]: 395 return self.dml.runtime.start_fn(self._require_index_ref(), argv, name=name) 396 397 def _call_builtin(self, uri: str, *args: Any, name: Optional[str] = None) -> Ref: 398 fn_ref = self._put_literal(Runnable(target=Uri(uri), kwargs={}, adapter="")) 399 argv: list[Ref] = [fn_ref] 400 for arg in args: 401 argv.append(arg if isinstance(arg, Ref) else self._put_literal(arg)) 402 result = self._start_fn(argv, name=name) 403 if result is None: 404 raise DmlRepoError("Function execution failed") 405 return result 406 407 def __len__(self) -> int: 408 return len(self.keys()) 409 410 def __iter__(self): 411 yield from self.keys() 412 413 def _get_named_node(self, name: str) -> "Node": 414 node_ref = self.dml.dag.describe(self._read_dag_ref())["names"].get(name) 415 if node_ref is None: 416 raise DmlRepoError(f"Node '{name}' not found in DAG") 417 return _make_node(self, node_ref) 418 419 def _set_named_node(self, name: str, value: Any) -> None: 420 if self.ref is not None: 421 raise DmlRepoError("Cannot set node names on a committed DAG.") 422 if isinstance(value, Node): 423 value = value.ref 424 if isinstance(value, Ref): 425 self.dml.runtime.set_node_name(self._require_index_ref(), name, value) 426 return 427 self.put(value, name=name) 428 429 def __getitem__(self, name: str) -> "Node": 430 return self._get_named_node(name) 431 432 def __setitem__(self, name: str, value: Any) -> None: 433 self.put(value, name=name) 434 435 def __getattr__(self, name: str) -> "Node": 436 if name.startswith("_"): 437 raise AttributeError(name) 438 return self[name] 439 440 def __setattr__(self, name: str, value: Any) -> None: 441 dataclass_fields = getattr(type(self), "__dataclass_fields__", {}) 442 if name in dataclass_fields or hasattr(type(self), name): 443 object.__setattr__(self, name, value) 444 return 445 self[name] = value 446 447 def keys(self) -> list[str]: 448 """Get the list of all node names in the dag""" 449 names_dict = self.dml.dag.describe(self._read_dag_ref())["names"] 450 return sorted(names_dict.keys()) 451 452 def values(self) -> list["Node"]: 453 """Get the list of all nodes in the dag""" 454 names_dict = self.dml.dag.describe(self._read_dag_ref())["names"] 455 return [_make_node(self, ref) for ref in names_dict.values()] 456 457 @property 458 def argv(self) -> "ListNode": 459 "Access the dag's argv node" 460 argv_ref = self.dml.dag.describe(self._read_dag_ref())["argv"] 461 if not isinstance(argv_ref, Ref): 462 raise DmlRepoError(f"'{self.__class__.__name__}' dag has no argv") 463 return cast(ListNode, _make_node(self, argv_ref)) 464 465 @property 466 def result(self) -> "Node": 467 """Get the result node of the dag""" 468 if self.ref is None: 469 raise DmlRepoError("Cannot access result of an uncommitted DAG") 470 description = self.dml.dag.describe(self.ref) 471 error_ref = description.get("error") 472 if isinstance(error_ref, Ref): 473 raise self.dml.dag.get_error(error_ref) 474 ref = description.get("result") 475 if not isinstance(ref, Ref): 476 raise DmlRepoError(f"'{self.__class__.__name__}' dag has not been committed yet") 477 return _make_node(self, ref) 478 479 @overload 480 def put(self, value: Union[list, "ListNode"], *, name=None) -> "ListNode": ... 481 @overload 482 def put(self, value: Union[dict, "DictNode"], *, name=None) -> "DictNode": ... 483 @overload 484 def put(self, value: Union[Runnable, "RunnableNode"], *, name=None) -> "RunnableNode": ... 485 @overload 486 def put(self, value: Union[Scalar, "ScalarNode"], *, name=None) -> "ScalarNode": ... 487 @overload 488 def put(self, value: Any, *, name=None) -> "Node": ... 489 def put(self, value: Any, *, name=None) -> "Node": 490 """ 491 Add a value to the DAG. 492 493 Parameters 494 ---------- 495 value : Union[Scalar, Collection] 496 Value to add 497 name : str, optional 498 Name for the node 499 Returns 500 ------- 501 Node 502 Node representing the value 503 504 Examples 505 -------- 506 >>> n1 = dag.put(42, name="answer") 507 >>> n1.value() 508 42 509 >>> n2 = dag.put({"a": 1, "b": [n1, "23"]}) 510 >>> n2.value() 511 {'a': 1, 'b': [42, '23']} 512 >>> n3 = dag.put({"a": 1, "b": [n1, "23"]}) 513 >>> n3.value() 514 {'a': 1, 'b': [42, '23']} 515 """ 516 return _make_node(self, self._put_literal(value, name=name)) 517 518 def require(self, dag_name: str | "Dag", node_name: str | None = None, *, name: str | None = None) -> "Node": 519 """ 520 Import a node from a different (committed) DAG into the current DAG. 521 522 Parameters 523 ---------- 524 dag_name : str 525 Name of the DAG to import from 526 node_name : str, optional 527 Name of the node to import. If None, imports the result node of the DAG. 528 529 Returns 530 ------- 531 Node 532 The loaded node or DAG 533 534 Examples 535 -------- 536 >>> dag = new(dml=dml, name="test", message="test") 537 >>> n1 = dag.put(42, name="answer") 538 >>> n2 = dag.put({"a": 1, "b": [n1, "23"]}, name="data") 539 >>> dag.commit(n2) 540 >>> dag2 = new(dml=dml, name="test2", message="test2") 541 >>> imported_n2 = dag2.require("test", "data", name="imported_data") 542 >>> imported_n2.value() 543 {'a': 1, 'b': [42, '23']} 544 """ 545 index = self._require_index_ref() 546 if isinstance(dag_name, Dag): 547 if dag_name.ref is None: 548 raise DmlRepoError("Cannot import an uncommitted DAG") 549 dag_ref = dag_name.ref 550 else: 551 commit = self.dml.runtime.describe(index)["parents"][0] 552 dag_ref = self.dml.show(revision=commit)["dags"].get(dag_name) 553 if dag_ref is None: 554 raise DmlRepoError(f"DAG not found: {dag_name}") 555 dag_info = self.dml.dag.describe(dag_ref) 556 node_ref = dag_info["names"].get(node_name) if node_name else dag_info.get("result") 557 if node_ref is None: 558 raise DmlRepoError(f"Node '{node_name}' not found in DAG '{dag_name}'") 559 node_ref = self.dml.runtime.put_import(index, dag_ref, node_ref, name=name) 560 return _make_node(self, node_ref) 561 562 def call( 563 self, 564 fn: Any, 565 *args: Any, 566 name: Optional[str] = None, 567 sleep: Optional[callable] = None, 568 timeout: int = -1, 569 ) -> "Node": 570 """ 571 Call a function node with arguments. 572 573 Parameters 574 ---------- 575 fn : Union[Runnable, RunnableNode] 576 Function to call 577 *args : Union[Node, Scalar, Collection] 578 Arguments to pass to the function 579 name : str, optional 580 Name for the result node 581 sleep : callable, optional 582 A nullary function that returns sleep time in milliseconds 583 timeout : int, default=-1 584 Maximum time to wait in milliseconds. If <= 0, wait indefinitely. 585 586 Returns 587 ------- 588 Node 589 Result node 590 591 Raises 592 ------ 593 TimeoutError 594 If the function call exceeds the timeout 595 Error 596 If the function returns an error 597 """ 598 sleep = sleep or BackoffWithJitter() 599 argv_seed = [fn, *args] 600 end = current_time_millis() + timeout 601 while timeout <= 0 or current_time_millis() < end: 602 argv_refs = [self._put_literal(value) for value in argv_seed] 603 resp = self._start_fn(argv_refs, name=name) 604 if resp: 605 return _make_node(self, resp) 606 time.sleep(sleep() / 1000) 607 raise TimeoutError(f"invoking function: {fn}") 608 609 def commit(self, value) -> None: 610 """ 611 Commit a value to the DAG. 612 613 Parameters 614 ---------- 615 value : Union[Node, Error, Any] 616 Value to commit 617 """ 618 # errors are committed as-is, everything else is a node 619 if not isinstance(value, (Error, Node)): 620 value = self.put(value) 621 if isinstance(value, Node): 622 value = value.ref 623 self.ref = self.dml.runtime.commit(self._require_index_ref(), value, message=self.message, name=self.name) 624 self.token = None # Clear the working index since it's now committed 625 626 def freeze(self, message: str | None = None) -> "Dag": 627 """Freeze this uncommitted DAG's runtime index for read-only inspection.""" 628 freeze_message = f"dag: {self.name}" 629 if message: 630 freeze_message = f"{freeze_message}\n{message}" 631 self.token = self.dml.runtime.freeze(self._require_index_ref(), message=freeze_message) 632 return self 633 634 def unfreeze(self) -> "Dag": 635 """Unfreeze this uncommitted DAG's runtime index.""" 636 self.token = self.dml.runtime.unfreeze(self._require_index_ref()) 637 return self 638 639 def cancel(self, max_retries: int = 3): 640 """Cancel the DAG's execution. 641 642 Parameters 643 ---------- 644 max_retries : int, default=3 645 Maximum cancellation retries after the initial attempt. 646 """ 647 if self.token is None: 648 raise DmlRepoError("Cannot cancel a committed DAG") 649 logger.info(f"Cancelling execution {self.token}") 650 self.dml.runtime.cancel(self.token, max_retries=max_retries) 651 self.token = None # Clear the index ref to indicate it's no longer active 652 raise CancellationError("DAG execution cancelled") 653 654 655class NodeError(Error): 656 """A stored error enriched with the node that produced it.""" 657 658 def __init__(self, error: Error, *, node_ref: Ref, dag: Dag): 659 super().__init__(message=error.message, origin=error.origin, type=error.type, stack=list(error.stack)) 660 self.node_ref = node_ref 661 self.dag = dag 662 663 def context(self) -> Dag: 664 """Return the function DAG that recorded this node's failure.""" 665 dag = self.dag 666 node_ref = self.node_ref 667 while True: 668 info = dag.dml.dag.describe_node(node_ref) 669 if info["type"] == "FnNode": 670 return Dag(dml=dag.dml, ref=info["dag"]) 671 if info["type"] != "ImportNode": 672 raise DmlRepoError(f"Node {node_ref} does not resolve to a failed function call") 673 dag = Dag(dml=dag.dml, ref=info["dag"]) 674 node_ref = info["node"] 675 676 677@dataclass(frozen=True) 678class Node: # noqa: F811 679 """ 680 Representation of a node in a DaggerML DAG. 681 682 Parameters 683 ---------- 684 dag : Dag 685 Parent DAG 686 ref : Ref 687 Node reference 688 """ 689 690 dag: Dag 691 ref: Ref 692 _info: dict = field(default_factory=dict) 693 694 def __repr__(self): 695 ref_id = self.ref if isinstance(self.ref, Error) else self.ref.to 696 return f"{self.__class__.__name__}({ref_id})" 697 698 def __hash__(self): 699 return hash(self.ref) 700 701 def __eq__(self, other): 702 if not isinstance(other, Node): 703 return NotImplemented 704 return self.ref == other.ref 705 706 def context(self, *, root: bool = True) -> Dag: 707 """ 708 Resolve the provenance context (DAG) for this node. 709 710 This follows import/function provenance while treating builtin 711 collection-construction and selection DAGs as transparent. 712 713 Parameters 714 ---------- 715 root : bool, default=True 716 If False, return the nearest sub-DAG in which this value exists as a 717 proper node across a non-builtin import/function boundary. If True, 718 continue recursively until provenance no longer crosses a 719 non-builtin import/function boundary and return that first rooted 720 context. 721 722 Returns 723 ------- 724 Dag 725 The nearest or rooted provenance DAG for this node. 726 727 Examples 728 -------- 729 >>> source = new(dml=dml, name="source", message="source") 730 >>> answer = source.put(42, name="answer") 731 >>> payload = source.put({"answer": answer}, name="payload") 732 >>> source.commit(payload) 733 >>> consumer = new(dml=dml, name="consumer", message="consumer") 734 >>> imported = consumer.require("source", "payload", name="payload") 735 >>> consumer.commit(imported) 736 >>> loaded = load("consumer", dml=dml) 737 >>> loaded.result["answer"].context() == source 738 True 739 """ 740 return _resolve_context(self, (), root=root) 741 742 @property 743 def type(self): 744 """Get the data type of the node.""" 745 return self._info["data_type"] 746 747 @overload 748 def value(self: "ScalarNode") -> Scalar: ... 749 @overload 750 def value(self: "ListNode") -> list: ... 751 @overload 752 def value(self: "DictNode") -> dict: ... 753 @overload 754 def value(self: "RunnableNode") -> Runnable: ... 755 @overload 756 def value(self: "Node") -> Any: ... 757 def value(self): 758 """ 759 Get the concrete value of this node. 760 761 Returns 762 ------- 763 Any 764 The actual value represented by this node 765 """ 766 value = self.dag.dml.dag.get_node(self.ref, recursive=True) 767 if isinstance(value, Error): 768 raise NodeError(value, node_ref=self.ref, dag=self.dag) 769 return value 770 771 def __call__(self, *args, name=None, sleep=None, timeout=-1, **kw) -> "Node": 772 raise TypeError(f"Node of type '{self.type}' is not callable") 773 774 775class ScalarNode(Node): 776 pass 777 778 779class RunnableNode(Node): 780 def __call__(self, *args, name=None, sleep=None, timeout=-1) -> "Node": 781 """ 782 Call this node as a function. 783 784 Parameters 785 ---------- 786 *args : Any 787 Arguments to pass to the function 788 name : str, optional 789 Name for the result node 790 sleep : callable, optional 791 A nullary function that returns sleep time in milliseconds 792 timeout : int, default=-1 793 Maximum time to wait in milliseconds. -1 means wait forever. 794 795 Returns 796 ------- 797 Node 798 Result node 799 800 Raises 801 ------ 802 TimeoutError 803 If the function call exceeds the timeout 804 Error 805 If the function returns an error 806 """ 807 return self.dag.call(self, *args, name=name, sleep=sleep, timeout=timeout) 808 809 810@dataclass(frozen=True) 811class Projection: 812 dag: Dag 813 base: Node 814 path: tuple[ProjectionStep, ...] 815 _info: dict = field(default_factory=dict) 816 817 def __repr__(self): 818 return f"Projection({self.base!r}, path={self.path!r})" 819 820 @classmethod 821 def from_step(cls, base: Node, step: ProjectionStep) -> "Projection": 822 return cls(dag=base.dag, base=base, path=(step,)) 823 824 def _extend(self, step: ProjectionStep) -> "Projection": 825 return Projection(dag=self.dag, base=self.base, path=(*self.path, step)) 826 827 def value(self): 828 return _apply_projection_path(self.base.value(), self.path) 829 830 def context(self, *, root: bool = True) -> Dag: 831 return _resolve_context(self.base, self.path, root=root) 832 833 def __call__(self, *args, **kwargs): 834 raise TypeError(f"Projection of type '{self.type}' is not callable") 835 836 @property 837 def type(self): 838 if "data_type" in self._info: 839 return self._info["data_type"] 840 return _info_for_value(self.value())["data_type"] 841 842 def __len__(self): 843 if "length" in self._info: 844 return self._info["length"] 845 value = self.value() 846 if not isinstance(value, (list, dict)): 847 raise TypeError(f"Object of type '{type(value).__name__}' has no len()") 848 return len(value) 849 850 def __iter__(self): 851 value = self.value() 852 if isinstance(value, list): 853 for i in range(len(value)): 854 yield self[i] 855 return 856 if isinstance(value, dict): 857 yield from self.keys() 858 return 859 raise TypeError(f"Object of type '{type(value).__name__}' is not iterable") 860 861 def __getitem__(self, key: ProjectionStep | slice) -> "Projection": 862 value = self.value() 863 if isinstance(value, dict): 864 if not isinstance(key, str): 865 raise TypeError(f"Dict keys must be strings but got {type(key).__name__}") 866 step = cast(ProjectionStep, key) 867 elif isinstance(value, list): 868 if not isinstance(key, (int, slice)): 869 raise TypeError(f"List indices must be integers or slices but got {type(key).__name__}") 870 step = _normalize_projection_step(cast(ProjectionStep | slice, key), length=len(value)) 871 else: 872 raise TypeError(f"Cannot project into object of type '{type(value).__name__}'") 873 projected = self._extend(step) 874 return Projection(dag=self.dag, base=self.base, path=projected.path, _info=_info_for_value(projected.value())) 875 876 def keys(self) -> list[str]: 877 value = self.value() 878 if not isinstance(value, dict): 879 raise TypeError(f"Cannot get keys of type: {type(value).__name__}") 880 return sorted(value.keys()) 881 882 883class CollectionNode(Node): # noqa: F811 884 """ 885 Representation of a collection node in a DaggerML DAG. 886 887 Parameters 888 ---------- 889 dag : Dag 890 Parent DAG 891 ref : Ref 892 Node reference 893 """ 894 895 def contains(self, item, *, name=None) -> "ScalarNode": 896 """ 897 For collection nodes, checks to see if `item` is in `self` 898 899 Returns 900 ------- 901 Node 902 Node with the boolean of is `item` in `self` 903 """ 904 item_ref = item.ref if isinstance(item, Node) else item 905 result = self.dag._call_builtin("daggerml:contains", self.ref, item_ref, name=name) 906 return cast(ScalarNode, _make_node(self.dag, result)) 907 908 def __contains__(self, item): 909 return self.contains(item).value() # has to return boolean 910 911 def __len__(self): # python requires this to be an int 912 """ 913 Get the node's length 914 915 Returns 916 ------- 917 Node 918 Node with the length of the collection 919 920 Raises 921 ------ 922 Error 923 If the node isn't a collection (e.g. list or dict). 924 """ 925 return self._info["length"] 926 927 928class ListNode(CollectionNode): # noqa: F811 929 """ 930 Representation of a collection node in a DaggerML DAG. 931 932 Parameters 933 ---------- 934 dag : Dag 935 Parent DAG 936 ref : Ref 937 Node reference 938 """ 939 940 @overload 941 def __getitem__(self, key: Union[slice, list[int]]) -> Union["ListNode", Projection]: ... 942 @overload 943 def __getitem__(self, key: Union[int, "Node"]) -> Union["Node", Projection]: ... 944 def __getitem__(self, key: Union[slice, list[int], int, "Node"]) -> Union["Node", Projection]: 945 if self.dag.ref is not None: 946 if isinstance(key, Node): 947 raise TypeError("Committed list projections require concrete int or slice keys") 948 step = _normalize_projection_step(key, length=len(self)) 949 return Projection( 950 dag=self.dag, 951 base=self, 952 path=(step,), 953 _info=_info_for_value(_apply_projection_step(self.value(), step)), 954 ) 955 if isinstance(key, slice): 956 if key.step is not None: 957 raise ValueError("Slice step is not supported") 958 start = key.start if key.start is not None else 0 959 stop = key.stop if key.stop is not None else len(self) 960 key = [start, stop] 961 return _make_node(self.dag, self.dag._call_builtin("daggerml:get", self.ref, key)) 962 963 def __iter__(self): 964 """ 965 Iterate over the node's values (items if it's a list, and keys if it's a 966 dict) 967 968 Returns 969 ------- 970 Node 971 Result node 972 973 Raises 974 ------ 975 Error 976 If the node isn't a collection (e.g. list or dict). 977 """ 978 for i in range(len(self)): 979 yield self[i] 980 981 def conj(self, item, *, name=None) -> "ListNode": 982 """ 983 For a list node, append an item 984 985 Returns 986 ------- 987 Node 988 Node containing the new collection 989 990 Notes 991 ----- 992 `append` is an alias `conj` 993 """ 994 item_ref = item.ref if isinstance(item, Node) else item 995 resp = self.dag._call_builtin("daggerml:conj", self.ref, item_ref, name=name) 996 return cast(ListNode, _make_node(self.dag, resp)) 997 998 def append(self, item, *, name=None) -> "ListNode": 999 """ 1000 For a list node, append an item 1001 1002 Returns 1003 ------- 1004 Node 1005 Node containing the new collection 1006 1007 See Also 1008 -------- 1009 conj : The main implementation 1010 """ 1011 return self.conj(item, name=name) 1012 1013 1014class DictNode(CollectionNode): # noqa: F811 1015 def __getitem__(self, key: Union[str, "Node"]) -> Union["Node", Projection]: 1016 if self.dag.ref is not None: 1017 if not isinstance(key, str): 1018 raise TypeError(f"Dict keys must be strings but got {type(key).__name__}") 1019 return Projection(dag=self.dag, base=self, path=(key,), _info=_info_for_value(self.value()[key])) 1020 return _make_node(self.dag, self.dag._call_builtin("daggerml:get", self.ref, key)) 1021 1022 def keys(self) -> list[str]: 1023 """ 1024 Get the keys of a dictionary node. 1025 1026 Parameters 1027 ---------- 1028 name : str, optional 1029 Name for the result node 1030 1031 Returns 1032 ------- 1033 list[str] 1034 List of keys in the dictionary node 1035 """ 1036 return self._info["keys"].copy() 1037 1038 def __iter__(self): 1039 """ 1040 Iterate over the node's values (items if it's a list, and keys if it's a 1041 dict) 1042 1043 Returns 1044 ------- 1045 Node 1046 Result node 1047 1048 Raises 1049 ------ 1050 Error 1051 If the node isn't a collection (e.g. list or dict). 1052 """ 1053 for k in self.keys(): 1054 yield k 1055 1056 def get(self, key, default=None, *, name=None) -> "Node": 1057 """ 1058 For a dict node, return the value for key if key exists, else default. 1059 1060 If default is not given, it defaults to None, so that this method never raises a KeyError. 1061 """ 1062 return _make_node(self.dag, self.dag._call_builtin("daggerml:get", self.ref, key, default, name=name)) 1063 1064 def items(self) -> Iterator[tuple[str, "Node|Projection"]]: 1065 """ 1066 Iterate over key-value pairs of a dictionary node. 1067 1068 Returns 1069 ------- 1070 Iterator[tuple[Node, Node]] 1071 Iterator over (key, value) pairs 1072 """ 1073 if self.type != "dict": 1074 raise Error(f"Cannot iterate items of type: {self.type}", origin="dml", type="TypeError") 1075 for k in self: 1076 yield k, self[k] 1077 1078 def values(self) -> list["Node|Projection"]: 1079 """ 1080 Get the values of a dictionary node. 1081 1082 Parameters 1083 ---------- 1084 name : str, optional 1085 Name for the result node 1086 1087 Returns 1088 ------- 1089 list[Node] 1090 List of values in the dictionary node 1091 """ 1092 return [self[k] for k in self] 1093 1094 def assoc(self, key, value, *, name=None) -> "DictNode": 1095 """ 1096 For a dict node, associate a new value into the map 1097 1098 Returns 1099 ------- 1100 Node 1101 Node containing the new dict 1102 """ 1103 value_ref = value.ref if isinstance(value, Node) else value 1104 resp = self.dag._call_builtin("daggerml:assoc", self.ref, key, value_ref, name=name) 1105 return cast(DictNode, _make_node(self.dag, resp)) 1106 1107 def update(self, update) -> "DictNode": 1108 """ 1109 For a dict node, update like python dicts 1110 1111 Returns 1112 ------- 1113 Node 1114 Node containing the new collection 1115 1116 Notes 1117 ----- 1118 calls `assoc` iteratively for k, v pairs in update. 1119 1120 See Also 1121 -------- 1122 assoc : The main implementation 1123 """ 1124 for k, v in update.items(): 1125 self = self.assoc(k, v) 1126 return self 1127 1128 1129################################################################################ 1130################## Codec system for encoding literals in DAGs ################## 1131################################################################################ 1132 1133LITERAL_CODEC_ENTRYPOINT_GROUP = "daggerml.codecs" 1134_codecs: list[tuple[int, int, "LiteralCodec"]] = [] 1135_plugins_loaded = False 1136_lock = RLock() 1137 1138 1139class LiteralCodec(Protocol): 1140 def can_encode(self, value: Any) -> bool: ... 1141 1142 def encode(self, value: Any, dag: "Dag") -> Any: ... 1143 1144 1145class CodecError(Error): 1146 def __init__(self, message: str): 1147 super().__init__(message, origin="dml-codec", type="codec-error") 1148 1149 1150def _entry_points(group=LITERAL_CODEC_ENTRYPOINT_GROUP) -> list[metadata.EntryPoint]: 1151 points = metadata.entry_points() 1152 result = list(points.select(group=group)) 1153 result.sort(key=lambda ep: (ep.name, ep.value)) 1154 return result 1155 1156 1157def ensure_literal_codec_plugins_loaded() -> None: 1158 global _plugins_loaded 1159 if _plugins_loaded: 1160 return 1161 with _lock: 1162 codec_seq = 0 1163 if _plugins_loaded: 1164 return 1165 loaded = [] 1166 for entry_point in _entry_points(): 1167 try: 1168 registrations = entry_point.load()() 1169 for item in registrations: 1170 priority, codec = item 1171 codec_seq += 1 1172 loaded.append((priority, codec_seq, codec)) 1173 except Exception as e: 1174 msg = f"Literal codec plugin '{entry_point.name} ({entry_point.value})' failed: {e}" 1175 raise CodecError(msg) from None 1176 loaded.sort(key=lambda item: (-item[0], item[1])) 1177 _codecs.extend(loaded) 1178 _plugins_loaded = True 1179 1180 1181def iter_codecs() -> Iterator[LiteralCodec]: 1182 ensure_literal_codec_plugins_loaded() 1183 yield from [codec for _priority, _seq, codec in _codecs] 1184 1185 1186def apply_codec(value: Any, *, dag: Dag) -> Any: 1187 for codec in iter_codecs(): 1188 try: 1189 if codec.can_encode(value): 1190 resp = codec.encode(value, dag) 1191 if isinstance(resp, type(value)): 1192 codec_name = codec.__class__.__name__ 1193 msg = f"Literal codec {codec_name} encoded {value.__class__.__name__} to {resp.__class__.__name__}." 1194 raise CodecError(msg) 1195 return resp 1196 except Exception as e: 1197 if isinstance(e, DmlRepoError): 1198 raise 1199 raise CodecError(f"Literal codec {codec.__class__.__name__} failed: {e}") from e 1200 raise CodecError(f"No codec found for value of type {type(value).__name__}") 1201 1202 1203def apply_codecs(value: Any, *, dag: Dag) -> Any: 1204 while not isinstance(value, (*get_args(Scalar), *get_args(Collection), Ref)): 1205 value = apply_codec(value, dag=dag) 1206 if isinstance(value, list): 1207 return [apply_codecs(v, dag=dag) for v in value] 1208 if isinstance(value, dict): 1209 return {k: apply_codecs(v, dag=dag) for k, v in value.items()} 1210 if isinstance(value, Uri): 1211 return Uri(apply_codecs(value.uri, dag=dag)) 1212 if isinstance(value, Runnable): 1213 target = apply_codecs(value.target, dag=dag) 1214 sub = apply_codecs(value.sub, dag=dag) 1215 kwargs = {k: apply_codecs(v, dag=dag) for k, v in value.kwargs.items()} 1216 return Runnable(target=target, adapter=value.adapter, kwargs=kwargs, sub=sub) 1217 return value 1218 1219 1220class MiscPyTypeCodec: 1221 def can_encode(self, value: Any) -> bool: 1222 return isinstance(value, Mapping) or ( 1223 isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)) 1224 ) 1225 1226 def encode(self, value: Sequence | Mapping, dag: Dag) -> Any: 1227 if isinstance(value, Mapping): 1228 return {k: apply_codecs(v, dag=dag) for k, v in value.items()} 1229 if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): 1230 return [apply_codecs(v, dag=dag) for v in value] 1231 1232 1233class NodeCodec: 1234 def can_encode(self, value: Any) -> bool: 1235 return isinstance(value, Node) 1236 1237 def encode(self, value: "Node", dag: Dag) -> Ref: 1238 assert dag.token is not None, "DAG must have a token to encode nodes" 1239 if value.dag.token is not None and value.dag.token == dag.token: 1240 return value.ref 1241 if value.dag.ref is None: 1242 raise CodecError("Cannot encode node from uncommitted DAG in a different index") 1243 try: 1244 return dag.dml.runtime.put_import(dag._require_index_ref(), value.dag.ref, node=value.ref, name=None) 1245 except Exception as e: 1246 raise CodecError(f"Failed to encode cross-dag node import: {e}") from e 1247 1248 1249class ProjectionCodec: 1250 def can_encode(self, value: Any) -> bool: 1251 return isinstance(value, Projection) 1252 1253 def encode(self, value: "Projection", dag: Dag) -> Ref: 1254 assert dag.token is not None, "DAG must have a token to encode projections" 1255 node_ref = NodeCodec().encode(value.base, dag) 1256 for step in value.path: 1257 node_ref = dag._call_builtin("daggerml:get", node_ref, step) 1258 return node_ref 1259 1260 1261def codecs() -> list: 1262 return [(0, NodeCodec()), (0, MiscPyTypeCodec()), (0, ProjectionCodec())]
Return the active default Dml runtime.
Set the process-default Dml runtime.
Clear the process-default Dml runtime.
Temporarily override the default Dml runtime for the active context.
74def new( 75 name="", 76 message="", 77 cache_key: str | None = None, 78 execution_id: str | None = None, 79 tags: list[str] | None = None, 80 dml: Dml | None = None, 81) -> "Dag": 82 """Create a new DAG using the active or provided Dml runtime.""" 83 runtime = dml or get_default_dml() 84 execution = Ref(f"index:{execution_id}") if execution_id is not None else None 85 create_kwargs = {"cache_key": cache_key, "execution": execution} 86 if tags is not None: 87 create_kwargs["tags"] = tags 88 index_id = runtime.runtime.create(**create_kwargs) 89 return Dag(dml=runtime, token=index_id, name=name, message=message)
Create a new DAG using the active or provided Dml runtime.
92def load( 93 name: str, 94 dml: Dml | None = None, 95 *, 96 revision: Ref | str = "HEAD", 97 remote: bool = False, 98 dep: str | None = None, 99) -> "Dag": 100 """Load a DAG using the active default Dml runtime.""" 101 dml = dml or get_default_dml() 102 dag_ref = dml.show(revision, remote=remote, dep=dep)["dags"].get(name) 103 if dag_ref is None: 104 raise DmlRepoError(f"DAG not found: {name}") 105 return Dag(dml=dml, ref=dag_ref, name=name)
Load a DAG using the active default Dml runtime.
108def resume( 109 frozen: Ref, 110 *, 111 name: str, 112 message: str, 113 dml: Dml | None = None, 114) -> "Dag": 115 """Resume a frozen DAG runtime with explicitly supplied commit metadata.""" 116 runtime = dml or get_default_dml() 117 index_id = runtime.runtime.unfreeze(frozen) 118 return Dag(dml=runtime, token=index_id, name=name, message=message)
Resume a frozen DAG runtime with explicitly supplied commit metadata.
Create a temporary Dml runtime with an unborn attached HEAD.
128def status() -> dict[str, object]: 129 """Return status for the active default Dml runtime.""" 130 dml, source = _resolve_default_dml(create=True) 131 return { 132 "default": { 133 "source": source, 134 "has_scoped_override": _SCOPED_DEFAULT_DML.get() is not _NO_DEFAULT_DML, 135 "has_process_default": _PROCESS_DEFAULT_DML is not None, 136 }, 137 "status": dml.status(), 138 }
Return status for the active default Dml runtime.
340@dataclass 341class Dag: 342 dml: Dml 343 token: Optional[Ref] = None # Working index id 344 ref: Optional[Ref] = None 345 name: str = "" # DAG name for commit 346 message: str = "" # Commit message 347 348 def __repr__(self): 349 to = self.ref.to if self.ref else (self.token.to if self.token is not None else "NA") 350 return f"Dag({to})" 351 352 def __hash__(self): 353 "Useful only for tests." 354 return 42 355 356 def __eq__(self, other): 357 "DAG equality is based on identity, not content." 358 if not isinstance(other, Dag): 359 return False 360 return self.ref == other.ref and self.token == other.token and self.dml == other.dml 361 362 def __enter__(self): 363 "Catch exceptions and commit an Error" 364 assert not self.ref 365 return self 366 367 def __exit__(self, exc_type, exc_value, tb): 368 if exc_value is not None: 369 # Convert exception to Error and commit it 370 traceback.print_exception(exc_type, exc_value, tb) 371 err = Error.from_ex(exc_value) if not isinstance(exc_value, Error) else exc_value 372 self.commit(err) 373 374 def _require_index_ref(self) -> Ref: 375 if self.token is None: 376 raise DmlRepoError("No active index") 377 return self.token 378 379 def _read_dag_ref(self) -> Ref: 380 """Return the completed or partial DAG backing this wrapper's reads.""" 381 if self.ref is not None: 382 return self.ref 383 return cast(Ref, self.dml.runtime.describe(self._require_index_ref())["dag"]) 384 385 @property 386 def tags(self) -> list[str]: 387 """Return the normalized tags stored on this DAG.""" 388 return list(self.dml.dag.describe(self._read_dag_ref())["tags"]) 389 390 def _put_literal(self, value: Any, *, name: Optional[str] = None) -> Ref: 391 index_id = self._require_index_ref() 392 value = apply_codecs(value, dag=self) 393 return self.dml.runtime.put_literal(index_id, value, name=name) 394 395 def _start_fn(self, argv: list[Ref], *, name: Optional[str] = None) -> Optional[Ref]: 396 return self.dml.runtime.start_fn(self._require_index_ref(), argv, name=name) 397 398 def _call_builtin(self, uri: str, *args: Any, name: Optional[str] = None) -> Ref: 399 fn_ref = self._put_literal(Runnable(target=Uri(uri), kwargs={}, adapter="")) 400 argv: list[Ref] = [fn_ref] 401 for arg in args: 402 argv.append(arg if isinstance(arg, Ref) else self._put_literal(arg)) 403 result = self._start_fn(argv, name=name) 404 if result is None: 405 raise DmlRepoError("Function execution failed") 406 return result 407 408 def __len__(self) -> int: 409 return len(self.keys()) 410 411 def __iter__(self): 412 yield from self.keys() 413 414 def _get_named_node(self, name: str) -> "Node": 415 node_ref = self.dml.dag.describe(self._read_dag_ref())["names"].get(name) 416 if node_ref is None: 417 raise DmlRepoError(f"Node '{name}' not found in DAG") 418 return _make_node(self, node_ref) 419 420 def _set_named_node(self, name: str, value: Any) -> None: 421 if self.ref is not None: 422 raise DmlRepoError("Cannot set node names on a committed DAG.") 423 if isinstance(value, Node): 424 value = value.ref 425 if isinstance(value, Ref): 426 self.dml.runtime.set_node_name(self._require_index_ref(), name, value) 427 return 428 self.put(value, name=name) 429 430 def __getitem__(self, name: str) -> "Node": 431 return self._get_named_node(name) 432 433 def __setitem__(self, name: str, value: Any) -> None: 434 self.put(value, name=name) 435 436 def __getattr__(self, name: str) -> "Node": 437 if name.startswith("_"): 438 raise AttributeError(name) 439 return self[name] 440 441 def __setattr__(self, name: str, value: Any) -> None: 442 dataclass_fields = getattr(type(self), "__dataclass_fields__", {}) 443 if name in dataclass_fields or hasattr(type(self), name): 444 object.__setattr__(self, name, value) 445 return 446 self[name] = value 447 448 def keys(self) -> list[str]: 449 """Get the list of all node names in the dag""" 450 names_dict = self.dml.dag.describe(self._read_dag_ref())["names"] 451 return sorted(names_dict.keys()) 452 453 def values(self) -> list["Node"]: 454 """Get the list of all nodes in the dag""" 455 names_dict = self.dml.dag.describe(self._read_dag_ref())["names"] 456 return [_make_node(self, ref) for ref in names_dict.values()] 457 458 @property 459 def argv(self) -> "ListNode": 460 "Access the dag's argv node" 461 argv_ref = self.dml.dag.describe(self._read_dag_ref())["argv"] 462 if not isinstance(argv_ref, Ref): 463 raise DmlRepoError(f"'{self.__class__.__name__}' dag has no argv") 464 return cast(ListNode, _make_node(self, argv_ref)) 465 466 @property 467 def result(self) -> "Node": 468 """Get the result node of the dag""" 469 if self.ref is None: 470 raise DmlRepoError("Cannot access result of an uncommitted DAG") 471 description = self.dml.dag.describe(self.ref) 472 error_ref = description.get("error") 473 if isinstance(error_ref, Ref): 474 raise self.dml.dag.get_error(error_ref) 475 ref = description.get("result") 476 if not isinstance(ref, Ref): 477 raise DmlRepoError(f"'{self.__class__.__name__}' dag has not been committed yet") 478 return _make_node(self, ref) 479 480 @overload 481 def put(self, value: Union[list, "ListNode"], *, name=None) -> "ListNode": ... 482 @overload 483 def put(self, value: Union[dict, "DictNode"], *, name=None) -> "DictNode": ... 484 @overload 485 def put(self, value: Union[Runnable, "RunnableNode"], *, name=None) -> "RunnableNode": ... 486 @overload 487 def put(self, value: Union[Scalar, "ScalarNode"], *, name=None) -> "ScalarNode": ... 488 @overload 489 def put(self, value: Any, *, name=None) -> "Node": ... 490 def put(self, value: Any, *, name=None) -> "Node": 491 """ 492 Add a value to the DAG. 493 494 Parameters 495 ---------- 496 value : Union[Scalar, Collection] 497 Value to add 498 name : str, optional 499 Name for the node 500 Returns 501 ------- 502 Node 503 Node representing the value 504 505 Examples 506 -------- 507 >>> n1 = dag.put(42, name="answer") 508 >>> n1.value() 509 42 510 >>> n2 = dag.put({"a": 1, "b": [n1, "23"]}) 511 >>> n2.value() 512 {'a': 1, 'b': [42, '23']} 513 >>> n3 = dag.put({"a": 1, "b": [n1, "23"]}) 514 >>> n3.value() 515 {'a': 1, 'b': [42, '23']} 516 """ 517 return _make_node(self, self._put_literal(value, name=name)) 518 519 def require(self, dag_name: str | "Dag", node_name: str | None = None, *, name: str | None = None) -> "Node": 520 """ 521 Import a node from a different (committed) DAG into the current DAG. 522 523 Parameters 524 ---------- 525 dag_name : str 526 Name of the DAG to import from 527 node_name : str, optional 528 Name of the node to import. If None, imports the result node of the DAG. 529 530 Returns 531 ------- 532 Node 533 The loaded node or DAG 534 535 Examples 536 -------- 537 >>> dag = new(dml=dml, name="test", message="test") 538 >>> n1 = dag.put(42, name="answer") 539 >>> n2 = dag.put({"a": 1, "b": [n1, "23"]}, name="data") 540 >>> dag.commit(n2) 541 >>> dag2 = new(dml=dml, name="test2", message="test2") 542 >>> imported_n2 = dag2.require("test", "data", name="imported_data") 543 >>> imported_n2.value() 544 {'a': 1, 'b': [42, '23']} 545 """ 546 index = self._require_index_ref() 547 if isinstance(dag_name, Dag): 548 if dag_name.ref is None: 549 raise DmlRepoError("Cannot import an uncommitted DAG") 550 dag_ref = dag_name.ref 551 else: 552 commit = self.dml.runtime.describe(index)["parents"][0] 553 dag_ref = self.dml.show(revision=commit)["dags"].get(dag_name) 554 if dag_ref is None: 555 raise DmlRepoError(f"DAG not found: {dag_name}") 556 dag_info = self.dml.dag.describe(dag_ref) 557 node_ref = dag_info["names"].get(node_name) if node_name else dag_info.get("result") 558 if node_ref is None: 559 raise DmlRepoError(f"Node '{node_name}' not found in DAG '{dag_name}'") 560 node_ref = self.dml.runtime.put_import(index, dag_ref, node_ref, name=name) 561 return _make_node(self, node_ref) 562 563 def call( 564 self, 565 fn: Any, 566 *args: Any, 567 name: Optional[str] = None, 568 sleep: Optional[callable] = None, 569 timeout: int = -1, 570 ) -> "Node": 571 """ 572 Call a function node with arguments. 573 574 Parameters 575 ---------- 576 fn : Union[Runnable, RunnableNode] 577 Function to call 578 *args : Union[Node, Scalar, Collection] 579 Arguments to pass to the function 580 name : str, optional 581 Name for the result node 582 sleep : callable, optional 583 A nullary function that returns sleep time in milliseconds 584 timeout : int, default=-1 585 Maximum time to wait in milliseconds. If <= 0, wait indefinitely. 586 587 Returns 588 ------- 589 Node 590 Result node 591 592 Raises 593 ------ 594 TimeoutError 595 If the function call exceeds the timeout 596 Error 597 If the function returns an error 598 """ 599 sleep = sleep or BackoffWithJitter() 600 argv_seed = [fn, *args] 601 end = current_time_millis() + timeout 602 while timeout <= 0 or current_time_millis() < end: 603 argv_refs = [self._put_literal(value) for value in argv_seed] 604 resp = self._start_fn(argv_refs, name=name) 605 if resp: 606 return _make_node(self, resp) 607 time.sleep(sleep() / 1000) 608 raise TimeoutError(f"invoking function: {fn}") 609 610 def commit(self, value) -> None: 611 """ 612 Commit a value to the DAG. 613 614 Parameters 615 ---------- 616 value : Union[Node, Error, Any] 617 Value to commit 618 """ 619 # errors are committed as-is, everything else is a node 620 if not isinstance(value, (Error, Node)): 621 value = self.put(value) 622 if isinstance(value, Node): 623 value = value.ref 624 self.ref = self.dml.runtime.commit(self._require_index_ref(), value, message=self.message, name=self.name) 625 self.token = None # Clear the working index since it's now committed 626 627 def freeze(self, message: str | None = None) -> "Dag": 628 """Freeze this uncommitted DAG's runtime index for read-only inspection.""" 629 freeze_message = f"dag: {self.name}" 630 if message: 631 freeze_message = f"{freeze_message}\n{message}" 632 self.token = self.dml.runtime.freeze(self._require_index_ref(), message=freeze_message) 633 return self 634 635 def unfreeze(self) -> "Dag": 636 """Unfreeze this uncommitted DAG's runtime index.""" 637 self.token = self.dml.runtime.unfreeze(self._require_index_ref()) 638 return self 639 640 def cancel(self, max_retries: int = 3): 641 """Cancel the DAG's execution. 642 643 Parameters 644 ---------- 645 max_retries : int, default=3 646 Maximum cancellation retries after the initial attempt. 647 """ 648 if self.token is None: 649 raise DmlRepoError("Cannot cancel a committed DAG") 650 logger.info(f"Cancelling execution {self.token}") 651 self.dml.runtime.cancel(self.token, max_retries=max_retries) 652 self.token = None # Clear the index ref to indicate it's no longer active 653 raise CancellationError("DAG execution cancelled")
458 @property 459 def argv(self) -> "ListNode": 460 "Access the dag's argv node" 461 argv_ref = self.dml.dag.describe(self._read_dag_ref())["argv"] 462 if not isinstance(argv_ref, Ref): 463 raise DmlRepoError(f"'{self.__class__.__name__}' dag has no argv") 464 return cast(ListNode, _make_node(self, argv_ref))
Access the dag's argv node
466 @property 467 def result(self) -> "Node": 468 """Get the result node of the dag""" 469 if self.ref is None: 470 raise DmlRepoError("Cannot access result of an uncommitted DAG") 471 description = self.dml.dag.describe(self.ref) 472 error_ref = description.get("error") 473 if isinstance(error_ref, Ref): 474 raise self.dml.dag.get_error(error_ref) 475 ref = description.get("result") 476 if not isinstance(ref, Ref): 477 raise DmlRepoError(f"'{self.__class__.__name__}' dag has not been committed yet") 478 return _make_node(self, ref)
Get the result node of the dag
490 def put(self, value: Any, *, name=None) -> "Node": 491 """ 492 Add a value to the DAG. 493 494 Parameters 495 ---------- 496 value : Union[Scalar, Collection] 497 Value to add 498 name : str, optional 499 Name for the node 500 Returns 501 ------- 502 Node 503 Node representing the value 504 505 Examples 506 -------- 507 >>> n1 = dag.put(42, name="answer") 508 >>> n1.value() 509 42 510 >>> n2 = dag.put({"a": 1, "b": [n1, "23"]}) 511 >>> n2.value() 512 {'a': 1, 'b': [42, '23']} 513 >>> n3 = dag.put({"a": 1, "b": [n1, "23"]}) 514 >>> n3.value() 515 {'a': 1, 'b': [42, '23']} 516 """ 517 return _make_node(self, self._put_literal(value, name=name))
Add a value to the DAG.
>>> n1 = dag.put(42, name="answer")
>>> n1.value()
42
>>> n2 = dag.put({"a": 1, "b": [n1, "23"]})
>>> n2.value()
{'a': 1, 'b': [42, '23']}
>>> n3 = dag.put({"a": 1, "b": [n1, "23"]})
>>> n3.value()
{'a': 1, 'b': [42, '23']}
519 def require(self, dag_name: str | "Dag", node_name: str | None = None, *, name: str | None = None) -> "Node": 520 """ 521 Import a node from a different (committed) DAG into the current DAG. 522 523 Parameters 524 ---------- 525 dag_name : str 526 Name of the DAG to import from 527 node_name : str, optional 528 Name of the node to import. If None, imports the result node of the DAG. 529 530 Returns 531 ------- 532 Node 533 The loaded node or DAG 534 535 Examples 536 -------- 537 >>> dag = new(dml=dml, name="test", message="test") 538 >>> n1 = dag.put(42, name="answer") 539 >>> n2 = dag.put({"a": 1, "b": [n1, "23"]}, name="data") 540 >>> dag.commit(n2) 541 >>> dag2 = new(dml=dml, name="test2", message="test2") 542 >>> imported_n2 = dag2.require("test", "data", name="imported_data") 543 >>> imported_n2.value() 544 {'a': 1, 'b': [42, '23']} 545 """ 546 index = self._require_index_ref() 547 if isinstance(dag_name, Dag): 548 if dag_name.ref is None: 549 raise DmlRepoError("Cannot import an uncommitted DAG") 550 dag_ref = dag_name.ref 551 else: 552 commit = self.dml.runtime.describe(index)["parents"][0] 553 dag_ref = self.dml.show(revision=commit)["dags"].get(dag_name) 554 if dag_ref is None: 555 raise DmlRepoError(f"DAG not found: {dag_name}") 556 dag_info = self.dml.dag.describe(dag_ref) 557 node_ref = dag_info["names"].get(node_name) if node_name else dag_info.get("result") 558 if node_ref is None: 559 raise DmlRepoError(f"Node '{node_name}' not found in DAG '{dag_name}'") 560 node_ref = self.dml.runtime.put_import(index, dag_ref, node_ref, name=name) 561 return _make_node(self, node_ref)
Import a node from a different (committed) DAG into the current DAG.
>>> dag = new(dml=dml, name="test", message="test")
>>> n1 = dag.put(42, name="answer")
>>> n2 = dag.put({"a": 1, "b": [n1, "23"]}, name="data")
>>> dag.commit(n2)
>>> dag2 = new(dml=dml, name="test2", message="test2")
>>> imported_n2 = dag2.require("test", "data", name="imported_data")
>>> imported_n2.value()
{'a': 1, 'b': [42, '23']}
563 def call( 564 self, 565 fn: Any, 566 *args: Any, 567 name: Optional[str] = None, 568 sleep: Optional[callable] = None, 569 timeout: int = -1, 570 ) -> "Node": 571 """ 572 Call a function node with arguments. 573 574 Parameters 575 ---------- 576 fn : Union[Runnable, RunnableNode] 577 Function to call 578 *args : Union[Node, Scalar, Collection] 579 Arguments to pass to the function 580 name : str, optional 581 Name for the result node 582 sleep : callable, optional 583 A nullary function that returns sleep time in milliseconds 584 timeout : int, default=-1 585 Maximum time to wait in milliseconds. If <= 0, wait indefinitely. 586 587 Returns 588 ------- 589 Node 590 Result node 591 592 Raises 593 ------ 594 TimeoutError 595 If the function call exceeds the timeout 596 Error 597 If the function returns an error 598 """ 599 sleep = sleep or BackoffWithJitter() 600 argv_seed = [fn, *args] 601 end = current_time_millis() + timeout 602 while timeout <= 0 or current_time_millis() < end: 603 argv_refs = [self._put_literal(value) for value in argv_seed] 604 resp = self._start_fn(argv_refs, name=name) 605 if resp: 606 return _make_node(self, resp) 607 time.sleep(sleep() / 1000) 608 raise TimeoutError(f"invoking function: {fn}")
Call a function node with arguments.
610 def commit(self, value) -> None: 611 """ 612 Commit a value to the DAG. 613 614 Parameters 615 ---------- 616 value : Union[Node, Error, Any] 617 Value to commit 618 """ 619 # errors are committed as-is, everything else is a node 620 if not isinstance(value, (Error, Node)): 621 value = self.put(value) 622 if isinstance(value, Node): 623 value = value.ref 624 self.ref = self.dml.runtime.commit(self._require_index_ref(), value, message=self.message, name=self.name) 625 self.token = None # Clear the working index since it's now committed
Commit a value to the DAG.
627 def freeze(self, message: str | None = None) -> "Dag": 628 """Freeze this uncommitted DAG's runtime index for read-only inspection.""" 629 freeze_message = f"dag: {self.name}" 630 if message: 631 freeze_message = f"{freeze_message}\n{message}" 632 self.token = self.dml.runtime.freeze(self._require_index_ref(), message=freeze_message) 633 return self
Freeze this uncommitted DAG's runtime index for read-only inspection.
640 def cancel(self, max_retries: int = 3): 641 """Cancel the DAG's execution. 642 643 Parameters 644 ---------- 645 max_retries : int, default=3 646 Maximum cancellation retries after the initial attempt. 647 """ 648 if self.token is None: 649 raise DmlRepoError("Cannot cancel a committed DAG") 650 logger.info(f"Cancelling execution {self.token}") 651 self.dml.runtime.cancel(self.token, max_retries=max_retries) 652 self.token = None # Clear the index ref to indicate it's no longer active 653 raise CancellationError("DAG execution cancelled")
Cancel the DAG's execution.
656class NodeError(Error): 657 """A stored error enriched with the node that produced it.""" 658 659 def __init__(self, error: Error, *, node_ref: Ref, dag: Dag): 660 super().__init__(message=error.message, origin=error.origin, type=error.type, stack=list(error.stack)) 661 self.node_ref = node_ref 662 self.dag = dag 663 664 def context(self) -> Dag: 665 """Return the function DAG that recorded this node's failure.""" 666 dag = self.dag 667 node_ref = self.node_ref 668 while True: 669 info = dag.dml.dag.describe_node(node_ref) 670 if info["type"] == "FnNode": 671 return Dag(dml=dag.dml, ref=info["dag"]) 672 if info["type"] != "ImportNode": 673 raise DmlRepoError(f"Node {node_ref} does not resolve to a failed function call") 674 dag = Dag(dml=dag.dml, ref=info["dag"]) 675 node_ref = info["node"]
A stored error enriched with the node that produced it.
664 def context(self) -> Dag: 665 """Return the function DAG that recorded this node's failure.""" 666 dag = self.dag 667 node_ref = self.node_ref 668 while True: 669 info = dag.dml.dag.describe_node(node_ref) 670 if info["type"] == "FnNode": 671 return Dag(dml=dag.dml, ref=info["dag"]) 672 if info["type"] != "ImportNode": 673 raise DmlRepoError(f"Node {node_ref} does not resolve to a failed function call") 674 dag = Dag(dml=dag.dml, ref=info["dag"]) 675 node_ref = info["node"]
Return the function DAG that recorded this node's failure.
678@dataclass(frozen=True) 679class Node: # noqa: F811 680 """ 681 Representation of a node in a DaggerML DAG. 682 683 Parameters 684 ---------- 685 dag : Dag 686 Parent DAG 687 ref : Ref 688 Node reference 689 """ 690 691 dag: Dag 692 ref: Ref 693 _info: dict = field(default_factory=dict) 694 695 def __repr__(self): 696 ref_id = self.ref if isinstance(self.ref, Error) else self.ref.to 697 return f"{self.__class__.__name__}({ref_id})" 698 699 def __hash__(self): 700 return hash(self.ref) 701 702 def __eq__(self, other): 703 if not isinstance(other, Node): 704 return NotImplemented 705 return self.ref == other.ref 706 707 def context(self, *, root: bool = True) -> Dag: 708 """ 709 Resolve the provenance context (DAG) for this node. 710 711 This follows import/function provenance while treating builtin 712 collection-construction and selection DAGs as transparent. 713 714 Parameters 715 ---------- 716 root : bool, default=True 717 If False, return the nearest sub-DAG in which this value exists as a 718 proper node across a non-builtin import/function boundary. If True, 719 continue recursively until provenance no longer crosses a 720 non-builtin import/function boundary and return that first rooted 721 context. 722 723 Returns 724 ------- 725 Dag 726 The nearest or rooted provenance DAG for this node. 727 728 Examples 729 -------- 730 >>> source = new(dml=dml, name="source", message="source") 731 >>> answer = source.put(42, name="answer") 732 >>> payload = source.put({"answer": answer}, name="payload") 733 >>> source.commit(payload) 734 >>> consumer = new(dml=dml, name="consumer", message="consumer") 735 >>> imported = consumer.require("source", "payload", name="payload") 736 >>> consumer.commit(imported) 737 >>> loaded = load("consumer", dml=dml) 738 >>> loaded.result["answer"].context() == source 739 True 740 """ 741 return _resolve_context(self, (), root=root) 742 743 @property 744 def type(self): 745 """Get the data type of the node.""" 746 return self._info["data_type"] 747 748 @overload 749 def value(self: "ScalarNode") -> Scalar: ... 750 @overload 751 def value(self: "ListNode") -> list: ... 752 @overload 753 def value(self: "DictNode") -> dict: ... 754 @overload 755 def value(self: "RunnableNode") -> Runnable: ... 756 @overload 757 def value(self: "Node") -> Any: ... 758 def value(self): 759 """ 760 Get the concrete value of this node. 761 762 Returns 763 ------- 764 Any 765 The actual value represented by this node 766 """ 767 value = self.dag.dml.dag.get_node(self.ref, recursive=True) 768 if isinstance(value, Error): 769 raise NodeError(value, node_ref=self.ref, dag=self.dag) 770 return value 771 772 def __call__(self, *args, name=None, sleep=None, timeout=-1, **kw) -> "Node": 773 raise TypeError(f"Node of type '{self.type}' is not callable")
Representation of a node in a DaggerML DAG.
707 def context(self, *, root: bool = True) -> Dag: 708 """ 709 Resolve the provenance context (DAG) for this node. 710 711 This follows import/function provenance while treating builtin 712 collection-construction and selection DAGs as transparent. 713 714 Parameters 715 ---------- 716 root : bool, default=True 717 If False, return the nearest sub-DAG in which this value exists as a 718 proper node across a non-builtin import/function boundary. If True, 719 continue recursively until provenance no longer crosses a 720 non-builtin import/function boundary and return that first rooted 721 context. 722 723 Returns 724 ------- 725 Dag 726 The nearest or rooted provenance DAG for this node. 727 728 Examples 729 -------- 730 >>> source = new(dml=dml, name="source", message="source") 731 >>> answer = source.put(42, name="answer") 732 >>> payload = source.put({"answer": answer}, name="payload") 733 >>> source.commit(payload) 734 >>> consumer = new(dml=dml, name="consumer", message="consumer") 735 >>> imported = consumer.require("source", "payload", name="payload") 736 >>> consumer.commit(imported) 737 >>> loaded = load("consumer", dml=dml) 738 >>> loaded.result["answer"].context() == source 739 True 740 """ 741 return _resolve_context(self, (), root=root)
Resolve the provenance context (DAG) for this node.
This follows import/function provenance while treating builtin collection-construction and selection DAGs as transparent.
>>> source = new(dml=dml, name="source", message="source")
>>> answer = source.put(42, name="answer")
>>> payload = source.put({"answer": answer}, name="payload")
>>> source.commit(payload)
>>> consumer = new(dml=dml, name="consumer", message="consumer")
>>> imported = consumer.require("source", "payload", name="payload")
>>> consumer.commit(imported)
>>> loaded = load("consumer", dml=dml)
>>> loaded.result["answer"].context() == source
True
758 def value(self): 759 """ 760 Get the concrete value of this node. 761 762 Returns 763 ------- 764 Any 765 The actual value represented by this node 766 """ 767 value = self.dag.dml.dag.get_node(self.ref, recursive=True) 768 if isinstance(value, Error): 769 raise NodeError(value, node_ref=self.ref, dag=self.dag) 770 return value
Get the concrete value of this node.
Representation of a node in a DaggerML DAG.
780class RunnableNode(Node): 781 def __call__(self, *args, name=None, sleep=None, timeout=-1) -> "Node": 782 """ 783 Call this node as a function. 784 785 Parameters 786 ---------- 787 *args : Any 788 Arguments to pass to the function 789 name : str, optional 790 Name for the result node 791 sleep : callable, optional 792 A nullary function that returns sleep time in milliseconds 793 timeout : int, default=-1 794 Maximum time to wait in milliseconds. -1 means wait forever. 795 796 Returns 797 ------- 798 Node 799 Result node 800 801 Raises 802 ------ 803 TimeoutError 804 If the function call exceeds the timeout 805 Error 806 If the function returns an error 807 """ 808 return self.dag.call(self, *args, name=name, sleep=sleep, timeout=timeout)
Representation of a node in a DaggerML DAG.
811@dataclass(frozen=True) 812class Projection: 813 dag: Dag 814 base: Node 815 path: tuple[ProjectionStep, ...] 816 _info: dict = field(default_factory=dict) 817 818 def __repr__(self): 819 return f"Projection({self.base!r}, path={self.path!r})" 820 821 @classmethod 822 def from_step(cls, base: Node, step: ProjectionStep) -> "Projection": 823 return cls(dag=base.dag, base=base, path=(step,)) 824 825 def _extend(self, step: ProjectionStep) -> "Projection": 826 return Projection(dag=self.dag, base=self.base, path=(*self.path, step)) 827 828 def value(self): 829 return _apply_projection_path(self.base.value(), self.path) 830 831 def context(self, *, root: bool = True) -> Dag: 832 return _resolve_context(self.base, self.path, root=root) 833 834 def __call__(self, *args, **kwargs): 835 raise TypeError(f"Projection of type '{self.type}' is not callable") 836 837 @property 838 def type(self): 839 if "data_type" in self._info: 840 return self._info["data_type"] 841 return _info_for_value(self.value())["data_type"] 842 843 def __len__(self): 844 if "length" in self._info: 845 return self._info["length"] 846 value = self.value() 847 if not isinstance(value, (list, dict)): 848 raise TypeError(f"Object of type '{type(value).__name__}' has no len()") 849 return len(value) 850 851 def __iter__(self): 852 value = self.value() 853 if isinstance(value, list): 854 for i in range(len(value)): 855 yield self[i] 856 return 857 if isinstance(value, dict): 858 yield from self.keys() 859 return 860 raise TypeError(f"Object of type '{type(value).__name__}' is not iterable") 861 862 def __getitem__(self, key: ProjectionStep | slice) -> "Projection": 863 value = self.value() 864 if isinstance(value, dict): 865 if not isinstance(key, str): 866 raise TypeError(f"Dict keys must be strings but got {type(key).__name__}") 867 step = cast(ProjectionStep, key) 868 elif isinstance(value, list): 869 if not isinstance(key, (int, slice)): 870 raise TypeError(f"List indices must be integers or slices but got {type(key).__name__}") 871 step = _normalize_projection_step(cast(ProjectionStep | slice, key), length=len(value)) 872 else: 873 raise TypeError(f"Cannot project into object of type '{type(value).__name__}'") 874 projected = self._extend(step) 875 return Projection(dag=self.dag, base=self.base, path=projected.path, _info=_info_for_value(projected.value())) 876 877 def keys(self) -> list[str]: 878 value = self.value() 879 if not isinstance(value, dict): 880 raise TypeError(f"Cannot get keys of type: {type(value).__name__}") 881 return sorted(value.keys())
884class CollectionNode(Node): # noqa: F811 885 """ 886 Representation of a collection node in a DaggerML DAG. 887 888 Parameters 889 ---------- 890 dag : Dag 891 Parent DAG 892 ref : Ref 893 Node reference 894 """ 895 896 def contains(self, item, *, name=None) -> "ScalarNode": 897 """ 898 For collection nodes, checks to see if `item` is in `self` 899 900 Returns 901 ------- 902 Node 903 Node with the boolean of is `item` in `self` 904 """ 905 item_ref = item.ref if isinstance(item, Node) else item 906 result = self.dag._call_builtin("daggerml:contains", self.ref, item_ref, name=name) 907 return cast(ScalarNode, _make_node(self.dag, result)) 908 909 def __contains__(self, item): 910 return self.contains(item).value() # has to return boolean 911 912 def __len__(self): # python requires this to be an int 913 """ 914 Get the node's length 915 916 Returns 917 ------- 918 Node 919 Node with the length of the collection 920 921 Raises 922 ------ 923 Error 924 If the node isn't a collection (e.g. list or dict). 925 """ 926 return self._info["length"]
Representation of a collection node in a DaggerML DAG.
896 def contains(self, item, *, name=None) -> "ScalarNode": 897 """ 898 For collection nodes, checks to see if `item` is in `self` 899 900 Returns 901 ------- 902 Node 903 Node with the boolean of is `item` in `self` 904 """ 905 item_ref = item.ref if isinstance(item, Node) else item 906 result = self.dag._call_builtin("daggerml:contains", self.ref, item_ref, name=name) 907 return cast(ScalarNode, _make_node(self.dag, result))
For collection nodes, checks to see if item is in self
item in self929class ListNode(CollectionNode): # noqa: F811 930 """ 931 Representation of a collection node in a DaggerML DAG. 932 933 Parameters 934 ---------- 935 dag : Dag 936 Parent DAG 937 ref : Ref 938 Node reference 939 """ 940 941 @overload 942 def __getitem__(self, key: Union[slice, list[int]]) -> Union["ListNode", Projection]: ... 943 @overload 944 def __getitem__(self, key: Union[int, "Node"]) -> Union["Node", Projection]: ... 945 def __getitem__(self, key: Union[slice, list[int], int, "Node"]) -> Union["Node", Projection]: 946 if self.dag.ref is not None: 947 if isinstance(key, Node): 948 raise TypeError("Committed list projections require concrete int or slice keys") 949 step = _normalize_projection_step(key, length=len(self)) 950 return Projection( 951 dag=self.dag, 952 base=self, 953 path=(step,), 954 _info=_info_for_value(_apply_projection_step(self.value(), step)), 955 ) 956 if isinstance(key, slice): 957 if key.step is not None: 958 raise ValueError("Slice step is not supported") 959 start = key.start if key.start is not None else 0 960 stop = key.stop if key.stop is not None else len(self) 961 key = [start, stop] 962 return _make_node(self.dag, self.dag._call_builtin("daggerml:get", self.ref, key)) 963 964 def __iter__(self): 965 """ 966 Iterate over the node's values (items if it's a list, and keys if it's a 967 dict) 968 969 Returns 970 ------- 971 Node 972 Result node 973 974 Raises 975 ------ 976 Error 977 If the node isn't a collection (e.g. list or dict). 978 """ 979 for i in range(len(self)): 980 yield self[i] 981 982 def conj(self, item, *, name=None) -> "ListNode": 983 """ 984 For a list node, append an item 985 986 Returns 987 ------- 988 Node 989 Node containing the new collection 990 991 Notes 992 ----- 993 `append` is an alias `conj` 994 """ 995 item_ref = item.ref if isinstance(item, Node) else item 996 resp = self.dag._call_builtin("daggerml:conj", self.ref, item_ref, name=name) 997 return cast(ListNode, _make_node(self.dag, resp)) 998 999 def append(self, item, *, name=None) -> "ListNode": 1000 """ 1001 For a list node, append an item 1002 1003 Returns 1004 ------- 1005 Node 1006 Node containing the new collection 1007 1008 See Also 1009 -------- 1010 conj : The main implementation 1011 """ 1012 return self.conj(item, name=name)
Representation of a collection node in a DaggerML DAG.
982 def conj(self, item, *, name=None) -> "ListNode": 983 """ 984 For a list node, append an item 985 986 Returns 987 ------- 988 Node 989 Node containing the new collection 990 991 Notes 992 ----- 993 `append` is an alias `conj` 994 """ 995 item_ref = item.ref if isinstance(item, Node) else item 996 resp = self.dag._call_builtin("daggerml:conj", self.ref, item_ref, name=name) 997 return cast(ListNode, _make_node(self.dag, resp))
For a list node, append an item
conj: The main implementation
1015class DictNode(CollectionNode): # noqa: F811 1016 def __getitem__(self, key: Union[str, "Node"]) -> Union["Node", Projection]: 1017 if self.dag.ref is not None: 1018 if not isinstance(key, str): 1019 raise TypeError(f"Dict keys must be strings but got {type(key).__name__}") 1020 return Projection(dag=self.dag, base=self, path=(key,), _info=_info_for_value(self.value()[key])) 1021 return _make_node(self.dag, self.dag._call_builtin("daggerml:get", self.ref, key)) 1022 1023 def keys(self) -> list[str]: 1024 """ 1025 Get the keys of a dictionary node. 1026 1027 Parameters 1028 ---------- 1029 name : str, optional 1030 Name for the result node 1031 1032 Returns 1033 ------- 1034 list[str] 1035 List of keys in the dictionary node 1036 """ 1037 return self._info["keys"].copy() 1038 1039 def __iter__(self): 1040 """ 1041 Iterate over the node's values (items if it's a list, and keys if it's a 1042 dict) 1043 1044 Returns 1045 ------- 1046 Node 1047 Result node 1048 1049 Raises 1050 ------ 1051 Error 1052 If the node isn't a collection (e.g. list or dict). 1053 """ 1054 for k in self.keys(): 1055 yield k 1056 1057 def get(self, key, default=None, *, name=None) -> "Node": 1058 """ 1059 For a dict node, return the value for key if key exists, else default. 1060 1061 If default is not given, it defaults to None, so that this method never raises a KeyError. 1062 """ 1063 return _make_node(self.dag, self.dag._call_builtin("daggerml:get", self.ref, key, default, name=name)) 1064 1065 def items(self) -> Iterator[tuple[str, "Node|Projection"]]: 1066 """ 1067 Iterate over key-value pairs of a dictionary node. 1068 1069 Returns 1070 ------- 1071 Iterator[tuple[Node, Node]] 1072 Iterator over (key, value) pairs 1073 """ 1074 if self.type != "dict": 1075 raise Error(f"Cannot iterate items of type: {self.type}", origin="dml", type="TypeError") 1076 for k in self: 1077 yield k, self[k] 1078 1079 def values(self) -> list["Node|Projection"]: 1080 """ 1081 Get the values of a dictionary node. 1082 1083 Parameters 1084 ---------- 1085 name : str, optional 1086 Name for the result node 1087 1088 Returns 1089 ------- 1090 list[Node] 1091 List of values in the dictionary node 1092 """ 1093 return [self[k] for k in self] 1094 1095 def assoc(self, key, value, *, name=None) -> "DictNode": 1096 """ 1097 For a dict node, associate a new value into the map 1098 1099 Returns 1100 ------- 1101 Node 1102 Node containing the new dict 1103 """ 1104 value_ref = value.ref if isinstance(value, Node) else value 1105 resp = self.dag._call_builtin("daggerml:assoc", self.ref, key, value_ref, name=name) 1106 return cast(DictNode, _make_node(self.dag, resp)) 1107 1108 def update(self, update) -> "DictNode": 1109 """ 1110 For a dict node, update like python dicts 1111 1112 Returns 1113 ------- 1114 Node 1115 Node containing the new collection 1116 1117 Notes 1118 ----- 1119 calls `assoc` iteratively for k, v pairs in update. 1120 1121 See Also 1122 -------- 1123 assoc : The main implementation 1124 """ 1125 for k, v in update.items(): 1126 self = self.assoc(k, v) 1127 return self
Representation of a collection node in a DaggerML DAG.
1023 def keys(self) -> list[str]: 1024 """ 1025 Get the keys of a dictionary node. 1026 1027 Parameters 1028 ---------- 1029 name : str, optional 1030 Name for the result node 1031 1032 Returns 1033 ------- 1034 list[str] 1035 List of keys in the dictionary node 1036 """ 1037 return self._info["keys"].copy()
Get the keys of a dictionary node.
1057 def get(self, key, default=None, *, name=None) -> "Node": 1058 """ 1059 For a dict node, return the value for key if key exists, else default. 1060 1061 If default is not given, it defaults to None, so that this method never raises a KeyError. 1062 """ 1063 return _make_node(self.dag, self.dag._call_builtin("daggerml:get", self.ref, key, default, name=name))
For a dict node, return the value for key if key exists, else default.
If default is not given, it defaults to None, so that this method never raises a KeyError.
1065 def items(self) -> Iterator[tuple[str, "Node|Projection"]]: 1066 """ 1067 Iterate over key-value pairs of a dictionary node. 1068 1069 Returns 1070 ------- 1071 Iterator[tuple[Node, Node]] 1072 Iterator over (key, value) pairs 1073 """ 1074 if self.type != "dict": 1075 raise Error(f"Cannot iterate items of type: {self.type}", origin="dml", type="TypeError") 1076 for k in self: 1077 yield k, self[k]
Iterate over key-value pairs of a dictionary node.
1079 def values(self) -> list["Node|Projection"]: 1080 """ 1081 Get the values of a dictionary node. 1082 1083 Parameters 1084 ---------- 1085 name : str, optional 1086 Name for the result node 1087 1088 Returns 1089 ------- 1090 list[Node] 1091 List of values in the dictionary node 1092 """ 1093 return [self[k] for k in self]
Get the values of a dictionary node.
1095 def assoc(self, key, value, *, name=None) -> "DictNode": 1096 """ 1097 For a dict node, associate a new value into the map 1098 1099 Returns 1100 ------- 1101 Node 1102 Node containing the new dict 1103 """ 1104 value_ref = value.ref if isinstance(value, Node) else value 1105 resp = self.dag._call_builtin("daggerml:assoc", self.ref, key, value_ref, name=name) 1106 return cast(DictNode, _make_node(self.dag, resp))
For a dict node, associate a new value into the map
1108 def update(self, update) -> "DictNode": 1109 """ 1110 For a dict node, update like python dicts 1111 1112 Returns 1113 ------- 1114 Node 1115 Node containing the new collection 1116 1117 Notes 1118 ----- 1119 calls `assoc` iteratively for k, v pairs in update. 1120 1121 See Also 1122 -------- 1123 assoc : The main implementation 1124 """ 1125 for k, v in update.items(): 1126 self = self.assoc(k, v) 1127 return self
Base class for protocol classes.
Protocol classes are defined as::
class Proto(Protocol):
def meth(self) -> int:
...
Such classes are primarily used with static type checkers that recognize structural subtyping (static duck-typing).
For example::
class C:
def meth(self) -> int:
return 0
def func(x: Proto) -> int:
return x.meth()
func(C()) # Passes static type check
See PEP 544 for details. Protocol classes decorated with @typing.runtime_checkable act as simple-minded runtime protocols that check only the presence of given attributes, ignoring their type signatures. Protocol classes can be generic, they are defined as::
class GenProto[T](Protocol):
def meth(self) -> T:
...
1968def _no_init_or_replace_init(self, *args, **kwargs): 1969 cls = type(self) 1970 1971 if cls._is_protocol: 1972 raise TypeError('Protocols cannot be instantiated') 1973 1974 # Already using a custom `__init__`. No need to calculate correct 1975 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1976 if cls.__init__ is not _no_init_or_replace_init: 1977 return 1978 1979 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1980 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1981 # searches for a proper new `__init__` in the MRO. The new `__init__` 1982 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1983 # instantiation of the protocol subclass will thus use the new 1984 # `__init__` and no longer call `_no_init_or_replace_init`. 1985 for base in cls.__mro__: 1986 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1987 if init is not _no_init_or_replace_init: 1988 cls.__init__ = init 1989 break 1990 else: 1991 # should not happen 1992 cls.__init__ = object.__init__ 1993 1994 cls.__init__(self, *args, **kwargs)
1141 def can_encode(self, value: Any) -> bool: ...
Error information with stack traces.
Represents a captured error from a computation, storing error details and stack trace information for debugging.
1158def ensure_literal_codec_plugins_loaded() -> None: 1159 global _plugins_loaded 1160 if _plugins_loaded: 1161 return 1162 with _lock: 1163 codec_seq = 0 1164 if _plugins_loaded: 1165 return 1166 loaded = [] 1167 for entry_point in _entry_points(): 1168 try: 1169 registrations = entry_point.load()() 1170 for item in registrations: 1171 priority, codec = item 1172 codec_seq += 1 1173 loaded.append((priority, codec_seq, codec)) 1174 except Exception as e: 1175 msg = f"Literal codec plugin '{entry_point.name} ({entry_point.value})' failed: {e}" 1176 raise CodecError(msg) from None 1177 loaded.sort(key=lambda item: (-item[0], item[1])) 1178 _codecs.extend(loaded) 1179 _plugins_loaded = True
1187def apply_codec(value: Any, *, dag: Dag) -> Any: 1188 for codec in iter_codecs(): 1189 try: 1190 if codec.can_encode(value): 1191 resp = codec.encode(value, dag) 1192 if isinstance(resp, type(value)): 1193 codec_name = codec.__class__.__name__ 1194 msg = f"Literal codec {codec_name} encoded {value.__class__.__name__} to {resp.__class__.__name__}." 1195 raise CodecError(msg) 1196 return resp 1197 except Exception as e: 1198 if isinstance(e, DmlRepoError): 1199 raise 1200 raise CodecError(f"Literal codec {codec.__class__.__name__} failed: {e}") from e 1201 raise CodecError(f"No codec found for value of type {type(value).__name__}")
1204def apply_codecs(value: Any, *, dag: Dag) -> Any: 1205 while not isinstance(value, (*get_args(Scalar), *get_args(Collection), Ref)): 1206 value = apply_codec(value, dag=dag) 1207 if isinstance(value, list): 1208 return [apply_codecs(v, dag=dag) for v in value] 1209 if isinstance(value, dict): 1210 return {k: apply_codecs(v, dag=dag) for k, v in value.items()} 1211 if isinstance(value, Uri): 1212 return Uri(apply_codecs(value.uri, dag=dag)) 1213 if isinstance(value, Runnable): 1214 target = apply_codecs(value.target, dag=dag) 1215 sub = apply_codecs(value.sub, dag=dag) 1216 kwargs = {k: apply_codecs(v, dag=dag) for k, v in value.kwargs.items()} 1217 return Runnable(target=target, adapter=value.adapter, kwargs=kwargs, sub=sub) 1218 return value
1221class MiscPyTypeCodec: 1222 def can_encode(self, value: Any) -> bool: 1223 return isinstance(value, Mapping) or ( 1224 isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)) 1225 ) 1226 1227 def encode(self, value: Sequence | Mapping, dag: Dag) -> Any: 1228 if isinstance(value, Mapping): 1229 return {k: apply_codecs(v, dag=dag) for k, v in value.items()} 1230 if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): 1231 return [apply_codecs(v, dag=dag) for v in value]
1227 def encode(self, value: Sequence | Mapping, dag: Dag) -> Any: 1228 if isinstance(value, Mapping): 1229 return {k: apply_codecs(v, dag=dag) for k, v in value.items()} 1230 if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): 1231 return [apply_codecs(v, dag=dag) for v in value]
1234class NodeCodec: 1235 def can_encode(self, value: Any) -> bool: 1236 return isinstance(value, Node) 1237 1238 def encode(self, value: "Node", dag: Dag) -> Ref: 1239 assert dag.token is not None, "DAG must have a token to encode nodes" 1240 if value.dag.token is not None and value.dag.token == dag.token: 1241 return value.ref 1242 if value.dag.ref is None: 1243 raise CodecError("Cannot encode node from uncommitted DAG in a different index") 1244 try: 1245 return dag.dml.runtime.put_import(dag._require_index_ref(), value.dag.ref, node=value.ref, name=None) 1246 except Exception as e: 1247 raise CodecError(f"Failed to encode cross-dag node import: {e}") from e
1238 def encode(self, value: "Node", dag: Dag) -> Ref: 1239 assert dag.token is not None, "DAG must have a token to encode nodes" 1240 if value.dag.token is not None and value.dag.token == dag.token: 1241 return value.ref 1242 if value.dag.ref is None: 1243 raise CodecError("Cannot encode node from uncommitted DAG in a different index") 1244 try: 1245 return dag.dml.runtime.put_import(dag._require_index_ref(), value.dag.ref, node=value.ref, name=None) 1246 except Exception as e: 1247 raise CodecError(f"Failed to encode cross-dag node import: {e}") from e
1250class ProjectionCodec: 1251 def can_encode(self, value: Any) -> bool: 1252 return isinstance(value, Projection) 1253 1254 def encode(self, value: "Projection", dag: Dag) -> Ref: 1255 assert dag.token is not None, "DAG must have a token to encode projections" 1256 node_ref = NodeCodec().encode(value.base, dag) 1257 for step in value.path: 1258 node_ref = dag._call_builtin("daggerml:get", node_ref, step) 1259 return node_ref
1254 def encode(self, value: "Projection", dag: Dag) -> Ref: 1255 assert dag.token is not None, "DAG must have a token to encode projections" 1256 node_ref = NodeCodec().encode(value.base, dag) 1257 for step in value.path: 1258 node_ref = dag._call_builtin("daggerml:get", node_ref, step) 1259 return node_ref