prefect-client 3.1.5__py3-none-any.whl → 3.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. prefect/__init__.py +3 -0
  2. prefect/_experimental/__init__.py +0 -0
  3. prefect/_experimental/lineage.py +181 -0
  4. prefect/_internal/compatibility/async_dispatch.py +38 -9
  5. prefect/_internal/compatibility/migration.py +1 -1
  6. prefect/_internal/concurrency/api.py +52 -52
  7. prefect/_internal/concurrency/calls.py +59 -35
  8. prefect/_internal/concurrency/cancellation.py +34 -18
  9. prefect/_internal/concurrency/event_loop.py +7 -6
  10. prefect/_internal/concurrency/threads.py +41 -33
  11. prefect/_internal/concurrency/waiters.py +28 -21
  12. prefect/_internal/pydantic/v1_schema.py +2 -2
  13. prefect/_internal/pydantic/v2_schema.py +10 -9
  14. prefect/_internal/pydantic/v2_validated_func.py +15 -10
  15. prefect/_internal/retries.py +15 -6
  16. prefect/_internal/schemas/bases.py +11 -8
  17. prefect/_internal/schemas/validators.py +7 -5
  18. prefect/_version.py +3 -3
  19. prefect/automations.py +53 -47
  20. prefect/blocks/abstract.py +12 -10
  21. prefect/blocks/core.py +148 -19
  22. prefect/blocks/system.py +2 -1
  23. prefect/cache_policies.py +11 -11
  24. prefect/client/__init__.py +3 -1
  25. prefect/client/base.py +36 -37
  26. prefect/client/cloud.py +26 -19
  27. prefect/client/collections.py +2 -2
  28. prefect/client/orchestration.py +430 -273
  29. prefect/client/schemas/__init__.py +24 -0
  30. prefect/client/schemas/actions.py +128 -121
  31. prefect/client/schemas/filters.py +1 -1
  32. prefect/client/schemas/objects.py +114 -85
  33. prefect/client/schemas/responses.py +19 -20
  34. prefect/client/schemas/schedules.py +136 -93
  35. prefect/client/subscriptions.py +30 -15
  36. prefect/client/utilities.py +46 -36
  37. prefect/concurrency/asyncio.py +6 -9
  38. prefect/concurrency/sync.py +35 -5
  39. prefect/context.py +40 -32
  40. prefect/deployments/flow_runs.py +6 -8
  41. prefect/deployments/runner.py +14 -14
  42. prefect/deployments/steps/core.py +3 -1
  43. prefect/deployments/steps/pull.py +60 -12
  44. prefect/docker/__init__.py +1 -1
  45. prefect/events/clients.py +55 -4
  46. prefect/events/filters.py +1 -1
  47. prefect/events/related.py +2 -1
  48. prefect/events/schemas/events.py +26 -21
  49. prefect/events/utilities.py +3 -2
  50. prefect/events/worker.py +8 -0
  51. prefect/filesystems.py +3 -3
  52. prefect/flow_engine.py +87 -87
  53. prefect/flow_runs.py +7 -5
  54. prefect/flows.py +218 -176
  55. prefect/logging/configuration.py +1 -1
  56. prefect/logging/highlighters.py +1 -2
  57. prefect/logging/loggers.py +30 -20
  58. prefect/main.py +17 -24
  59. prefect/results.py +43 -22
  60. prefect/runner/runner.py +43 -21
  61. prefect/runner/server.py +30 -32
  62. prefect/runner/storage.py +3 -3
  63. prefect/runner/submit.py +3 -6
  64. prefect/runner/utils.py +6 -6
  65. prefect/runtime/flow_run.py +7 -0
  66. prefect/serializers.py +28 -24
  67. prefect/settings/constants.py +2 -2
  68. prefect/settings/legacy.py +1 -1
  69. prefect/settings/models/experiments.py +5 -0
  70. prefect/settings/models/server/events.py +10 -0
  71. prefect/task_engine.py +87 -26
  72. prefect/task_runners.py +2 -2
  73. prefect/task_worker.py +43 -25
  74. prefect/tasks.py +148 -142
  75. prefect/telemetry/bootstrap.py +15 -2
  76. prefect/telemetry/instrumentation.py +1 -1
  77. prefect/telemetry/processors.py +10 -7
  78. prefect/telemetry/run_telemetry.py +231 -0
  79. prefect/transactions.py +14 -14
  80. prefect/types/__init__.py +5 -5
  81. prefect/utilities/_engine.py +96 -0
  82. prefect/utilities/annotations.py +25 -18
  83. prefect/utilities/asyncutils.py +126 -140
  84. prefect/utilities/callables.py +87 -78
  85. prefect/utilities/collections.py +278 -117
  86. prefect/utilities/compat.py +13 -21
  87. prefect/utilities/context.py +6 -5
  88. prefect/utilities/dispatch.py +23 -12
  89. prefect/utilities/dockerutils.py +33 -32
  90. prefect/utilities/engine.py +126 -239
  91. prefect/utilities/filesystem.py +18 -15
  92. prefect/utilities/hashing.py +10 -11
  93. prefect/utilities/importtools.py +40 -27
  94. prefect/utilities/math.py +9 -5
  95. prefect/utilities/names.py +3 -3
  96. prefect/utilities/processutils.py +121 -57
  97. prefect/utilities/pydantic.py +41 -36
  98. prefect/utilities/render_swagger.py +22 -12
  99. prefect/utilities/schema_tools/__init__.py +2 -1
  100. prefect/utilities/schema_tools/hydration.py +50 -43
  101. prefect/utilities/schema_tools/validation.py +52 -42
  102. prefect/utilities/services.py +13 -12
  103. prefect/utilities/templating.py +45 -45
  104. prefect/utilities/text.py +2 -1
  105. prefect/utilities/timeout.py +4 -4
  106. prefect/utilities/urls.py +9 -4
  107. prefect/utilities/visualization.py +46 -24
  108. prefect/variables.py +136 -27
  109. prefect/workers/base.py +15 -8
  110. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/METADATA +5 -2
  111. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/RECORD +114 -110
  112. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/LICENSE +0 -0
  113. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/WHEEL +0 -0
  114. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,7 @@
