prefect-client 3.0.0rc9__py3-none-any.whl → 3.0.0rc11__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 (49) hide show
  1. prefect/_internal/compatibility/migration.py +48 -8
  2. prefect/_internal/concurrency/api.py +1 -1
  3. prefect/_internal/retries.py +61 -0
  4. prefect/agent.py +6 -0
  5. prefect/client/cloud.py +1 -1
  6. prefect/client/schemas/objects.py +3 -4
  7. prefect/concurrency/asyncio.py +3 -3
  8. prefect/concurrency/events.py +1 -1
  9. prefect/concurrency/services.py +3 -2
  10. prefect/concurrency/sync.py +19 -5
  11. prefect/context.py +14 -2
  12. prefect/deployments/__init__.py +28 -15
  13. prefect/deployments/schedules.py +5 -2
  14. prefect/deployments/steps/pull.py +7 -0
  15. prefect/events/schemas/automations.py +3 -3
  16. prefect/exceptions.py +4 -1
  17. prefect/filesystems.py +4 -3
  18. prefect/flow_engine.py +76 -14
  19. prefect/flows.py +222 -64
  20. prefect/futures.py +53 -7
  21. prefect/infrastructure/__init__.py +6 -0
  22. prefect/infrastructure/base.py +6 -0
  23. prefect/logging/loggers.py +1 -1
  24. prefect/results.py +50 -67
  25. prefect/runner/runner.py +93 -20
  26. prefect/runner/server.py +20 -22
  27. prefect/runner/submit.py +0 -8
  28. prefect/runtime/flow_run.py +38 -3
  29. prefect/serializers.py +3 -3
  30. prefect/settings.py +15 -45
  31. prefect/task_engine.py +77 -21
  32. prefect/task_runners.py +28 -16
  33. prefect/task_worker.py +6 -4
  34. prefect/tasks.py +30 -5
  35. prefect/transactions.py +18 -2
  36. prefect/utilities/asyncutils.py +9 -3
  37. prefect/utilities/engine.py +34 -1
  38. prefect/utilities/importtools.py +1 -1
  39. prefect/utilities/timeout.py +20 -5
  40. prefect/workers/base.py +98 -208
  41. prefect/workers/block.py +6 -0
  42. prefect/workers/cloud.py +6 -0
  43. prefect/workers/process.py +262 -4
  44. prefect/workers/server.py +27 -9
  45. {prefect_client-3.0.0rc9.dist-info → prefect_client-3.0.0rc11.dist-info}/METADATA +4 -4
  46. {prefect_client-3.0.0rc9.dist-info → prefect_client-3.0.0rc11.dist-info}/RECORD +49 -44
  47. {prefect_client-3.0.0rc9.dist-info → prefect_client-3.0.0rc11.dist-info}/LICENSE +0 -0
  48. {prefect_client-3.0.0rc9.dist-info → prefect_client-3.0.0rc11.dist-info}/WHEEL +0 -0
  49. {prefect_client-3.0.0rc9.dist-info → prefect_client-3.0.0rc11.dist-info}/top_level.txt +0 -0
@@ -5,11 +5,41 @@ The `getattr_migration` function is used to handle imports for moved or removed
5
5
  It is used in the `__getattr__` attribute of modules that have moved or removed objects.
6
6
 
7
7
  Usage:
