prefect-client 2.19.9__py3-none-any.whl → 2.20.0__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.
@@ -139,12 +139,40 @@ class MicrosoftTeamsWebhook(AppriseNotificationBlock):
139
139
  url: SecretStr = Field(
140
140
  ...,
141
141
  title="Webhook URL",
142
- description="The Teams incoming webhook URL used to send notifications.",
142
+ description="The Microsoft Power Automate (Workflows) URL used to send notifications to Teams.",
143
143
  examples=[
144
- "https://your-org.webhook.office.com/webhookb2/XXX/IncomingWebhook/YYY/ZZZ"
144
+ "https://prod-NO.LOCATION.logic.azure.com:443/workflows/WFID/triggers/manual/paths/invoke?sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=SIGNATURE"
145
145
  ],
146
146
  )
147
147
 
148
+ include_image: bool = Field(
149
+ default=True,
150
+ description="Include an image with the notification.",
151
+ )
152
+
153
+ wrap: bool = Field(
154
+ default=True,
155
+ description="Wrap the notification text.",
156
+ )
157
+
158
+ def block_initialization(self) -> None:
159
+ """see https://github.com/caronc/apprise/pull/1172"""
160
+ from apprise.plugins.workflows import NotifyWorkflows
161
+
162
+ if not (
163
+ parsed_url := NotifyWorkflows.parse_native_url(self.url.get_secret_value())
164
+ ):
165
+ raise ValueError("Invalid Microsoft Teams Workflow URL provided.")
166
+
167
+ parsed_url.update(
168
+ {
169
+ "include_image": self.include_image,
170
+ "wrap": self.wrap,
171
+ }
172
+ )
173
+
174
+ self._start_apprise_client(SecretStr(NotifyWorkflows(**parsed_url).url()))
175
+
148
176
 
149
177
  class PagerDutyWebHook(AbstractAppriseNotificationBlock):
150
178
  """
prefect/engine.py CHANGED
@@ -107,7 +107,7 @@ from uuid import UUID, uuid4
107
107
 
108
108
  import anyio
109
109
  import pendulum
110
- from anyio import start_blocking_portal
110
+ from anyio.from_thread import start_blocking_portal
111
111
  from typing_extensions import Literal
112
112
 
113
113
  import prefect
@@ -211,6 +211,7 @@ from prefect.utilities.engine import (
211
211
  _resolve_custom_task_run_name,
212
212
  capture_sigterm,
213
213
  check_api_reachable,
214
+ collapse_excgroups,
214
215
  collect_task_run_inputs,
215
216
  emit_task_run_state_change_event,
216
217
  propose_state,
@@ -279,7 +280,7 @@ def enter_flow_run_engine_from_flow_call(
279
280
  # the user. Generally, you should enter contexts _within_ the async `begin_run`
280
281
  # instead but if you need to enter a context from the main thread you'll need to do
281
282
  # it here.
282
- contexts = [capture_sigterm()]
283
+ contexts = [capture_sigterm(), collapse_excgroups()]
283
284
 
284
285
  if flow.isasync and (
285
286
  not is_subflow_run or (is_subflow_run and parent_flow_run_context.flow.isasync)
@@ -324,7 +325,7 @@ def enter_flow_run_engine_from_subprocess(flow_run_id: UUID) -> State:
324
325
  flow_run_id,
325
326
  user_thread=threading.current_thread(),
326
327
  ),
327
- contexts=[capture_sigterm()],
328
+ contexts=[capture_sigterm(), collapse_excgroups()],
328
329
  )
329
330
 
330
331
  APILogHandler.flush()
@@ -2248,9 +2249,9 @@ async def report_flow_run_crashes(flow_run: FlowRun, client: PrefectClient, flow
2248
2249
 
2249
2250
  This context _must_ reraise the exception to properly exit the run.
2250
2251
  """
2251
-
2252
2252
  try:
2253
- yield
2253
+ with collapse_excgroups():
2254
+ yield
2254
2255
  except (Abort, Pause):
2255
2256
  # Do not capture internal signals as crashes
2256
2257
  raise
@@ -2287,7 +2288,8 @@ async def report_task_run_crashes(task_run: TaskRun, client: PrefectClient):
2287
2288
  This context _must_ reraise the exception to properly exit the run.