1
1
  import enum
2
2
  import os
3
3
  import re
4
- from typing import (
5
- TYPE_CHECKING,
6
- Any,
7
- Dict,
8
- NamedTuple,
9
- Optional,
10
- Set,
11
- Type,
12
- TypeVar,
13
- Union,
14
- )
4
+ from typing import TYPE_CHECKING, Any, NamedTuple, Optional, TypeVar, Union, cast
15
5
 
16
6
  from prefect.client.utilities import inject_client
17
7
  from prefect.utilities.annotations import NotSet
@@ -21,7 +11,7 @@ if TYPE_CHECKING:
21
11
  from prefect.client.orchestration import PrefectClient
22
12
 
23
13
 
24
- T = TypeVar("T", str, int, float, bool, dict, list, None)
14
+ T = TypeVar("T", str, int, float, bool, dict[Any, Any], list[Any], None)
25
15
 
26
16
  PLACEHOLDER_CAPTURE_REGEX = re.compile(r"({{\s*([\w\.\-\[\]$]+)\s*}})")
27
17
  BLOCK_DOCUMENT_PLACEHOLDER_PREFIX = "prefect.blocks."
@@ -62,7 +52,7 @@ def determine_placeholder_type(name: str) -> PlaceholderType:
62
52
  return PlaceholderType.STANDARD
63
53
 
64
54
 
65
- def find_placeholders(template: T) -> Set[Placeholder]:
55
+ def find_placeholders(template: T) -> set[Placeholder]:
66
56
  """
67
57
  Finds all placeholders in a template.
68
58
 
@@ -72,8 +62,9 @@ def find_placeholders(template: T) -> Set[Placeholder]:
72
62
  Returns:
73
63
  A set of all placeholders in the template
74
64
  """
65
+ seed: set[Placeholder] = set()
75
66
  if isinstance(template, (int, float, bool)):
76
- return set()
67
+ return seed
77
68
  if isinstance(template, str):