8
- ```python
9
- from prefect._internal.compatibility.migration import getattr_migration
10
8
 
11
- __getattr__ = getattr_migration(__name__)
12
- ```
9
+ Moved objects:
10
+ 1. Add the old and new path to the `MOVED_IN_V3` dictionary, e.g. `MOVED_IN_V3 = {"old_path": "new_path"}`
11
+ 2. In the module where the object was moved from, add the following lines:
12
+ ```python
13
+ # at top
14
+ from prefect._internal.compatibility.migration import getattr_migration
15
+
16
+ # at bottom
17
+ __getattr__ = getattr_migration(__name__)
18
+ ```
19
+
20
+ Example at src/prefect/engine.py
21
+
22
+ Removed objects:
23
+ 1. Add the old path and error message to the `REMOVED_IN_V3` dictionary, e.g. `REMOVED_IN_V3 = {"old_path": "error_message"}`
24
+ 2. In the module where the object was removed, add the following lines:
25
+ ```python
26
+ # at top
27
+ from prefect._internal.compatibility.migration import getattr_migration
28
+
29
+ # at bottom
30
+ __getattr__ = getattr_migration(__name__)
31
+
32
+ ```
33
+ If the entire old module was removed, add a stub for the module with the following lines:
34
+ ```python
35
+ # at top
36
+ from prefect._internal.compatibility.migration import getattr_migration
37
+
38
+ # at bottom
39
+ __getattr__ = getattr_migration(__name__)
40
+ ```
41
+
42
+ Example at src/prefect/infrastructure/base.py
13
43
  """
14
44
 
15
45
  import sys
@@ -30,14 +60,24 @@ MOVED_IN_V3 = {
30
60
  "prefect.client:get_client": "prefect.client.orchestration:get_client",
31
61
  }
32
62
 
63
+ upgrade_guide_msg = "Refer to the upgrade guide for more information: https://docs.prefect.io/latest/guides/upgrade-guide-agents-to-workers/."
64
+
33
65
  REMOVED_IN_V3 = {
34
66
  "prefect.client.schemas.objects:MinimalDeploymentSchedule": "Use `prefect.client.schemas.actions.DeploymentScheduleCreate` instead.",
67
+ "prefect.context:PrefectObjectRegistry": upgrade_guide_msg,
35
68
  "prefect.deployments.deployments:Deployment": "Use `flow.serve()`, `flow.deploy()`, or `prefect deploy` instead.",
36
69
  "prefect.deployments:Deployment": "Use `flow.serve()`, `flow.deploy()`, or `prefect deploy` instead.",
37
- "prefect.filesystems:GCS": "Use `prefect_gcp` instead.",
38
- "prefect.filesystems:Azure": "Use `prefect_azure` instead.",
39
- "prefect.filesystems:S3": "Use `prefect_aws` instead.",
70
+ "prefect.filesystems:GCS": "Use `prefect_gcp.GcsBucket` instead.",
71
+ "prefect.filesystems:Azure": "Use `prefect_azure.AzureBlobStorageContainer` instead.",
72
+ "prefect.filesystems:S3": "Use `prefect_aws.S3Bucket` instead.",
73
+ "prefect.filesystems:GitHub": "Use `prefect_github.GitHubRepository` instead.",
40
74
  "prefect.engine:_out_of_process_pause": "Use `prefect.flow_runs.pause_flow_run` instead.",
75
+ "prefect.agent:PrefectAgent": "Use workers instead. " + upgrade_guide_msg,
76
+ "prefect.infrastructure:KubernetesJob": "Use workers instead. " + upgrade_guide_msg,
77
+ "prefect.infrastructure.base:Infrastructure": "Use the `BaseWorker` class to create custom infrastructure integrations instead. "
78
+ + upgrade_guide_msg,
79
+ "prefect.workers.block:BlockWorkerJobConfiguration": upgrade_guide_msg,
80
+ "prefect.workers.cloud:BlockWorker": upgrade_guide_msg,
41
81
  }
42
82
 
43
83
  # IMPORTANT FOR USAGE: When adding new modules to MOVED_IN_V3 or REMOVED_IN_V3, include the following lines at the bottom of that module:
@@ -114,7 +154,7 @@ def getattr_migration(module_name: str) -> Callable[[str], Any]:
114
154
  if import_path in REMOVED_IN_V3.keys():
115
155
  error_message = REMOVED_IN_V3[import_path]
116
156
  raise PrefectImportError(
117
- f"{import_path!r} has been removed. {error_message}"
157
+ f"`{import_path}` has been removed. {error_message}"
118
158
  )
119
159
 
120
160
  globals: Dict[str, Any] = sys.modules[module_name].__dict__
@@ -151,7 +151,7 @@ class from_async(_base):
151
151
  __call: Union[Callable[[], T], Call[T]],
152
152
  timeout: Optional[float] = None,
153
153
  done_callbacks: Optional[Iterable[Call]] = None,
154
- ) -> Call[T]:
154
+ ) -> T:
155
155
  call = _cast_to_call(__call)
156
156
  waiter = AsyncWaiter(call=call)
157
157
  for callback in done_callbacks or []:
@@ -0,0 +1,61 @@
1
+ import asyncio
2
+ from functools import wraps
3
+ from typing import Any, Callable, Tuple, Type
4
+
5
+ from prefect.logging.loggers import get_logger
6
+ from prefect.utilities.math import clamped_poisson_interval
7
+
8
+ logger = get_logger("retries")
9
+
10
+
11
+ def exponential_backoff_with_jitter(
12
+ attempt: int, base_delay: float, max_delay: float
13
+ ) -> float:
14
+ average_interval = min(base_delay * (2**attempt), max_delay)
15
+ return clamped_poisson_interval(average_interval, clamping_factor=0.3)
16
+
17
+
18
+ def retry_async_fn(
19
+ max_attempts: int = 3,
20
+ backoff_strategy: Callable[
21
+ [int, float, float], float
22
+ ] = exponential_backoff_with_jitter,
23
+ base_delay: float = 1,
24
+ max_delay: float = 10,
25
+ retry_on_exceptions: Tuple[Type[Exception], ...] = (Exception,),
26
+ ):
27
+ """A decorator for retrying an async function.
28
+
29
+ Args:
30
+ max_attempts: The maximum number of times to retry the function.
31
+ backoff_strategy: A function that takes in the number of attempts, the base
32
+ delay, and the maximum delay, and returns the delay to use for the next
33
+ attempt. Defaults to an exponential backoff with jitter.
34
+ base_delay: The base delay to use for the first attempt.
35
+ max_delay: The maximum delay to use for the last attempt.
36
+ retry_on_exceptions: A tuple of exception types to retry on. Defaults to
37
+ retrying on all exceptions.
38
+ """
39
+
40
+ def decorator(func):
41
+ @wraps(func)
42
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
43
+ for attempt in range(max_attempts):
44
+ try:
45
+ return await func(*args, **kwargs)
46
+ except retry_on_exceptions as e:
47
+ if attempt == max_attempts - 1:
48
+ logger.exception(
49
+ f"Function {func.__name__!r} failed after {max_attempts} attempts"
50
+ )
51
+ raise
52
+ delay = backoff_strategy(attempt, base_delay, max_delay)
53
+ logger.warning(
54
+ f"Attempt {attempt + 1} of function {func.__name__!r} failed with {type(e).__name__}. "
55
+ f"Retrying in {delay:.2f} seconds..."
56
+ )
57
+ await asyncio.sleep(delay)
58
+
59
+ return wrapper
60
+
61
+ return decorator
prefect/agent.py ADDED
@@ -0,0 +1,6 @@
1
+ """
2
+ 2024-06-27: This surfaces an actionable error message for moved or removed objects in Prefect 3.0 upgrade.
3
+ """
4
+ from prefect._internal.compatibility.migration import getattr_migration
5
+
6
+ __getattr__ = getattr_migration(__name__)
prefect/client/cloud.py CHANGED
@@ -9,7 +9,7 @@ from starlette import status
9
9
  import prefect.context
10
10
  import prefect.settings
11
11
  from prefect.client.base import PrefectHttpxAsyncClient
12
- from prefect.client.schemas import Workspace
12
+ from prefect.client.schemas.objects import Workspace
13
13
  from prefect.exceptions import ObjectNotFound, PrefectException
14
14
  from prefect.settings import (
15
15
  PREFECT_API_KEY,
@@ -8,7 +8,6 @@ from typing import (
8
8
  Generic,
9
9
  List,
10
10
  Optional,
11
- TypeVar,
12
11
  Union,
13
12
  overload,
14
13
  )
@@ -26,7 +25,7 @@ from pydantic import (
26
25
  model_validator,
27
26
  )
28
27
  from pydantic_extra_types.pendulum_dt import DateTime
29
- from typing_extensions import Literal, Self
28
+ from typing_extensions import Literal, Self, TypeVar
30
29
 
31
30
  from prefect._internal.compatibility.migration import getattr_migration
32
31
  from prefect._internal.schemas.bases import ObjectBaseModel, PrefectBaseModel
@@ -61,7 +60,7 @@ if TYPE_CHECKING:
61
60
  from prefect.results import BaseResult
62
61
 
63
62
 
64
- R = TypeVar("R")
63
+ R = TypeVar("R", default=Any)
65
64
 
66
65
 
67
66
  DEFAULT_BLOCK_SCHEMA_VERSION = "non-versioned"
@@ -792,7 +791,7 @@ class TaskRun(ObjectBaseModel):
792
791
 
793
792
  state: Optional[State] = Field(
794
793
  default=None,
795
- description="The state of the flow run.",
794
+ description="The state of the task run.",
796
795
  examples=["State(type=StateType.COMPLETED)"],
797
796
  )
798
797
 
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
2
  from contextlib import asynccontextmanager
3
- from typing import List, Literal, Optional, Union, cast
3
+ from typing import AsyncGenerator, List, Literal, Optional, Union, cast
4
4
 
5
5
  import httpx
6
6
  import pendulum
@@ -34,7 +34,7 @@ async def concurrency(
34
34
  names: Union[str, List[str]],
35
35
  occupy: int = 1,
36
36
  timeout_seconds: Optional[float] = None,
37
- ):
37
+ ) -> AsyncGenerator[None, None]:
38
38
  """A context manager that acquires and releases concurrency slots from the