2288
2289
  """
2289
2290
  try:
2290
- yield
2291
+ with collapse_excgroups():
2292
+ yield
2291
2293
  except (Abort, Pause):
2292
2294
  # Do not capture internal signals as crashes
2293
2295
  raise
prefect/flows.py CHANGED
@@ -1676,6 +1676,7 @@ def load_flow_from_entrypoint(
1676
1676
  if ":" in entrypoint:
1677
1677
  # split by the last colon once to handle Windows paths with drive letters i.e C:\path\to\file.py:do_stuff
1678
1678
  path, func_name = entrypoint.rsplit(":", maxsplit=1)
1679
+
1679
1680
  else:
1680
1681
  path, func_name = entrypoint.rsplit(".", maxsplit=1)
1681
1682
  try:
@@ -1684,15 +1685,13 @@ def load_flow_from_entrypoint(
1684
1685
  raise MissingFlowError(
1685
1686
  f"Flow function with name {func_name!r} not found in {path!r}. "
1686
1687
  ) from exc
1687
- except ScriptError as exc:
1688
+ except ScriptError:
1688
1689
  # If the flow has dependencies that are not installed in the current
1689
- # environment, fallback to loading the flow via AST parsing. The
1690
- # drawback of this approach is that we're unable to actually load the
1691
- # function, so we create a placeholder flow that will re-raise this
1692
- # exception when called.
1693
-
1690
+ # environment, fallback to loading the flow via AST parsing.
1694
1691
  if use_placeholder_flow:
1695
- flow = load_placeholder_flow(entrypoint=entrypoint, raises=exc)
1692
+ flow = safe_load_flow_from_entrypoint(entrypoint)
1693
+ if flow is None:
1694
+ raise
1696
1695
  else:
1697
1696
  raise
1698
1697
 
@@ -1855,6 +1854,147 @@ def load_placeholder_flow(entrypoint: str, raises: Exception):
1855
1854
  return Flow(**arguments)
1856
1855
 
1857
1856
 
1857
+ def safe_load_flow_from_entrypoint(entrypoint: str) -> Optional[Flow]:
1858
+ """
1859
+ Load a flow from an entrypoint and return None if an exception is raised.
1860
+
1861
+ Args:
1862
+ entrypoint: a string in the format `<path_to_script>:<flow_func_name>`
1863
+ or a module path to a flow function
1864
+ """
1865
+ func_def, source_code = _entrypoint_definition_and_source(entrypoint)
1866
+ path = None
1867
+ if ":" in entrypoint:
1868
+ path = entrypoint.rsplit(":")[0]
1869
+ namespace = safe_load_namespace(source_code, filepath=path)
1870
+ if func_def.name in namespace:
1871
+ return namespace[func_def.name]
1872
+ else:
1873
+ # If the function is not in the namespace, if may be due to missing dependencies
1874
+ # for the function. We will attempt to compile each annotation and default value
1875
+ # and remove them from the function definition to see if the function can be
1876
+ # compiled without them.
1877
+
1878
+ return _sanitize_and_load_flow(func_def, namespace)
1879
+
1880
+
1881
+ def _sanitize_and_load_flow(
1882
+ func_def: Union[ast.FunctionDef, ast.AsyncFunctionDef], namespace: Dict[str, Any]
1883
+ ) -> Optional[Flow]:
1884
+ """
1885
+ Attempt to load a flow from the function definition after sanitizing the annotations
1886
+ and defaults that can't be compiled.
1887
+
1888
+ Args:
1889
+ func_def: the function definition
1890
+ namespace: the namespace to load the function into
1891
+
1892
+ Returns:
1893
+ The loaded function or None if the function can't be loaded
1894
+ after sanitizing the annotations and defaults.
1895
+ """
1896
+ args = func_def.args.posonlyargs + func_def.args.args + func_def.args.kwonlyargs
1897
+ if func_def.args.vararg:
1898
+ args.append(func_def.args.vararg)
1899
+ if func_def.args.kwarg:
1900
+ args.append(func_def.args.kwarg)
1901
+ # Remove annotations that can't be compiled
1902
+ for arg in args:
1903
+ if arg.annotation is not None:
1904
+ try:
1905
+ code = compile(
1906
+ ast.Expression(arg.annotation),
1907
+ filename="<ast>",
1908
+ mode="eval",
1909
+ )
1910
+ exec(code, namespace)
1911
+ except Exception as e:
1912
+ logger.debug(
1913
+ "Failed to evaluate annotation for argument %s due to the following error. Ignoring annotation.",
1914
+ arg.arg,
1915
+ exc_info=e,
1916
+ )
1917
+ arg.annotation = None
1918
+
1919
+ # Remove defaults that can't be compiled
1920
+ new_defaults = []
1921
+ for default in func_def.args.defaults:
1922
+ try:
1923
+ code = compile(ast.Expression(default), "<ast>", "eval")
1924
+ exec(code, namespace)
1925
+ new_defaults.append(default)
1926
+ except Exception as e:
1927
+ logger.debug(
1928
+ "Failed to evaluate default value %s due to the following error. Ignoring default.",
1929
+ default,
1930
+ exc_info=e,
1931
+ )
1932
+ new_defaults.append(
1933
+ ast.Constant(
1934
+ value=None, lineno=default.lineno, col_offset=default.col_offset
1935
+ )
1936
+ )
1937
+ func_def.args.defaults = new_defaults
1938
+
1939
+ # Remove kw_defaults that can't be compiled
1940
+ new_kw_defaults = []
1941
+ for default in func_def.args.kw_defaults:
1942
+ if default is not None:
1943
+ try:
1944
+ code = compile(ast.Expression(default), "<ast>", "eval")
1945
+ exec(code, namespace)
1946
+ new_kw_defaults.append(default)
1947
+ except Exception as e:
1948
+ logger.debug(
1949
+ "Failed to evaluate default value %s due to the following error. Ignoring default.",
1950
+ default,
1951
+ exc_info=e,
1952
+ )
1953
+ new_kw_defaults.append(
1954
+ ast.Constant(
1955
+ value=None,
1956
+ lineno=default.lineno,
1957
+ col_offset=default.col_offset,
1958
+ )
1959
+ )
1960
+ else:
1961
+ new_kw_defaults.append(
1962
+ ast.Constant(
1963
+ value=None,
1964
+ lineno=func_def.lineno,
1965
+ col_offset=func_def.col_offset,
1966
+ )
1967
+ )
1968
+ func_def.args.kw_defaults = new_kw_defaults
1969
+
1970
+ if func_def.returns is not None:
1971
+ try:
1972
+ code = compile(
1973
+ ast.Expression(func_def.returns), filename="<ast>", mode="eval"
1974
+ )
1975
+ exec(code, namespace)
1976
+ except Exception as e:
1977
+ logger.debug(
1978
+ "Failed to evaluate return annotation due to the following error. Ignoring annotation.",
1979
+ exc_info=e,
1980
+ )
1981
+ func_def.returns = None
1982
+
1983
+ # Attempt to compile the function without annotations and defaults that
1984
+ # can't be compiled
1985
+ try:
1986
+ code = compile(
1987
+ ast.Module(body=[func_def], type_ignores=[]),
1988
+ filename="<ast>",
1989
+ mode="exec",
1990
+ )
1991
+ exec(code, namespace)
1992
+ except Exception as e:
1993
+ logger.debug("Failed to compile: %s", e)
1994
+ else:
1995
+ return namespace.get(func_def.name)
1996
+
1997
+
1858
1998
  def load_flow_arguments_from_entrypoint(
1859
1999
  entrypoint: str, arguments: Optional[Union[List[str], Set[str]]] = None
1860
2000
  ) -> Dict[str, Any]:
@@ -1870,6 +2010,9 @@ def load_flow_arguments_from_entrypoint(
1870
2010
  """
