typed-ffmpeg-compatible 2.4.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. typed_ffmpeg/__init__.py +25 -0
  2. typed_ffmpeg/base.py +114 -0
  3. typed_ffmpeg/common/__init__.py +0 -0
  4. typed_ffmpeg/common/schema.py +308 -0
  5. typed_ffmpeg/common/serialize.py +132 -0
  6. typed_ffmpeg/dag/__init__.py +13 -0
  7. typed_ffmpeg/dag/compile.py +51 -0
  8. typed_ffmpeg/dag/context.py +221 -0
  9. typed_ffmpeg/dag/factory.py +31 -0
  10. typed_ffmpeg/dag/global_runnable/__init__.py +0 -0
  11. typed_ffmpeg/dag/global_runnable/global_args.py +178 -0
  12. typed_ffmpeg/dag/global_runnable/runnable.py +174 -0
  13. typed_ffmpeg/dag/io/__init__.py +0 -0
  14. typed_ffmpeg/dag/io/_input.py +197 -0
  15. typed_ffmpeg/dag/io/_output.py +320 -0
  16. typed_ffmpeg/dag/io/output_args.py +327 -0
  17. typed_ffmpeg/dag/nodes.py +479 -0
  18. typed_ffmpeg/dag/schema.py +210 -0
  19. typed_ffmpeg/dag/utils.py +41 -0
  20. typed_ffmpeg/dag/validate.py +172 -0
  21. typed_ffmpeg/exceptions.py +42 -0
  22. typed_ffmpeg/filters.py +3572 -0
  23. typed_ffmpeg/probe.py +43 -0
  24. typed_ffmpeg/py.typed +0 -0
  25. typed_ffmpeg/schema.py +29 -0
  26. typed_ffmpeg/streams/__init__.py +5 -0
  27. typed_ffmpeg/streams/audio.py +7358 -0
  28. typed_ffmpeg/streams/av.py +22 -0
  29. typed_ffmpeg/streams/channel_layout.py +39 -0
  30. typed_ffmpeg/streams/video.py +13469 -0
  31. typed_ffmpeg/types.py +119 -0
  32. typed_ffmpeg/utils/__init__.py +0 -0
  33. typed_ffmpeg/utils/escaping.py +49 -0
  34. typed_ffmpeg/utils/lazy_eval/__init__.py +0 -0
  35. typed_ffmpeg/utils/lazy_eval/operator.py +134 -0
  36. typed_ffmpeg/utils/lazy_eval/schema.py +211 -0
  37. typed_ffmpeg/utils/run.py +27 -0
  38. typed_ffmpeg/utils/snapshot.py +26 -0
  39. typed_ffmpeg/utils/typing.py +17 -0
  40. typed_ffmpeg/utils/view.py +64 -0
  41. typed_ffmpeg_compatible-2.4.1.dist-info/LICENSE +21 -0
  42. typed_ffmpeg_compatible-2.4.1.dist-info/METADATA +182 -0
  43. typed_ffmpeg_compatible-2.4.1.dist-info/RECORD +45 -0
  44. typed_ffmpeg_compatible-2.4.1.dist-info/WHEEL +4 -0
  45. typed_ffmpeg_compatible-2.4.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,479 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os.path