78
69
  result = PLACEHOLDER_CAPTURE_REGEX.findall(template)
79
70
  return {
@@ -81,18 +72,16 @@ def find_placeholders(template: T) -> Set[Placeholder]:
81
72
  for full_match, name in result
82
73
  }
83
74
  elif isinstance(template, dict):
84
- return set().union(
85
- *[find_placeholders(value) for key, value in template.items()]
86
- )
75
+ return seed.union(*[find_placeholders(value) for value in template.values()])
87
76
  elif isinstance(template, list):
88
- return set().union(*[find_placeholders(item) for item in template])
77
+ return seed.union(*[find_placeholders(item) for item in template])
89
78
  else:
90
79
  raise ValueError(f"Unexpected type: {type(template)}")
91
80
 
92
81
 
93
82
  def apply_values(
94
- template: T, values: Dict[str, Any], remove_notset: bool = True
95
- ) -> Union[T, Type[NotSet]]:
83
+ template: T, values: dict[str, Any], remove_notset: bool = True
84
+ ) -> Union[T, type[NotSet]]:
96
85
  """
97
86
  Replaces placeholders in a template with values from a supplied dictionary.
98
87
 
@@ -120,7 +109,7 @@ def apply_values(
120
109
  Returns:
121
110
  The template with the values applied
122
111
  """
123
- if isinstance(template, (int, float, bool, type(NotSet), type(None))):
112
+ if template in (NotSet, None) or isinstance(template, (int, float)):
124
113
  return template
125
114
  if isinstance(template, str):
126
115
  placeholders = find_placeholders(template)