1871
2011
 
1872
2012
  func_def, source_code = _entrypoint_definition_and_source(entrypoint)
2013
+ path = None
2014
+ if ":" in entrypoint:
2015
+ path = entrypoint.rsplit(":")[0]
1873
2016
 
1874
2017
  if arguments is None:
1875
2018
  # If no arguments are provided default to known arguments that are of
@@ -1905,7 +2048,7 @@ def load_flow_arguments_from_entrypoint(
1905
2048
 
1906
2049
  # if the arg value is not a raw str (i.e. a variable or expression),
1907
2050
  # then attempt to evaluate it
1908
- namespace = safe_load_namespace(source_code)
2051
+ namespace = safe_load_namespace(source_code, filepath=path)
1909
2052
  literal_arg_value = ast.get_source_segment(source_code, keyword.value)
1910
2053
  cleaned_value = (
1911
2054
  literal_arg_value.replace("\n", "") if literal_arg_value else ""
prefect/task_server.py CHANGED
@@ -9,6 +9,7 @@ from functools import partial
9
9
  from typing import List, Optional, Type
10
10
 
11
11
  import anyio
12
+ from exceptiongroup import BaseExceptionGroup # novermin
12
13
  from websockets.exceptions import InvalidStatusCode
13
14
 
14
15
  from prefect import Task, get_client
@@ -225,16 +226,21 @@ class TaskServer:
225
226
  validated_state=state,
226
227
  )
227
228
 
228
- self._runs_task_group.start_soon(
229
- partial(
230
- submit_autonomous_task_run_to_engine,
231
- task=task,
232
- task_run=task_run,
233
- parameters=parameters,
234
- task_runner=self.task_runner,
235
- client=self._client,
229
+ try:
230
+ self._runs_task_group.start_soon(
231
+ partial(
232
+ submit_autonomous_task_run_to_engine,
233
+ task=task,
234
+ task_run=task_run,
235
+ parameters=parameters,
236
+ task_runner=self.task_runner,
237
+ client=self._client,
238
+ )
239
+ )
240
+ except BaseException as exc:
241
+ logger.exception(
242
+ f"Failed to submit task run {task_run.id!r} to engine", exc_info=exc
236
243
  )
237
- )
238
244
 
239
245
  async def execute_task_run(self, task_run: TaskRun):
240
246
  """Execute a task run in the task server."""
@@ -301,6 +307,14 @@ async def serve(*tasks: Task, task_runner: Optional[Type[BaseTaskRunner]] = None
301
307
  try:
302
308
  await task_server.start()
303
309
 
310
+ except BaseExceptionGroup as exc: # novermin
311
+ exceptions = exc.exceptions
312
+ n_exceptions = len(exceptions)
313
+ logger.error(
314
+ f"Task worker stopped with {n_exceptions} exception{'s' if n_exceptions != 1 else ''}:"
315
+ f"\n" + "\n".join(str(e) for e in exceptions)
316
+ )
317
+
304
318
  except StopTaskServer:
305
319
  logger.info("Task server stopped.")
306
320
 
@@ -29,7 +29,9 @@ from uuid import UUID, uuid4
29
29
 
30
30
  import anyio
31
31
  import anyio.abc
32
+ import anyio.to_thread
32
33
  import sniffio
34
+ from anyio.from_thread import start_blocking_portal
33
35
  from typing_extensions import Literal, ParamSpec, TypeGuard
34
36
 
35
37
  from prefect.logging import get_logger
@@ -134,7 +136,7 @@ async def run_sync_in_worker_thread(
134
136
  """
135
137
  call = partial(__fn, *args, **kwargs)
136
138
  return await anyio.to_thread.run_sync(
137
- call, cancellable=True, limiter=get_thread_limiter()
139
+ call, abandon_on_cancel=True, limiter=get_thread_limiter()
138
140
  )
139
141
 
140
142
 
@@ -202,7 +204,7 @@ async def run_sync_in_interruptible_worker_thread(
202
204
  partial(
203
205
  anyio.to_thread.run_sync,
204
206
  capture_worker_thread_and_result,
205
- cancellable=True,
207
+ abandon_on_cancel=True,
206
208
  limiter=get_thread_limiter(),
207
209
  )
208
210
  )
@@ -228,7 +230,7 @@ def run_async_in_new_loop(__fn: Callable[..., Awaitable[T]], *args: Any, **kwarg
228
230
 
229
231
  def in_async_worker_thread() -> bool:
230
232
  try:
231
- anyio.from_thread.threadlocals.current_async_module
233
+ anyio.from_thread.threadlocals.current_async_backend
232
234
  except AttributeError:
233
235
  return False
234
236
  else:
@@ -338,7 +340,7 @@ def sync(__async_fn: Callable[P, Awaitable[T]], *args: P.args, **kwargs: P.kwarg
338
340
  "`sync` called from an asynchronous context; "
339
341
  "you should `await` the async function directly instead."
340
342
  )
341
- with anyio.start_blocking_portal() as portal:
343
+ with start_blocking_portal() as portal:
342
344
  return portal.call(partial(__async_fn, *args, **kwargs))
343
345
  elif in_async_worker_thread():
344
346
  # In a sync context but we can access the event loop thread; send the async
@@ -346,17 +346,19 @@ def parameter_schema_from_entrypoint(entrypoint: str) -> ParameterSchema:
346
346
  Returns:
347
347
  ParameterSchema: The parameter schema for the function.
348
348
  """
349
+ filepath = None
349
350
  if ":" in entrypoint:
350
351
  # split by the last colon once to handle Windows paths with drive letters i.e C:\path\to\file.py:do_stuff
351
352
  path, func_name = entrypoint.rsplit(":", maxsplit=1)
352
353
  source_code = Path(path).read_text()
354
+ filepath = path
353
355
  else:
354
356
  path, func_name = entrypoint.rsplit(".", maxsplit=1)
355
357
  spec = importlib.util.find_spec(path)
356
358
  if not spec or not spec.origin:
357
359
  raise ValueError(f"Could not find module {path!r}")
358
360
  source_code = Path(spec.origin).read_text()
359
- signature = _generate_signature_from_source(source_code, func_name)
361
+ signature = _generate_signature_from_source(source_code, func_name, filepath)
360
362
  docstring = _get_docstring_from_source(source_code, func_name)
361
363
  return generate_parameter_schema(signature, parameter_docstrings(docstring))
362
364
 
@@ -424,7 +426,7 @@ def raise_for_reserved_arguments(fn: Callable, reserved_arguments: Iterable[str]
424
426
 
425
427
 
426
428
  def _generate_signature_from_source(
427
- source_code: str, func_name: str
429
+ source_code: str, func_name: str, filepath: Optional[str] = None
428
430
  ) -> inspect.Signature:
429
431
  """
430
432
  Extract the signature of a function from its source code.
@@ -440,7 +442,7 @@ def _generate_signature_from_source(
440
442
  """
441
443
  # Load the namespace from the source code. Missing imports and exceptions while
442
444
  # loading local class definitions are ignored.
443
- namespace = safe_load_namespace(source_code)
445
+ namespace = safe_load_namespace(source_code, filepath=filepath)
444
446
  # Parse the source code into an AST
445
447
  parsed_code = ast.parse(source_code)
446
448
 
@@ -4,11 +4,13 @@ import inspect
4
4
  import os
5
5
  import signal
6
6
  import time
7
+ from contextlib import contextmanager
7
8
  from functools import partial
8
9
  from typing import (
9
10
  Any,
10
11
  Callable,
11
12
  Dict,
13
+ Generator,
12
14
  Iterable,
13
15
  Optional,
14
16
  Set,
@@ -18,6 +20,7 @@ from typing import (
18
20
  from uuid import UUID, uuid4
19
21
 
20
22
  import anyio
23
+ from exceptiongroup import BaseExceptionGroup # novermin
21
24
  from typing_extensions import Literal
22
25
 
23
26
  import prefect
@@ -734,3 +737,14 @@ def emit_task_run_state_change_event(
734
737
  },
735
738
  follows=follows,
736
739
  )
740
+
741
+
742
+ @contextmanager
743
+ def collapse_excgroups() -> Generator[None, None, None]:
744
+ try:
745
+ yield
746
+ except BaseException as exc:
747
+ while isinstance(exc, BaseExceptionGroup) and len(exc.exceptions) == 1:
748
+ exc = exc.exceptions[0]
749
+
750
+ raise exc
@@ -362,79 +362,159 @@ class AliasedModuleLoader(Loader):
362
362
  sys.modules[self.alias] = root_module
363
363
 
364
364
 
365
- def safe_load_namespace(source_code: str):
365
+ def safe_load_namespace(
366
+ source_code: str, filepath: Optional[str] = None
367
+ ) -> Dict[str, Any]:
366
368
  """
367
- Safely load a namespace from source code.
369
+ Safely load a namespace from source code, optionally handling relative imports.
368
370
 
369
- This function will attempt to import all modules and classes defined in the source
370
- code. If an import fails, the error is caught and the import is skipped. This function
371
- will also attempt to compile and evaluate class and function definitions locally.
371
+ If a `filepath` is provided, `sys.path` is modified to support relative imports.
372
+ Changes to `sys.path` are reverted after completion, but this function is not thread safe
373
+ and use of it in threaded contexts may result in undesirable behavior.
372
374
 
373
375
  Args:
374
376
  source_code: The source code to load
377
+ filepath: Optional file path of the source code. If provided, enables relative imports.
375
378
 
376
379
  Returns:
377
- The namespace loaded from the source code. Can be used when evaluating source
378
- code.
380
+ The namespace loaded from the source code.
379
381
  """
380
382
  parsed_code = ast.parse(source_code)
381
383
 
382
- namespace = {"__name__": "prefect_safe_namespace_loader"}
384
+ namespace: Dict[str, Any] = {"__name__": "prefect_safe_namespace_loader"}
383
385
 
384
- # Remove the body of the if __name__ == "__main__": block from the AST to prevent
385
- # execution of guarded code
386
- new_body = []
387
- for node in parsed_code.body:
388
- if _is_main_block(node):
389
- continue
390
- new_body.append(node)
386
+ # Remove the body of the if __name__ == "__main__": block
387
+ new_body = [node for node in parsed_code.body if not _is_main_block(node)]
391
388
  parsed_code.body = new_body
392
389
 
393
- # Walk through the AST and find all import statements
394
- for node in ast.walk(parsed_code):
395
- if isinstance(node, ast.Import):
396
- for alias in node.names:
397
- module_name = alias.name
398
- as_name = alias.asname if alias.asname else module_name
399
- try:
400
- # Attempt to import the module
401
- namespace[as_name] = importlib.import_module(module_name)
402
- logger.debug("Successfully imported %s", module_name)
403
- except ImportError as e:
404
- logger.debug(f"Failed to import {module_name}: {e}")
405
- elif isinstance(node, ast.ImportFrom):
406
- module_name = node.module
407
- if module_name is None:
408
- continue
409
- try:
410
- module = importlib.import_module(module_name)
390
+ temp_module = None
391
+ original_sys_path = None
392
+
393
+ if filepath:
394
+ # Setup for relative imports
395
+ file_dir = os.path.dirname(os.path.abspath(filepath))
396
+ package_name = os.path.basename(file_dir)
397
+ parent_dir = os.path.dirname(file_dir)
398
+
399
+ # Save original sys.path and modify it
400
+ original_sys_path = sys.path.copy()
401
+ sys.path.insert(0, parent_dir)
402
+
403
+ # Create a temporary module for import context
404
+ temp_module = ModuleType(package_name)
405
+ temp_module.__file__ = filepath
406
+ temp_module.__package__ = package_name
407
+
408
+ # Create a spec for the module
409
+ temp_module.__spec__ = ModuleSpec(package_name, None)
410
+ temp_module.__spec__.loader = None
411
+ temp_module.__spec__.submodule_search_locations = [file_dir]
412
+
413
+ try:
414
+ for node in parsed_code.body:
415
+ if isinstance(node, ast.Import):
411
416
  for alias in node.names:
412
- name = alias.name
413
- asname = alias.asname if alias.asname else name
417
+ module_name = alias.name
418
+ as_name = alias.asname or module_name
414
419
  try:
415
- # Get the specific attribute from the module
416
- attribute = getattr(module, name)
417
- namespace[asname] = attribute
418
- except AttributeError as e:
419
- logger.debug(
420
- "Failed to retrieve %s from %s: %s", name, module_name, e
421
- )
422
- except ImportError as e:
423
- logger.debug("Failed to import from %s: %s", node.module, e)
424
-
425
- # Handle local definitions
426
- for node in parsed_code.body:
427
- if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.Assign)):
428
- try:
429
- # Compile and execute each class and function definition and assignment
430
- code = compile(
431
- ast.Module(body=[node], type_ignores=[]),
432
- filename="<ast>",
433
- mode="exec",
434
- )
435
- exec(code, namespace)
436
- except Exception as e:
437
- logger.debug("Failed to compile: %s", e)
420
+ namespace[as_name] = importlib.import_module(module_name)
421
+ logger.debug("Successfully imported %s", module_name)
422
+ except ImportError as e:
423
+ logger.debug(f"Failed to import {module_name}: {e}")
424
+ elif isinstance(node, ast.ImportFrom):
425
+ module_name = node.module or ""
426
+ if filepath:
427
+ try:
428
+ if node.level > 0:
429
+ # For relative imports, use the parent package to inform the import
430
+ package_parts = temp_module.__package__.split(".")
431
+ if len(package_parts) < node.level:
432
+ raise ImportError(
433
+ "Attempted relative import beyond top-level package"
434
+ )
435
+ parent_package = ".".join(
436
+ package_parts[: (1 - node.level)]
437
+ if node.level > 1
438
+ else package_parts
439
+ )
440
+ module = importlib.import_module(
441
+ f".{module_name}" if module_name else "",
442
+ package=parent_package,
443
+ )
444
+ else:
445
+ # Absolute imports are handled as normal
446
+ module = importlib.import_module(module_name)
447
+
448
+ for alias in node.names:
449
+ name = alias.name
450
+ asname = alias.asname or name
451
+ if name == "*":
452
+ # Handle 'from module import *'
453
+ module_dict = {
454
+ k: v
455
+ for k, v in module.__dict__.items()
456
+ if not k.startswith("_")
457
+ }
458
+ namespace.update(module_dict)
459
+ else:
460
+ try:
461
+ attribute = getattr(module, name)
462
+ namespace[asname] = attribute
463
+ except AttributeError as e:
464
+ logger.debug(
465
+ "Failed to retrieve %s from %s: %s",
466
+ name,
467
+ module_name,
468
+ e,
469
+ )
470
+ except ImportError as e:
471
+ logger.debug("Failed to import from %s: %s", module_name, e)
472
+ else:
473
+ # Handle as absolute import when no filepath is provided
474
+ try:
475
+ module = importlib.import_module(module_name)
476
+ for alias in node.names:
477
+ name = alias.name
478
+ asname = alias.asname or name
479
+ if name == "*":
480
+ # Handle 'from module import *'
481
+ module_dict = {
482
+ k: v
483
+ for k, v in module.__dict__.items()
484
+ if not k.startswith("_")
485
+ }
486
+ namespace.update(module_dict)
487
+ else:
488
+ try:
489
+ attribute = getattr(module, name)
490
+ namespace[asname] = attribute
491
+ except AttributeError as e:
492
+ logger.debug(
493
+ "Failed to retrieve %s from %s: %s",
494
+ name,
495
+ module_name,
496
+ e,
497
+ )
498
+ except ImportError as e:
499
+ logger.debug("Failed to import from %s: %s", module_name, e)
500
+ # Handle local definitions
501
+ for node in parsed_code.body:
502
+ if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.Assign)):
503
+ try:
504
+ code = compile(
505
+ ast.Module(body=[node], type_ignores=[]),
506
+ filename="<ast>",
507
+ mode="exec",
508
+ )
509
+ exec(code, namespace)
510
+ except Exception as e:
511
+ logger.debug("Failed to compile: %s", e)
512
+
513
+ finally:
514
+ # Restore original sys.path if it was modified
515
+ if original_sys_path:
516
+ sys.path[:] = original_sys_path
517
+
438
518
  return namespace
439
519
 
440
520
 
@@ -1,4 +1,5 @@
1
1
  import sys
2
+ from asyncio import CancelledError
2
3
  from collections import deque
3
4
  from traceback import format_exception
4
5
  from types import TracebackType
@@ -67,6 +68,11 @@ async def critical_service_loop(
67
68
  backoff_count = 0
68
69
 
69
70
  track_record.append(True)
71
+ except CancelledError as exc:
72
+ # Exit immediately because the task was cancelled, possibly due
73
+ # to a signal or timeout.
74
+ logger.debug(f"Run of {workload!r} cancelled", exc_info=exc)
75
+ return
70
76
  except httpx.TransportError as exc:
71
77
  # httpx.TransportError is the base class for any kind of communications
72
78
  # error, like timeouts, connection failures, etc. This does _not_ cover
@@ -138,7 +144,7 @@ async def critical_service_loop(
138
144
  failures.clear()
139
145
  printer(
140
146
  "Backing off due to consecutive errors, using increased interval of "
141
- f" {interval * 2**backoff_count}s."
147
+ f" {interval * 2 ** backoff_count}s."
142
148
  )
143
149
 
144
150
  if run_once:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prefect-client
3
- Version: 2.19.9
3
+ Version: 2.20.0
4
4
  Summary: Workflow orchestration and management.
5
5
  Home-page: https://www.prefect.io
6
6
  Author: Prefect Technologies, Inc.
@@ -23,7 +23,7 @@ Classifier: Programming Language :: Python :: 3.11
23
23
  Classifier: Topic :: Software Development :: Libraries
24
24
  Requires-Python: >=3.8
25
25
  Description-Content-Type: text/markdown
26
- Requires-Dist: anyio <4.0.0,>=3.7.1
26
+ Requires-Dist: anyio <5.0.0,>=4.4.0
27
27
  Requires-Dist: asgi-lifespan <3.0,>=1.0
28
28
  Requires-Dist: cachetools <6.0,>=5.3
29
29
  Requires-Dist: cloudpickle <4.0,>=2.0
@@ -5,11 +5,11 @@ prefect/agent.py,sha256=HaGT0yh3fciluYpO99dVHo_LHq7N2cYLuWNrEV_kPV8,27789
5
5
  prefect/artifacts.py,sha256=mreaBE4qMoXkjc9YI-5cAxoye7ixraHB_zr8GTK9xPU,8694
6
6
  prefect/automations.py,sha256=rjVtQblBlKhD_q24bG6zbxJeb_XQJnodMlhr565aZJY,4853
7
7
  prefect/context.py,sha256=Hgn3rIjCbqfCmGnZzV_eZ2FwxGjEhaZjUw_nppqNQSA,18189
8
- prefect/engine.py,sha256=nbJ1LXRBZG1St9K631fVUntQnMTTw3GmC4_XVLByR_s,91781
8
+ prefect/engine.py,sha256=czyTcLPIaj_LOpPuWNheK0FfdOwX42VKLtonm7vZqH0,91938
9
9
  prefect/exceptions.py,sha256=ElqC81_w6XbTaxLYANLMIPK8Fz46NmJZCRKL4NZ-JIg,10907
10
10
  prefect/filesystems.py,sha256=XniPSdBAqywj43X7GyfuWJQIbz07QJ5Y3cVNLhIF3lQ,35260
11
11
  prefect/flow_runs.py,sha256=mFHLavZk1yZ62H3UazuNDBZWAF7AqKttA4rMcHgsVSw,3119
12
- prefect/flows.py,sha256=gK8VnDt9R3PWbBBJU3z46PWhj1BEHbAHEZGLJzcotUk,79746
12
+ prefect/flows.py,sha256=-Ietkctf2ONzmqcytI9Fc8T1-vtPmEuOo3m6wPNLIrQ,84735
13
13
  prefect/futures.py,sha256=RaWfYIXtH7RsWxQ5QWTTlAzwtVV8XWpXaZT_hLq35vQ,12590
14
14
  prefect/manifests.py,sha256=sTM7j8Us5d49zaydYKWsKb7zJ96v1ChkLkLeR0GFYD8,683
15
15
  prefect/new_flow_engine.py,sha256=A1adTWTBAwPCn6ay003Jsoc2SdYgHV4AcJo1bmpa_7Y,16039
@@ -23,7 +23,7 @@ prefect/settings.py,sha256=gFVXmGLapnkIV7hQvRJMJ6472UZJr6gqZYk1xwcqgnQ,74931
23
23
  prefect/states.py,sha256=B38zIXnqc8cmw3GPxmMQ4thX6pXb6UtG4PoTZ5thGQs,21036
24
24
  prefect/task_engine.py,sha256=_2I7XLwoT_nNhpzTMa_52aQKjsDoaW6WpzwIHYEWZS0,2598
25
25
  prefect/task_runners.py,sha256=HXUg5UqhZRN2QNBqMdGE1lKhwFhT8TaRN75ScgLbnw8,11012
26
- prefect/task_server.py,sha256=3f6rDIOXmhhF_MDHGk5owaU9lyLHsR-zgCp6pIHEUyo,11075
26
+ prefect/task_server.py,sha256=-wHuAlY8DLEQuJcEvcFfB4da0Xdnmqk6D7GjHShh-Ik,11668
27
27
  prefect/tasks.py,sha256=HWTT9Y6obBId1VcLCQbmBFN4WUKE0j1pw2IvzyTgF70,56134
28
28
  prefect/variables.py,sha256=4r5gVGpAZxLWHj5JoNZJTuktX1-u3ENzVp3t4M6FDgs,3815
29
29
  prefect/_internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -156,7 +156,7 @@ prefect/blocks/abstract.py,sha256=AiAs0MC5JKCf0Xg0yofC5Qu2TZ52AjDMP1ntMGuP2dY,16
156
156
  prefect/blocks/core.py,sha256=66pGFVPxtCCGWELqPXYqN8L0GoUXuUqv6jWw3Kk-tyY,43496
157
157
  prefect/blocks/fields.py,sha256=ANOzbNyDCBIvm6ktgbLTMs7JW2Sf6CruyATjAW61ks0,1607
158
158
  prefect/blocks/kubernetes.py,sha256=IN-hZkzIRvqjd_dzPZby3q8p7m2oUWqArBq24BU9cDg,4071
159
- prefect/blocks/notifications.py,sha256=raXBPidAfec7VCyLA1bb46RD06i0zPtVonWcXtkeOUU,27211
159
+ prefect/blocks/notifications.py,sha256=LJd2mgV29URqItJyxtWUpdo4wswtm7KyIseuAjV3joI,28132
160
160
  prefect/blocks/system.py,sha256=aIRiFKlXIQ1sMaqniMXYolFsx2IVN3taBMH3KCThB2I,3089
161
161
  prefect/blocks/webhook.py,sha256=VzQ-qcRtW8MMuYEGYwFgt1QXtWedUtVmeTo7iE2UQ78,2008
162
162
  prefect/client/__init__.py,sha256=yJ5FRF9RxNUio2V_HmyKCKw5G6CZO0h8cv6xA_Hkpcc,477
@@ -254,23 +254,23 @@ prefect/software/python.py,sha256=EssQ16aMvWSzzWagtNPfjQLu9ehieRwN0iWeqpBVtRU,17
254
254
  prefect/types/__init__.py,sha256=aZvlQ2uXl949sJ_khmxSVkRH3o6edo-eJ_GBGMBN5Yg,3134
255
255
  prefect/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
256
256
  prefect/utilities/annotations.py,sha256=bXB43j5Zsq5gaBcJe9qnszBlnNwCTwqSTgcu2OkkRLo,2776
257
- prefect/utilities/asyncutils.py,sha256=AyQUahL_KSkKmAWbEYeAJhEC27qSze48yUJrQPvfOW0,17164
258
- prefect/utilities/callables.py,sha256=-Ccr5JmDoafth46MPPQCRStBMSG_ickCDfBztpd_MAs,21119
257
+ prefect/utilities/asyncutils.py,sha256=ftu6MaV9qOZ3oCIErrneW07km2BydCezOMzvPUuCMUo,17246
258
+ prefect/utilities/callables.py,sha256=YWilWp6oyL3D-hsyUuSE-h2KZ0aO_nbjhW75KsrLVmQ,21224
259
259
  prefect/utilities/collections.py,sha256=0v-NNXxYYzkUTCCNDMNB44AnDv9yj35UYouNraCqlo8,15449
260
260
  prefect/utilities/compat.py,sha256=mNQZDnzyKaOqy-OV-DnmH_dc7CNF5nQgW_EsA4xMr7g,906
261
261
  prefect/utilities/context.py,sha256=BThuUW94-IYgFYTeMIM9KMo8ShT3oiI7w5ajZHzU1j0,1377
262
262
  prefect/utilities/dispatch.py,sha256=BSAuYf3uchA6giBB90Z9tsmnR94SAqHZMHl01fRuA64,5467
263
263
  prefect/utilities/dockerutils.py,sha256=O5lIgCej5KGRYU2TC1NzNuIK595uOIWJilhZXYEVtOA,20180
264
- prefect/utilities/engine.py,sha256=TKiYqpfgt4zopuI8yvh2e-V9GgLcRrh3TpKRhvLuHdw,25669
264
+ prefect/utilities/engine.py,sha256=6O7zYZQfpo6FtsI6n9DUNs-MB7_xLs3iXiCnSukR8qI,26046
265
265
  prefect/utilities/filesystem.py,sha256=M_TeZ1MftjBf7hDLWk-Iphir369TpJ1binMsBKiO9YE,4449
266
266
  prefect/utilities/hashing.py,sha256=EOwZLmoIZImuSTxAvVqInabxJ-4RpEfYeg9e2EDQF8o,1752
267
- prefect/utilities/importtools.py,sha256=MxGyPxfKjn6WtXVY9t8xRfDqK5MPUa4NBZqvyqIvp-4,15535
267
+ prefect/utilities/importtools.py,sha256=JteP9zFz-oJyxSVYr63kJ-RpDL2jjTfJMqgYaBst19M,19518
268
268
  prefect/utilities/math.py,sha256=wLwcKVidpNeWQi1TUIWWLHGjlz9UgboX9FUGhx_CQzo,2821
269
269
  prefect/utilities/names.py,sha256=x-stHcF7_tebJPvB1dz-5FvdXJXNBTg2kFZXSnIBBmk,1657
270
270
  prefect/utilities/processutils.py,sha256=yo_GO48pZzgn4A0IK5irTAoqyUCYvWKDSqHXCrtP8c4,14547
271
271
  prefect/utilities/pydantic.py,sha256=3IR73F3gkuRG6HQfCEP9ENIC6qbK6oOFawjsYJfoUkg,9984
272
272
  prefect/utilities/render_swagger.py,sha256=h2UrORVN3f7gM4zurtMnySjQXZIOWbji3uMinpbkl8U,3717
273
- prefect/utilities/services.py,sha256=u0Gpdw5pYceaSLCqOihGyFb2AlMBYE2P9Ts9qRb3N9Q,6584
273
+ prefect/utilities/services.py,sha256=POYQRdvkUs-0dFcgV-BOyII0NFttgW1NjDAJR1bbGqU,6865
274
274
  prefect/utilities/slugify.py,sha256=57Vb14t13F3zm1P65KAu8nVeAz0iJCd1Qc5eMG-R5y8,169
275
275
  prefect/utilities/templating.py,sha256=t32Gcsvvm8ibzdqXwcWzY7JkwftPn73FiiLYEnQWyKM,13237
276
276
  prefect/utilities/text.py,sha256=eXGIsCcZ7h_6hy8T5GDQjL8GiKyktoOqavYub0QjgO4,445
@@ -285,8 +285,8 @@ prefect/workers/block.py,sha256=aYY__uq3v1eq1kkbVukxyhQNbkknaKYo6-_3tcrfKKA,8067
285
285
  prefect/workers/process.py,sha256=pPtCdA7fKQ4OsvoitT-cayZeh5HgLX4xBUYlb2Zad-Q,9475
286
286
  prefect/workers/server.py,sha256=WVZJxR8nTMzK0ov0BD0xw5OyQpT26AxlXbsGQ1OrxeQ,1551
287
287
  prefect/workers/utilities.py,sha256=VfPfAlGtTuDj0-Kb8WlMgAuOfgXCdrGAnKMapPSBrwc,2483
288
- prefect_client-2.19.9.dist-info/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
289
- prefect_client-2.19.9.dist-info/METADATA,sha256=LBa5dw7OxgaHgyE71e63fFvfM5Q5wAJ62YAoaFjNWDw,7410
290
- prefect_client-2.19.9.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
291
- prefect_client-2.19.9.dist-info/top_level.txt,sha256=MJZYJgFdbRc2woQCeB4vM6T33tr01TmkEhRcns6H_H4,8
292
- prefect_client-2.19.9.dist-info/RECORD,,
288
+ prefect_client-2.20.0.dist-info/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
289
+ prefect_client-2.20.0.dist-info/METADATA,sha256=GRcdYDHMdwoy3FRSFZvP_sNOvtU3VEgUx1VMD6wUXdU,7410
290
+ prefect_client-2.20.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
291
+ prefect_client-2.20.0.dist-info/top_level.txt,sha256=MJZYJgFdbRc2woQCeB4vM6T33tr01TmkEhRcns6H_H4,8
292
+ prefect_client-2.20.0.dist-info/RECORD,,