5
+ from dataclasses import dataclass, replace
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from ..exceptions import FFMpegTypeError, FFMpegValueError
10
+ from ..schema import Default, StreamType
11
+ from ..utils.escaping import escape
12
+ from ..utils.lazy_eval.schema import LazyValue
13
+ from ..utils.typing import override
14
+ from .global_runnable.runnable import GlobalRunable
15
+ from .io.output_args import OutputArgs
16
+ from .schema import Node, Stream
17
+
18
+ if TYPE_CHECKING:
19
+ from ..streams.audio import AudioStream
20
+ from ..streams.av import AVStream
21
+ from ..streams.video import VideoStream
22
+ from .context import DAGContext
23
+
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ @dataclass(frozen=True, kw_only=True)
29
+ class FilterNode(Node):
30
+ """
31
+ A filter node that can be used to apply filters to streams
32
+ """
33
+
34
+ name: str
35
+ """
36
+ The name of the filter
37
+ """
38
+
39
+ inputs: tuple[FilterableStream, ...] = ()
40
+ """
41
+ The input streams
42
+ """
43
+
44
+ input_typings: tuple[StreamType, ...] = ()
45
+ """
46
+ The input typings
47
+ """
48
+
49
+ output_typings: tuple[StreamType, ...] = ()
50
+ """
51
+ The output typings
52
+ """
53
+
54
+ @override
55
+ def repr(self) -> str:
56
+ return self.name
57
+
58
+ def video(self, index: int) -> "VideoStream":
59
+ """
60
+ Return the video stream at the specified index
61
+
62
+ Args:
63
+ index: the index of the video stream
64
+
65
+ Returns:
66
+ the video stream at the specified index
67
+ """
68
+ from ..streams.video import VideoStream
69
+
70
+ video_outputs = [i for i, k in enumerate(self.output_typings) if k == StreamType.video]
71
+ if not len(video_outputs) > index:
72
+ raise FFMpegValueError(f"Specified index {index} is out of range for video outputs {len(video_outputs)}")
73
+ return VideoStream(node=self, index=video_outputs[index])
74
+
75
+ def audio(self, index: int) -> "AudioStream":
76
+ """
77
+ Return the audio stream at the specified index
78
+
79
+ Args:
80
+ index: the index of the audio stream
81
+
82
+ Returns:
83
+ the audio stream at the specified index
84
+ """
85
+ from ..streams.audio import AudioStream
86
+
87
+ audio_outputs = [i for i, k in enumerate(self.output_typings) if k == StreamType.audio]
88
+ if not len(audio_outputs) > index:
89
+ raise FFMpegValueError(f"Specified index {index} is out of range for audio outputs {len(audio_outputs)}")
90
+
91
+ return AudioStream(node=self, index=audio_outputs[index])
92
+
93
+ def __post_init__(self) -> None:
94
+ from ..streams.audio import AudioStream
95
+ from ..streams.video import VideoStream
96
+
97
+ super().__post_init__()
98
+
99
+ if len(self.inputs) != len(self.input_typings):
100
+ raise FFMpegValueError(f"Expected {len(self.input_typings)} inputs, got {len(self.inputs)}")
101
+
102
+ stream: FilterableStream
103
+ expected_type: StreamType
104
+
105
+ for i, (stream, expected_type) in enumerate(zip(self.inputs, self.input_typings)):
106
+ if expected_type == StreamType.video:
107
+ if not isinstance(stream, VideoStream):
108
+ raise FFMpegTypeError(
109
+ f"Expected input {i} to have video component, got {stream.__class__.__name__}"
110
+ )
111
+ if expected_type == StreamType.audio:
112
+ if not isinstance(stream, AudioStream):
113
+ raise FFMpegTypeError(
114
+ f"Expected input {i} to have audio component, got {stream.__class__.__name__}"
115
+ )
116
+
117
+ @override
118
+ def get_args(self, context: DAGContext = None) -> list[str]:
119
+ from .context import DAGContext
120
+
121
+ if not context:
122
+ context = DAGContext.build(self)
123
+
124
+ incoming_labels = "".join(f"[{k.label(context)}]" for k in self.inputs)
125
+ outputs = context.get_outgoing_streams(self)
126
+
127
+ outgoing_labels = ""
128
+ for output in sorted(outputs, key=lambda stream: stream.index or 0):
129
+ # NOTE: all outgoing streams must be filterable
130
+ assert isinstance(output, FilterableStream)
131
+ outgoing_labels += f"[{output.label(context)}]"
132
+
133
+ commands = []
134
+ for key, value in self.kwargs:
135
+ assert not isinstance(value, LazyValue), f"LazyValue should have been evaluated: {key}={value}"
136
+
137
+ # Note: the -nooption syntax cannot be used for boolean AVOptions, use -option 0/-option 1.
138
+ if isinstance(value, bool):
139
+ value = str(int(value))
140
+
141
+ if not isinstance(value, Default):
142
+ commands += [f"{key}={escape(value)}"]
143
+
144
+ if commands:
145
+ return [incoming_labels] + [f"{self.name}="] + [escape(":".join(commands), "\\'[],;")] + [outgoing_labels]
146
+ return [incoming_labels] + [f"{self.name}"] + [outgoing_labels]
147
+
148
+
149
+ @dataclass(frozen=True, kw_only=True)
150
+ class FilterableStream(Stream, OutputArgs):
151
+ """
152
+ A stream that can be used as input to a filter
153
+ """
154
+
155
+ node: "FilterNode | InputNode"
156
+
157
+ @override
158
+ def _output_node(self, *streams: FilterableStream, filename: str | Path, **kwargs: Any) -> OutputNode:
159
+ """
160
+ Output the streams to a file URL
161
+
162
+ Args:
163
+ *streams: the other streams to output
164
+ filename: the filename to output to
165
+ **kwargs: the arguments for the output
166
+
167
+ Returns:
168
+ the output stream
169
+ """
170
+ return OutputNode(inputs=(self, *streams), filename=str(filename), kwargs=tuple(kwargs.items()))
171
+
172
+ def vfilter(
173
+ self,
174
+ *streams: "FilterableStream",
175
+ name: str,
176
+ input_typings: tuple[StreamType, ...] = (StreamType.video,),
177
+ **kwargs: Any,
178
+ ) -> "VideoStream":
179
+ """
180
+ Apply a custom video filter which has only one output to this stream
181
+
182
+ Args:
183
+ *streams (FilterableStream): the streams to apply the filter to
184
+ name: the name of the filter
185
+ input_typings: the input typings
186
+ **kwargs: the arguments for the filter
187
+
188
+ Returns:
189
+ the output stream
190
+ """
191
+ return self.filter_multi_output(
192
+ *streams,
193
+ name=name,
194
+ input_typings=input_typings,
195
+ output_typings=(StreamType.video,),
196
+ **kwargs,
197
+ ).video(0)
198
+
199
+ def afilter(
200
+ self,
201
+ *streams: "FilterableStream",
202
+ name: str,
203
+ input_typings: tuple[StreamType, ...] = (StreamType.audio,),
204
+ **kwargs: Any,
205
+ ) -> "AudioStream":
206
+ """
207
+ Apply a custom audio filter which has only one output to this stream
208
+
209
+ Args:
210
+ *streams (FilterableStream): the streams to apply the filter to
211
+ name: the name of the filter
212
+ input_typings: the input typings
213
+ **kwargs: the arguments for the filter
214
+
215
+ Returns:
216
+ the output stream
217
+ """
218
+ return self.filter_multi_output(
219
+ *streams,
220
+ name=name,
221
+ input_typings=input_typings,
222
+ output_typings=(StreamType.audio,),
223
+ **kwargs,
224
+ ).audio(0)
225
+
226
+ def filter_multi_output(
227
+ self,
228
+ *streams: "FilterableStream",
229
+ name: str,
230
+ input_typings: tuple[StreamType, ...] = (),
231
+ output_typings: tuple[StreamType, ...] = (),
232
+ **kwargs: Any,
233
+ ) -> "FilterNode":
234
+ """
235
+ Apply a custom filter which has multiple outputs to this stream
236
+
237
+ Args:
238
+ *streams (FilterableStream): the streams to apply the filter to
239
+ name: the name of the filter
240
+ input_typings: the input typings
241
+ output_typings: the output typings
242
+ **kwargs: the arguments for the filter
243
+
244
+ Returns:
245
+ the FilterNode
246
+ """
247
+ return FilterNode(
248
+ name=name,
249
+ kwargs=tuple(kwargs.items()),
250
+ inputs=(self, *streams),
251
+ input_typings=input_typings,
252
+ output_typings=output_typings,
253
+ )
254
+
255
+ def label(self, context: DAGContext = None) -> str:
256
+ """
257
+ Return the label for this stream
258
+
259
+ Args:
260
+ context: the DAG context
261
+
262
+ Returns:
263
+ the label for this stream
264
+ """
265
+ from ..streams.audio import AudioStream
266
+ from ..streams.av import AVStream
267
+ from ..streams.video import VideoStream
268
+ from .context import DAGContext
269
+
270
+ if not context:
271
+ context = DAGContext.build(self.node)
272
+
273
+ if isinstance(self.node, InputNode):
274
+ if isinstance(self, AVStream):
275
+ return f"{context.get_node_label(self.node)}"
276
+ elif isinstance(self, VideoStream):
277
+ return f"{context.get_node_label(self.node)}:v"
278
+ elif isinstance(self, AudioStream):
279
+ return f"{context.get_node_label(self.node)}:a"
280
+ raise FFMpegValueError(f"Unknown stream type: {self.__class__.__name__}") # pragma: no cover
281
+
282
+ if isinstance(self.node, FilterNode):
283
+ if len(self.node.output_typings) > 1:
284
+ return f"{context.get_node_label(self.node)}#{self.index}"
285
+ return f"{context.get_node_label(self.node)}"
286
+ raise FFMpegValueError(f"Unknown node type: {self.node.__class__.__name__}") # pragma: no cover
287
+
288
+ def __post_init__(self) -> None:
289
+ if isinstance(self.node, InputNode):
290
+ assert self.index is None, "Input streams cannot have an index"
291
+ else:
292
+ assert self.index is not None, "Filter streams must have an index"
293
+
294
+
295
+ @dataclass(frozen=True, kw_only=True)
296
+ class InputNode(Node):
297
+ """
298
+ A node that can be used to read from files
299
+ """
300
+
301
+ filename: str
302
+ """
303
+ The filename to read from
304
+ """
305
+
306
+ inputs: tuple[()] = ()
307
+
308
+ @override
309
+ def repr(self) -> str:
310
+ return os.path.basename(self.filename)
311
+
312
+ @property
313
+ def video(self) -> "VideoStream":
314
+ """
315
+ Return the video stream of this node
316
+
317
+ Returns:
318
+ the video stream
319
+ """
320
+ from ..streams.video import VideoStream
321
+
322
+ return VideoStream(node=self)
323
+
324
+ @property
325
+ def audio(self) -> "AudioStream":
326
+ """
327
+ Return the audio stream of this node
328
+
329
+ Returns:
330
+ the audio stream
331
+ """
332
+ from ..streams.audio import AudioStream
333
+
334
+ return AudioStream(node=self)
335
+
336
+ def stream(self) -> "AVStream":
337
+ """
338
+ Return the output stream of this node
339
+
340
+ Returns:
341
+ the output stream
342
+ """
343
+ from ..streams.av import AVStream
344
+
345
+ return AVStream(node=self)
346
+
347
+ @override
348
+ def get_args(self, context: DAGContext = None) -> list[str]:
349
+ commands = []
350
+ for key, value in self.kwargs:
351
+ if isinstance(value, bool):
352
+ if value is True:
353
+ commands += [f"-{key}"]
354
+ # NOTE: the -nooption is not supported
355
+ else:
356
+ commands += [f"-{key}", str(value)]
357
+ commands += ["-i", self.filename]
358
+ return commands
359
+
360
+
361
+ @dataclass(frozen=True, kw_only=True)
362
+ class OutputNode(Node):
363
+ filename: str
364
+ """
365
+ The filename to output to
366
+ """
367
+ inputs: tuple[FilterableStream, ...]
368
+
369
+ @override
370
+ def repr(self) -> str:
371
+ return os.path.basename(self.filename)
372
+
373
+ def stream(self) -> "OutputStream":
374
+ """
375
+ Return the output stream of this node
376
+
377
+ Returns:
378
+ the output stream
379
+ """
380
+ return OutputStream(node=self)
381
+
382
+ @override
383
+ def get_args(self, context: DAGContext = None) -> list[str]:
384
+ # !handle mapping
385
+ commands = []
386
+
387
+ if context and (
388
+ any(isinstance(k.node, FilterNode) for k in self.inputs)
389
+ or len([k for k in context.all_nodes if isinstance(k, OutputNode)]) > 1
390
+ ):
391
+ for input in self.inputs:
392
+ if isinstance(input.node, InputNode):
393
+ commands += ["-map", input.label(context)]
394
+ else:
395
+ commands += ["-map", f"[{input.label(context)}]"]
396
+
397
+ for key, value in self.kwargs:
398
+ if isinstance(value, bool):
399
+ if value is True:
400
+ commands += [f"-{key}"]
401
+ # NOTE: the -nooption is not supported
402
+ else:
403
+ commands += [f"-{key}", str(value)]
404
+ commands += [self.filename]
405
+ return commands
406
+
407
+
408
+ @dataclass(frozen=True, kw_only=True)
409
+ class OutputStream(Stream, GlobalRunable):
410
+ node: OutputNode
411
+
412
+ @override
413
+ def _global_node(self, *streams: OutputStream, **kwargs: Any) -> GlobalNode:
414
+ """
415
+ Add extra global command-line argument
416
+
417
+ Args:
418
+ **kwargs: the extra arguments
419
+
420
+ Returns:
421
+ the output stream
422
+ """
423
+ return GlobalNode(inputs=(self, *streams), kwargs=tuple(kwargs.items()))
424
+
425
+
426
+ @dataclass(frozen=True, kw_only=True)
427
+ class GlobalNode(Node):
428
+ """
429
+ A node that can be used to set global options
430
+ """
431
+
432
+ inputs: tuple[OutputStream, ...]
433
+
434
+ @override
435
+ def repr(self) -> str:
436
+ return " ".join(self.get_args())
437
+
438
+ def stream(self) -> "GlobalStream":
439
+ """
440
+ Return the output stream of this node
441
+
442
+ Returns:
443
+ the output stream
444
+ """
445
+ return GlobalStream(node=self)
446
+
447
+ @override
448
+ def get_args(self, context: DAGContext = None) -> list[str]:
449
+ commands = []
450
+ for key, value in self.kwargs:
451
+ if isinstance(value, bool):
452
+ if value is True:
453
+ commands += [f"-{key}"]
454
+ # NOTE: the -no{key} format since not really for global options
455
+ else:
456
+ commands += [f"-{key}", str(value)]
457
+ return commands
458
+
459
+
460
+ @dataclass(frozen=True, kw_only=True)
461
+ class GlobalStream(Stream, GlobalRunable):
462
+ node: GlobalNode
463
+
464
+ @override
465
+ def _global_node(self, *streams: OutputStream, **kwargs: Any) -> GlobalNode:
466
+ """
467
+ Add extra global command-line argument
468
+
469
+ Args:
470
+ **kwargs: the extra arguments
471
+
472
+ Returns:
473
+ the output stream
474
+ """
475
+ inputs = (*self.node.inputs, *streams)
476
+ kwargs = dict(self.node.kwargs) | kwargs
477
+
478
+ new_node = replace(self.node, inputs=inputs, kwargs=tuple(kwargs.items()))
479
+ return new_node
@@ -0,0 +1,210 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, replace
5
+ from functools import cached_property
6
+ from typing import TYPE_CHECKING, Literal
7
+
8
+ from ..utils.lazy_eval.schema import LazyValue
9
+ from .utils import is_dag
10
+
11
+ if TYPE_CHECKING:
12
+ from .context import DAGContext
13
+
14
+
15
+ @dataclass(frozen=True, kw_only=True)
16
+ class HashableBaseModel:
17
+ """
18
+ A base class for hashable dataclasses.
19
+ """
20
+
21
+ @cached_property
22
+ def hex(self) -> str:
23
+ """
24
+ Get the hexadecimal hash of the object.
25
+ """
26
+ return hex(abs(hash(self)))[2:]
27
+
28
+
29
+ @dataclass(frozen=True, kw_only=True)
30
+ class Stream(HashableBaseModel):
31
+ """
32
+ A 'Stream' represents a sequence of data flow in the Directed Acyclic Graph (DAG).
33
+
34
+ Note:
35
+ Each stream in the DAG is a sequence of operations that transforms the data from its input form to its output form. The stream is an essential component of the DAG, as it defines the order and the nature of the operations that are performed on the data.
36
+ """
37
+
38
+ node: Node
39
+ """
40
+ Represents the node that the stream is connected to in the upstream direction.
41
+
42
+ Note:
43
+ In the context of a data stream, the 'upstream' refers to the source of the data, or where the data is coming from. Therefore, the 'upstream node' is the node that is providing the data to the current stream.
44
+ """
45
+
46
+ index: int | None = None # the nth child of the node
47
+ """
48
+ Represents the index of the stream in the node's output streams.
49
+
50
+ Note:
51
+ See Also: [Stream specifiers](https://ffmpeg.org/ffmpeg.html#Stream-specifiers-1) `stream_index`
52
+ """
53
+
54
+ def view(self, format: Literal["png", "svg", "dot"] = "png") -> str:
55
+ """
56
+ Visualize the stream.
57
+
58
+ Args:
59
+ format: The format of the view.
60
+
61
+ Returns:
62
+ The file path of the visualization.
63
+ """
64
+ from ..utils.view import view
65
+
66
+ return view(self.node, format=format)
67
+
68
+ def _repr_png_(self) -> bytes: # pragma: no cover
69
+ with open(self.view(format="png"), "rb") as f:
70
+ return f.read()
71
+
72
+ def _repr_svg_(self) -> str: # pragma: no cover
73
+ with open(self.view(format="svg"), "r") as f:
74
+ return f.read()
75
+
76
+
77
+ @dataclass(frozen=True, kw_only=True)
78
+ class Node(HashableBaseModel, ABC):
79
+ """
80
+ A 'Node' represents a single operation in the Directed Acyclic Graph (DAG).
81
+
82
+ Note:
83
+ Each node in the DAG represents a single operation that transforms the data from its input form to its output form. The node is an essential component of the DAG, as it defines the nature of the operations that are performed on the data.
84
+ """
85
+
86
+ # Filter_Node_Option_Type
87
+ kwargs: tuple[tuple[str, str | int | float | bool | LazyValue], ...] = ()
88
+ """
89
+ Represents the keyword arguments of the node.
90
+ """
91
+
92
+ inputs: tuple[Stream, ...] = ()
93
+ """
94
+ Represents the input streams of the node.
95
+ """
96
+
97
+ def __post_init__(self) -> None:
98
+ # Validate the DAG
99
+ passed = set()
100
+ nodes = [self]
101
+ output = {}
102
+
103
+ while nodes:
104
+ node = nodes.pop()
105
+
106
+ if node in passed:
107
+ continue
108
+ passed.add(node)
109
+
110
+ nodes.extend(k.node for k in node.inputs)
111
+
112
+ output[node.hex] = set(k.node.hex for k in node.inputs)
113
+
114
+ if not is_dag(output):
115
+ raise ValueError(f"Graph is not a DAG: {output}") # pragma: no cover
116
+
117
+ @abstractmethod
118
+ def get_args(self, context: DAGContext = None) -> list[str]:
119
+ """
120
+ Get the arguments of the node.
121
+
122
+ Args:
123
+ context: The DAG context.
124
+
125
+ Returns:
126
+ The arguments of the node.
127
+ """
128
+
129
+ def repr(self) -> str:
130
+ """
131
+ Get the representation of the node.
132
+
133
+ Returns:
134
+ The representation of the node.
135
+ """
136
+ return repr(self)
137
+
138
+ def replace(self, old_node: Node, new_node: Node) -> Node:
139
+ """
140
+ Replace the old node in the graph with the new node.
141
+
142
+ Args:
143
+ old_node: The old node to replace.
144
+ new_node: The new node to replace with.
145
+
146
+ Returns:
147
+ The new graph with the replaced node.
148
+ """
149
+ if self == old_node:
150
+ return new_node
151
+
152
+ inputs = []
153
+
154
+ for stream in self.inputs:
155
+ new_stream_node = stream.node.replace(old_node, new_node)
156
+
157
+ if new_stream_node != stream.node:
158
+ # need to create a new stream
159
+ new_stream = replace(stream, node=new_stream_node)
160
+ inputs.append(new_stream)
161
+ else:
162
+ inputs.append(stream)
163
+
164
+ return replace(self, inputs=tuple(inputs))
165
+
166
+ @property
167
+ def max_depth(self) -> int:
168
+ """
169
+ Get the maximum depth of the node.
170
+
171
+ Returns:
172
+ The maximum depth of the node.
173
+ """
174
+ return max((i.node.max_depth for i in self.inputs), default=0) + 1
175
+
176
+ @property
177
+ def upstream_nodes(self) -> set[Node]:
178
+ """
179
+ Get all upstream nodes of the node.
180
+
181
+ Returns:
182
+ The upstream nodes of the node.
183
+ """
184
+ output = {self}
185
+ for input in self.inputs:
186
+ output |= input.node.upstream_nodes
187
+
188
+ return output
189
+
190
+ def view(self, format: Literal["png", "svg", "dot"] = "png") -> str:
191
+ """
192
+ Visualize the Node.
193
+
194
+ Args:
195
+ format: The format of the view.
196
+
197
+ Returns:
198
+ The file path of the visualization.
199
+ """
200
+ from ..utils.view import view
201
+
202
+ return view(self, format=format)
203
+
204
+ def _repr_png_(self) -> bytes: # pragma: no cover
205
+ with open(self.view(format="png"), "rb") as f:
206
+ return f.read()
207
+
208
+ def _repr_svg_(self) -> str: # pragma: no cover
209
+ with open(self.view(format="svg"), "r") as f:
210
+ return f.read()