@@ -155,7 +144,7 @@ def apply_values(
155
144
 
156
145
  return template
157
146
  elif isinstance(template, dict):
158
- updated_template = {}
147
+ updated_template: dict[str, Any] = {}
159
148
  for key, value in template.items():
160
149
  updated_value = apply_values(value, values, remove_notset=remove_notset)
161
150
  if updated_value is not NotSet:
@@ -163,22 +152,22 @@ def apply_values(
163
152
  elif not remove_notset:
164
153
  updated_template[key] = value
165
154
 
166
- return updated_template
155
+ return cast(T, updated_template)
167
156
  elif isinstance(template, list):
168
- updated_list = []
157
+ updated_list: list[Any] = []
169
158
  for value in template:
170
159
  updated_value = apply_values(value, values, remove_notset=remove_notset)
171
160
  if updated_value is not NotSet:
172
161
  updated_list.append(updated_value)
173
- return updated_list
162
+ return cast(T, updated_list)
174
163
  else:
175
164
  raise ValueError(f"Unexpected template type {type(template).__name__!r}")
176
165
 
177
166
 
178
167
  @inject_client
179
168
  async def resolve_block_document_references(
180
- template: T, client: "PrefectClient" = None
181
- ) -> Union[T, Dict[str, Any]]:
169
+ template: T, client: Optional["PrefectClient"] = None
170
+ ) -> Union[T, dict[str, Any]]:
182
171
  """
183
172
  Resolve block document references in a template by replacing each reference with
184
173
  the data of the block document.
@@ -242,12 +231,17 @@ async def resolve_block_document_references(
242
231
  Returns:
243
232
  The template with block documents resolved
244
233
  """
234
+ if TYPE_CHECKING:
235
+ # The @inject_client decorator takes care of providing the client, but
236
+ # the function signature must mark it as optional to callers.
237
+ assert client is not None
238
+
245
239
  if isinstance(template, dict):
246
240
  block_document_id = template.get("$ref", {}).get("block_document_id")
247
241
  if block_document_id:
248
242
  block_document = await client.read_block_document(block_document_id)
249
243
  return block_document.data
250
- updated_template = {}
244
+ updated_template: dict[str, Any] = {}
251
245
  for key, value in template.items():
252
246
  updated_value = await resolve_block_document_references(
253
247
  value, client=client
@@ -265,7 +259,7 @@ async def resolve_block_document_references(
265
259
  placeholder.type is PlaceholderType.BLOCK_DOCUMENT
266
260
  for placeholder in placeholders
267
261
  )
268
- if len(placeholders) == 0 or not has_block_document_placeholder:
262
+ if not (placeholders and has_block_document_placeholder):
269
263
  return template
270
264
  elif (
271
265
  len(placeholders) == 1
@@ -274,31 +268,32 @@ async def resolve_block_document_references(
274
268
  ):
275
269
  # value_keypath will be a list containing a dot path if additional
276
270
  # attributes are accessed and an empty list otherwise.
277
- block_type_slug, block_document_name, *value_keypath = (
278
- list(placeholders)[0]
279
- .name.replace(BLOCK_DOCUMENT_PLACEHOLDER_PREFIX, "")
280
- .split(".", 2)
281
- )
271
+ [placeholder] = placeholders
272
+ parts = placeholder.name.replace(
273
+ BLOCK_DOCUMENT_PLACEHOLDER_PREFIX, ""
274
+ ).split(".", 2)
275
+ block_type_slug, block_document_name, *value_keypath = parts
282
276
  block_document = await client.read_block_document_by_name(
283
277
  name=block_document_name, block_type_slug=block_type_slug
284
278
  )
285
- value = block_document.data
279
+ data = block_document.data
280
+ value: Union[T, dict[str, Any]] = data
286
281
 
287
282
  # resolving system blocks to their data for backwards compatibility
288
- if len(value) == 1 and "value" in value:
283
+ if len(data) == 1 and "value" in data:
289
284
  # only resolve the value if the keypath is not already pointing to "value"
290
- if len(value_keypath) == 0 or value_keypath[0][:5] != "value":
291
- value = value["value"]
285
+ if not (value_keypath and value_keypath[0].startswith("value")):
286
+ data = value = value["value"]
292
287
 
293
288
  # resolving keypath/block attributes
294
- if len(value_keypath) > 0:
295
- value_keypath: str = value_keypath[0]
296
- value = get_from_dict(value, value_keypath, default=NotSet)
297
- if value is NotSet:
289
+ if value_keypath:
290
+ from_dict: Any = get_from_dict(data, value_keypath[0], default=NotSet)
291
+ if from_dict is NotSet:
298
292
  raise ValueError(
299
293
  f"Invalid template: {template!r}. Could not resolve the"
300
294
  " keypath in the block document data."
301
295
  )
296
+ value = from_dict
302
297
 
303
298
  return value
304
299
  else:
@@ -311,7 +306,7 @@ async def resolve_block_document_references(
311
306
 
312
307
 
313
308
  @inject_client
314
- async def resolve_variables(template: T, client: Optional["PrefectClient"] = None):
309
+ async def resolve_variables(template: T, client: Optional["PrefectClient"] = None) -> T:
315
310
  """
316
311
  Resolve variables in a template by replacing each variable placeholder with the
317
312
  value of the variable.
@@ -326,6 +321,11 @@ async def resolve_variables(template: T, client: Optional["PrefectClient"] = Non
326
321
  Returns:
327
322
  The template with variables resolved
328
323
  """
324
+ if TYPE_CHECKING:
325
+ # The @inject_client decorator takes care of providing the client, but
326
+ # the function signature must mark it as optional to callers.
327
+ assert client is not None
328
+
329
329
  if isinstance(template, str):
330
330
  placeholders = find_placeholders(template)
331
331
  has_variable_placeholder = any(
@@ -346,7 +346,7 @@ async def resolve_variables(template: T, client: Optional["PrefectClient"] = Non
346
346
  if variable is None:
347
347
  return ""
348
348
  else:
349
- return variable.value
349
+ return cast(T, variable.value)
350
350
  else:
351
351
  for full_match, name, placeholder_type in placeholders:
352
352
  if placeholder_type is PlaceholderType.VARIABLE:
@@ -355,7 +355,7 @@ async def resolve_variables(template: T, client: Optional["PrefectClient"] = Non
355
355
  if variable is None:
356
356
  template = template.replace(full_match, "")
357
357
  else:
358
- template = template.replace(full_match, variable.value)
358
+ template = template.replace(full_match, str(variable.value))
359
359
  return template
360
360
  elif isinstance(template, dict):
361
361
  return {
prefect/utilities/text.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import difflib
2
- from typing import Iterable, Optional
2
+ from collections.abc import Iterable
3
+ from typing import Optional
3
4
 
4
5
 
5
6
  def truncated_to(length: int, value: Optional[str]) -> str:
@@ -1,6 +1,6 @@
1
1
  from asyncio import CancelledError
2
2
  from contextlib import contextmanager
3
- from typing import Optional, Type
3
+ from typing import Optional
4
4
 
5
5
  from prefect._internal.concurrency.cancellation import (
6
6
  cancel_async_after,
@@ -8,7 +8,7 @@ from prefect._internal.concurrency.cancellation import (
8
8
  )
9
9
 
10
10
 
11
- def fail_if_not_timeout_error(timeout_exc_type: Type[Exception]) -> None:
11
+ def fail_if_not_timeout_error(timeout_exc_type: type[Exception]) -> None:
12
12
  if not issubclass(timeout_exc_type, TimeoutError):
13
13
  raise ValueError(
14
14
  "The `timeout_exc_type` argument must be a subclass of `TimeoutError`."
@@ -17,7 +17,7 @@ def fail_if_not_timeout_error(timeout_exc_type: Type[Exception]) -> None:
17
17
 
18
18
  @contextmanager
19
19
  def timeout_async(
20
- seconds: Optional[float] = None, timeout_exc_type: Type[TimeoutError] = TimeoutError
20
+ seconds: Optional[float] = None, timeout_exc_type: type[TimeoutError] = TimeoutError
21
21
  ):
22
22
  fail_if_not_timeout_error(timeout_exc_type)
23
23
 
@@ -34,7 +34,7 @@ def timeout_async(
34
34
 
35
35
  @contextmanager
36
36
  def timeout(
37
- seconds: Optional[float] = None, timeout_exc_type: Type[TimeoutError] = TimeoutError
37
+ seconds: Optional[float] = None, timeout_exc_type: type[TimeoutError] = TimeoutError
38
38
  ):
39
39
  fail_if_not_timeout_error(timeout_exc_type)
40
40
 
prefect/utilities/urls.py CHANGED
@@ -2,6 +2,7 @@ import inspect
2
2
  import ipaddress
3
3
  import socket
4
4
  import urllib.parse
5
+ from logging import Logger
5
6
  from string import Formatter
6
7
  from typing import TYPE_CHECKING, Any, Literal, Optional, Union
7
8
  from urllib.parse import urlparse
@@ -19,7 +20,7 @@ if TYPE_CHECKING:
19
20
  from prefect.futures import PrefectFuture
20
21
  from prefect.variables import Variable
21
22
 
22
- logger = get_logger("utilities.urls")
23
+ logger: Logger = get_logger("utilities.urls")
23
24
 
24
25
  # The following objects are excluded from UI URL generation because we lack a
25
26
  # directly-addressable URL:
@@ -64,7 +65,7 @@ URLType = Literal["ui", "api"]
64
65
  RUN_TYPES = {"flow-run", "task-run"}
65
66
 
66
67
 
67
- def validate_restricted_url(url: str):
68
+ def validate_restricted_url(url: str) -> None:
68
69
  """
69
70
  Validate that the provided URL is safe for outbound requests. This prevents
70
71
  attacks like SSRF (Server Side Request Forgery), where an attacker can make
@@ -123,7 +124,7 @@ def convert_class_to_name(obj: Any) -> str:
123
124
 
124
125
  def url_for(
125
126
  obj: Union[
126
- "PrefectFuture",
127
+ "PrefectFuture[Any]",
127
128
  "Block",
128
129
  "Variable",
129
130
  "Automation",
@@ -163,6 +164,7 @@ def url_for(
163
164
  url_for("flow-run", obj_id="123e4567-e89b-12d3-a456-426614174000")
164
165
  """
165
166
  from prefect.blocks.core import Block
167
+ from prefect.client.schemas.objects import WorkPool
166
168
  from prefect.events.schemas.automations import Automation
167
169
  from prefect.events.schemas.events import ReceivedEvent, Resource
168
170
  from prefect.futures import PrefectFuture
@@ -228,8 +230,10 @@ def url_for(
228
230
  elif name == "block":
229
231
  # Blocks are client-side objects whose API representation is a
230
232
  # BlockDocument.
231
- obj_id = obj._block_document_id
233
+ obj_id = getattr(obj, "_block_document_id")
232
234
  elif name in ("variable", "work-pool"):
235
+ if TYPE_CHECKING:
236
+ assert isinstance(obj, (Variable, WorkPool))
233
237
  obj_id = obj.name
234
238
  elif isinstance(obj, Resource):
235
239
  obj_id = obj.id.rpartition(".")[2]
@@ -244,6 +248,7 @@ def url_for(
244
248
  url_format = (
245
249
  UI_URL_FORMATS.get(name) if url_type == "ui" else API_URL_FORMATS.get(name)
246
250
  )
251
+ assert url_format is not None
247
252
 
248
253
  if isinstance(obj, ReceivedEvent):
249
254
  url = url_format.format(
@@ -2,10 +2,12 @@
2
2
  Utilities for working with Flow.visualize()
3
3
  """
4
4
 
5
+ from collections.abc import Coroutine
5
6
  from functools import partial
6
- from typing import Any, List, Optional
7
+ from typing import Any, Literal, Optional, Union, overload
7
8
 
8
- import graphviz
9
+ import graphviz # type: ignore # no typing stubs available
10
+ from typing_extensions import Self
9
11
 
10
12
  from prefect._internal.concurrency.api import from_async
11
13
 
@@ -19,7 +21,7 @@ class VisualizationUnsupportedError(Exception):
19
21
 
20
22
 
21
23
  class TaskVizTrackerState:
22
- current = None
24
+ current: Optional["TaskVizTracker"] = None
23
25
 
24
26
 
25
27
  class GraphvizImportError(Exception):
@@ -30,16 +32,36 @@ class GraphvizExecutableNotFoundError(Exception):
30
32
  pass
31
33
 
32
34
 
33
- def get_task_viz_tracker():
35
+ def get_task_viz_tracker() -> Optional["TaskVizTracker"]:
34
36
  return TaskVizTrackerState.current
35
37
 
36
38
 
39
+ @overload
40
+ def track_viz_task(
41
+ is_async: Literal[True],
42
+ task_name: str,
43
+ parameters: dict[str, Any],
44
+ viz_return_value: Optional[Any] = None,
45
+ ) -> Coroutine[Any, Any, Any]:
46
+ ...
47
+
48
+
49
+ @overload
50
+ def track_viz_task(
51
+ is_async: Literal[False],
52
+ task_name: str,
53
+ parameters: dict[str, Any],
54
+ viz_return_value: Optional[Any] = None,
55
+ ) -> Any:
56
+ ...
57
+
58
+
37
59
  def track_viz_task(
38
60
  is_async: bool,
39
61
  task_name: str,
40
- parameters: dict,
62
+ parameters: dict[str, Any],
41
63
  viz_return_value: Optional[Any] = None,
42
- ):
64
+ ) -> Union[Coroutine[Any, Any, Any], Any]:
43
65
  """Return a result if sync otherwise return a coroutine that returns the result"""
44
66
  if is_async:
45
67
  return from_async.wait_for_call_in_loop_thread(
@@ -50,14 +72,14 @@ def track_viz_task(
50
72
 
51
73
 
52
74
  def _track_viz_task(
53
- task_name,
54
- parameters,
55
- viz_return_value=None,
75
+ task_name: str,
76
+ parameters: dict[str, Any],
77
+ viz_return_value: Optional[Any] = None,
56
78
  ) -> Any:
57
79
  task_run_tracker = get_task_viz_tracker()
58
80
  if task_run_tracker:
59
- upstream_tasks = []
60
- for k, v in parameters.items():
81
+ upstream_tasks: list[VizTask] = []
82
+ for _, v in parameters.items():
61
83
  if isinstance(v, VizTask):
62
84
  upstream_tasks.append(v)
63
85
  # if it's an object that we've already seen,
@@ -85,19 +107,19 @@ class VizTask:
85
107
  def __init__(
86
108
  self,
87
109
  name: str,
88
- upstream_tasks: Optional[List["VizTask"]] = None,
110
+ upstream_tasks: Optional[list["VizTask"]] = None,
89
111
  ):
90
112
  self.name = name
91
- self.upstream_tasks = upstream_tasks if upstream_tasks else []
113
+ self.upstream_tasks: list[VizTask] = upstream_tasks if upstream_tasks else []
92
114
 
93
115
 
94
116
  class TaskVizTracker:
95
117
  def __init__(self):
96
- self.tasks = []
97
- self.dynamic_task_counter = {}
98
- self.object_id_to_task = {}
118
+ self.tasks: list[VizTask] = []
119
+ self.dynamic_task_counter: dict[str, int] = {}
120
+ self.object_id_to_task: dict[int, VizTask] = {}
99
121
 
100
- def add_task(self, task: VizTask):
122
+ def add_task(self, task: VizTask) -> None:
101
123
  if task.name not in self.dynamic_task_counter:
102
124
  self.dynamic_task_counter[task.name] = 0
103
125
  else:
@@ -106,11 +128,11 @@ class TaskVizTracker:
106
128
  task.name = f"{task.name}-{self.dynamic_task_counter[task.name]}"
107
129
  self.tasks.append(task)
108
130
 
109
- def __enter__(self):
131
+ def __enter__(self) -> Self:
110
132
  TaskVizTrackerState.current = self
111
133
  return self
112
134
 
113
- def __exit__(self, exc_type, exc_val, exc_tb):
135
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
114
136
  TaskVizTrackerState.current = None
115
137
 
116
138
  def link_viz_return_value_to_viz_task(
@@ -129,7 +151,7 @@ class TaskVizTracker:
129
151
  self.object_id_to_task[id(viz_return_value)] = viz_task
130
152
 
131
153
 
132
- def build_task_dependencies(task_run_tracker: TaskVizTracker):
154
+ def build_task_dependencies(task_run_tracker: TaskVizTracker) -> graphviz.Digraph:
133
155
  """
134
156
  Constructs a Graphviz directed graph object that represents the dependencies
135
157
  between tasks in the given TaskVizTracker.
@@ -151,9 +173,9 @@ def build_task_dependencies(task_run_tracker: TaskVizTracker):
151
173
  try:
152
174
  g = graphviz.Digraph()
153
175
  for task in task_run_tracker.tasks:
154
- g.node(task.name)
176
+ g.node(task.name) # type: ignore[reportUnknownMemberType]
155
177
  for upstream in task.upstream_tasks:
156
- g.edge(upstream.name, task.name)
178
+ g.edge(upstream.name, task.name) # type: ignore[reportUnknownMemberType]
157
179
  return g
158
180
  except ImportError as exc:
159
181
  raise GraphvizImportError from exc
@@ -166,7 +188,7 @@ def build_task_dependencies(task_run_tracker: TaskVizTracker):
166
188
  )
167
189
 
168
190
 
169
- def visualize_task_dependencies(graph: graphviz.Digraph, flow_run_name: str):
191
+ def visualize_task_dependencies(graph: graphviz.Digraph, flow_run_name: str) -> None:
170
192
  """
171
193
  Renders and displays a Graphviz directed graph representing task dependencies.
172
194
 
@@ -184,7 +206,7 @@ def visualize_task_dependencies(graph: graphviz.Digraph, flow_run_name: str):
184
206
  specifying a `viz_return_value`.
185
207
  """
186
208
  try:
187
- graph.render(filename=flow_run_name, view=True, format="png", cleanup=True)
209
+ graph.render(filename=flow_run_name, view=True, format="png", cleanup=True) # type: ignore[reportUnknownMemberType]
188
210
  except graphviz.backend.ExecutableNotFound as exc:
189
211
  msg = (
190
212
  "It appears you do not have Graphviz installed, or it is not on your "