39
39
  given concurrency limits.
40
40
 
@@ -77,7 +77,7 @@ async def concurrency(
77
77
  _emit_concurrency_release_events(limits, occupy, emitted_events)
78
78
 
79
79
 
80
- async def rate_limit(names: Union[str, List[str]], occupy: int = 1):
80
+ async def rate_limit(names: Union[str, List[str]], occupy: int = 1) -> None:
81
81
  """Block execution until an `occupy` number of slots of the concurrency
82
82
  limits given in `names` are acquired. Requires that all given concurrency
83
83
  limits have a slot decay.
@@ -54,6 +54,6 @@ def _emit_concurrency_release_events(
54
54
  limits: List[MinimalConcurrencyLimitResponse],
55
55
  occupy: int,
56
56
  events: Dict[UUID, Optional[Event]],
57
- ):
57
+ ) -> None:
58
58
  for limit in limits:
59
59
  _emit_concurrency_event("released", limit, limits, occupy, events[limit.id])
@@ -3,6 +3,7 @@ import concurrent.futures
3
3
  from contextlib import asynccontextmanager
4
4
  from typing import (
5
5
  TYPE_CHECKING,
6
+ AsyncGenerator,
6
7
  FrozenSet,
7
8
  Optional,
8
9
  Tuple,
@@ -27,14 +28,14 @@ class ConcurrencySlotAcquisitionService(QueueService):
27
28
  self.concurrency_limit_names = sorted(list(concurrency_limit_names))
28
29
 
29
30
  @asynccontextmanager
30
- async def _lifespan(self):
31
+ async def _lifespan(self) -> AsyncGenerator[None, None]:
31
32
  async with get_client() as client:
32
33
  self._client = client
33
34
  yield
34
35
 
35
36
  async def _handle(
36
37
  self, item: Tuple[int, str, Optional[float], concurrent.futures.Future]
37
- ):
38
+ ) -> None:
38
39
  occupy, mode, timeout_seconds, future = item
39
40
  try:
40
41
  response = await self.acquire_slots(occupy, mode, timeout_seconds)
@@ -1,5 +1,15 @@
1
1
  from contextlib import contextmanager
2
- from typing import List, Optional, Union, cast
2
+ from typing import (
3
+ Any,
4
+ Awaitable,
5
+ Callable,
6
+ Generator,
7
+ List,
8
+ Optional,
9
+ TypeVar,
10
+ Union,
11
+ cast,
12
+ )
3
13
 
4
14
  import pendulum
5
15
 
@@ -22,13 +32,15 @@ from .events import (
22
32
  _emit_concurrency_release_events,
23
33
  )
24
34
 
35
+ T = TypeVar("T")
36
+
25
37
 
26
38
  @contextmanager
27
39
  def concurrency(
28
40
  names: Union[str, List[str]],
29
41
  occupy: int = 1,
30
42
  timeout_seconds: Optional[float] = None,
31
- ):
43
+ ) -> Generator[None, None, None]:
32
44
  """A context manager that acquires and releases concurrency slots from the
33
45
  given concurrency limits.
34
46
 
@@ -75,7 +87,7 @@ def concurrency(
75
87
  _emit_concurrency_release_events(limits, occupy, emitted_events)
76
88
 
77
89
 
78
- def rate_limit(names: Union[str, List[str]], occupy: int = 1):
90
+ def rate_limit(names: Union[str, List[str]], occupy: int = 1) -> None:
79
91
  """Block execution until an `occupy` number of slots of the concurrency
80
92
  limits given in `names` are acquired. Requires that all given concurrency
81
93
  limits have a slot decay.
@@ -91,11 +103,13 @@ def rate_limit(names: Union[str, List[str]], occupy: int = 1):
91
103
  _emit_concurrency_acquisition_events(limits, occupy)
92
104
 
93
105
 
94
- def _call_async_function_from_sync(fn, *args, **kwargs):
106
+ def _call_async_function_from_sync(
107
+ fn: Callable[..., Awaitable[T]], *args: Any, **kwargs: Any
108
+ ) -> T:
95
109
  loop = get_running_loop()
96
110
  call = create_call(fn, *args, **kwargs)
97
111
 
98
112
  if loop is not None:
99
113
  return from_sync.call_soon_in_loop_thread(call).result()
100
114
  else:
101
- return call()
115
+ return call() # type: ignore [return-value]
prefect/context.py CHANGED
@@ -9,6 +9,7 @@ For more user-accessible information about the current run, see [`prefect.runtim
9
9
  import os
10
10
  import sys
11
11
  import warnings
12
+ import weakref
12
13
  from contextlib import ExitStack, contextmanager
13
14
  from contextvars import ContextVar, Token
14
15
  from pathlib import Path
@@ -17,6 +18,7 @@ from typing import (
17
18
  Any,
18
19
  Dict,
19
20
  Generator,
21
+ Mapping,
20
22
  Optional,
21
23
  Set,
22
24
  Type,
@@ -32,6 +34,7 @@ from typing_extensions import Self
32
34
  import prefect.logging
33
35
  import prefect.logging.configuration
34
36
  import prefect.settings
37
+ from prefect._internal.compatibility.migration import getattr_migration
35
38
  from prefect.client.orchestration import PrefectClient, SyncPrefectClient, get_client
36
39
  from prefect.client.schemas import FlowRun, TaskRun
37
40
  from prefect.events.worker import EventsWorker
@@ -290,8 +293,12 @@ class EngineContext(RunContext):
290
293
  # Counter for flow pauses
291
294
  observed_flow_pauses: Dict[str, int] = Field(default_factory=dict)
292
295
 
293
- # Tracking for result from task runs in this flow run
294
- task_run_results: Dict[int, State] = Field(default_factory=dict)
296
+ # Tracking for result from task runs in this flow run for dependency tracking
297
+ # Holds the ID of the object returned by the task run and task run state
298
+ # This is a weakref dictionary to avoid undermining garbage collection
299
+ task_run_results: Mapping[int, State] = Field(
300
+ default_factory=weakref.WeakValueDictionary
301
+ )
295
302
 
296
303
  # Events worker to emit events to Prefect Cloud
297
304
  events: Optional[EventsWorker] = None
@@ -608,3 +615,8 @@ def root_settings_context():
608
615
 
609
616
 
610
617
  GLOBAL_SETTINGS_CONTEXT: SettingsContext = root_settings_context()
618
+
619
+
620
+ # 2024-07-02: This surfaces an actionable error message for removed objects
621
+ # in Prefect 3.0 upgrade.
622
+ __getattr__ = getattr_migration(__name__)
@@ -1,20 +1,33 @@
1
+ from typing import TYPE_CHECKING
1
2
  from prefect._internal.compatibility.migration import getattr_migration
2
- import prefect.deployments.base
3
- import prefect.deployments.steps
4
- from prefect.deployments.base import (
5
- initialize_project,
6
- )
7
3
 
8
- from prefect.deployments.runner import (
9
- RunnerDeployment,
10
- deploy,
11
- DockerImage,
12
- EntrypointType,
13
- )
14
4
 
5
+ if TYPE_CHECKING:
6
+ from .flow_runs import run_deployment
7
+ from .base import initialize_project
8
+ from .runner import deploy
15
9
 
16
- from prefect.deployments.flow_runs import (
17
- run_deployment,
18
- )
10
+ _public_api: dict[str, tuple[str, str]] = {
11
+ "initialize_project": (__spec__.parent, ".base"),
12
+ "run_deployment": (__spec__.parent, ".flow_runs"),
13
+ "deploy": (__spec__.parent, ".runner"),
14
+ }
19
15
 
20
- __getattr__ = getattr_migration(__name__)
16
+ # Declare API for type-checkers
17
+ __all__ = ["initialize_project", "deploy", "run_deployment"]
18
+
19
+
20
+ def __getattr__(attr_name: str) -> object:
21
+ dynamic_attr = _public_api.get(attr_name)
22
+ if dynamic_attr is None:
23
+ return getattr_migration(__name__)(attr_name)
24
+
25
+ package, module_name = dynamic_attr
26
+
27
+ from importlib import import_module
28
+
29
+ if module_name == "__module__":
30
+ return import_module(f".{attr_name}", package=package)
31
+ else:
32
+ module = import_module(module_name, package=package)
33
+ return getattr(module, attr_name)
@@ -1,11 +1,14 @@
1
- from typing import TYPE_CHECKING, Any, List, Optional
1
+ from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Union
2
2
 
3
3
  from prefect.client.schemas.actions import DeploymentScheduleCreate
4
4
  from prefect.client.schemas.schedules import is_schedule_type
5
5
 
6
6
  if TYPE_CHECKING:
7
7
  from prefect.client.schemas.schedules import SCHEDULE_TYPES
8
- from prefect.client.types.flexible_schedule_list import FlexibleScheduleList
8
+
9
+ FlexibleScheduleList = Sequence[
10
+ Union[DeploymentScheduleCreate, dict[str, Any], "SCHEDULE_TYPES"]
11
+ ]
9
12
 
10
13
 
11
14
  def create_deployment_schedule_create(
@@ -6,6 +6,7 @@ import os
6
6
  from pathlib import Path
7
7
  from typing import TYPE_CHECKING, Any, Optional
8
8
 
9
+ from prefect._internal.retries import retry_async_fn
9
10
  from prefect.logging.loggers import get_logger
10
11
  from prefect.runner.storage import BlockStorageAdapter, GitRepository, RemoteStorage
11
12
  from prefect.utilities.asyncutils import sync_compatible
@@ -31,6 +32,12 @@ def set_working_directory(directory: str) -> dict:
31
32
  return dict(directory=directory)
32
33
 
33
34
 
35
+ @retry_async_fn(
36
+ max_attempts=3,
37
+ base_delay=1,
38
+ max_delay=10,
39
+ retry_on_exceptions=(RuntimeError,),
40
+ )
34
41
  @sync_compatible
35
42
  async def git_clone(
36
43
  repository: str,
@@ -187,18 +187,18 @@ class EventTrigger(ResourceTrigger):
187
187
  within: Optional[timedelta] = data.get("within")
188
188
 
189
189
  if isinstance(within, (int, float)):
190
- data["within"] = within = timedelta(seconds=within)
190
+ within = timedelta(seconds=within)
191
191
 
192
192
  if posture == Posture.Proactive:
193
193
  if not within or within == timedelta(0):
194
- data["within"] = timedelta(seconds=10.0)
194
+ within = timedelta(seconds=10.0)
195
195
  elif within < timedelta(seconds=10.0):
196
196
  raise ValueError(
197
197
  "`within` for Proactive triggers must be greater than or equal to "
198
198
  "10 seconds"
199
199
  )
200
200
 
201
- return data
201
+ return data | {"within": within} if within else data
202
202
 
203
203
  def describe_for_cli(self, indent: int = 0) -> str:
204
204
  """Return a human-readable description of this trigger for the CLI"""
prefect/exceptions.py CHANGED
@@ -178,7 +178,10 @@ class ParameterTypeError(PrefectException):
178
178
 
179
179
  @classmethod
180
180
  def from_validation_error(cls, exc: ValidationError) -> Self:
181
- bad_params = [f'{".".join(err["loc"])}: {err["msg"]}' for err in exc.errors()]
181
+ bad_params = [
182
+ f'{".".join(str(item) for item in err["loc"])}: {err["msg"]}'
183
+ for err in exc.errors()
184
+ ]
182
185
  msg = "Flow run received invalid parameters:\n - " + "\n - ".join(bad_params)
183
186
  return cls(msg)
184
187
 
prefect/filesystems.py CHANGED
@@ -95,7 +95,7 @@ class LocalFileSystem(WritableFileSystem, WritableDeploymentStorage):
95
95
  def cast_pathlib(cls, value):
96
96
  return stringify_path(value)
97
97
 
98
- def _resolve_path(self, path: str) -> Path:
98
+ def _resolve_path(self, path: str, validate: bool = False) -> Path:
99
99
  # Only resolve the base path at runtime, default to the current directory
100
100
  basepath = (
101
101
  Path(self.basepath).expanduser().resolve()
@@ -114,11 +114,12 @@ class LocalFileSystem(WritableFileSystem, WritableDeploymentStorage):
114
114
  resolved_path = basepath / resolved_path
115
115
  else:
116
116
  resolved_path = resolved_path.resolve()
117
+
118
+ if validate:
117
119
  if basepath not in resolved_path.parents and (basepath != resolved_path):
118
120
  raise ValueError(
119
121
  f"Provided path {resolved_path} is outside of the base path {basepath}."
120
122
  )
121
-
122
123
  return resolved_path
123
124
 
124
125
  @sync_compatible
@@ -184,7 +185,7 @@ class LocalFileSystem(WritableFileSystem, WritableDeploymentStorage):
184
185
  Defaults to copying the entire contents of the current working directory to the block's basepath.
185
186
  An `ignore_file` path may be provided that can include gitignore style expressions for filepaths to ignore.
186
187
  """
187
- destination_path = self._resolve_path(to_path)
188
+ destination_path = self._resolve_path(to_path, validate=True)
188
189
 
189
190
  if not local_path:
190
191
  local_path = Path(".").absolute()