prefect-client 3.0.10__py3-none-any.whl → 3.0.11__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 (67) hide show
  1. prefect/__init__.py +17 -14
  2. prefect/_internal/schemas/bases.py +1 -0
  3. prefect/_internal/schemas/validators.py +5 -3
  4. prefect/_version.py +3 -3
  5. prefect/client/cloud.py +2 -2
  6. prefect/client/orchestration.py +4 -4
  7. prefect/client/schemas/filters.py +14 -0
  8. prefect/context.py +3 -2
  9. prefect/deployments/runner.py +15 -6
  10. prefect/events/schemas/automations.py +3 -3
  11. prefect/events/schemas/deployment_triggers.py +10 -5
  12. prefect/flow_engine.py +4 -4
  13. prefect/flows.py +24 -9
  14. prefect/futures.py +4 -4
  15. prefect/logging/handlers.py +1 -1
  16. prefect/logging/highlighters.py +2 -0
  17. prefect/logging/logging.yml +82 -83
  18. prefect/runner/runner.py +1 -2
  19. prefect/runner/server.py +12 -1
  20. prefect/settings/__init__.py +59 -0
  21. prefect/settings/base.py +131 -0
  22. prefect/settings/constants.py +8 -0
  23. prefect/settings/context.py +65 -0
  24. prefect/settings/legacy.py +167 -0
  25. prefect/settings/models/__init__.py +0 -0
  26. prefect/settings/models/api.py +41 -0
  27. prefect/settings/models/cli.py +31 -0
  28. prefect/settings/models/client.py +90 -0
  29. prefect/settings/models/cloud.py +58 -0
  30. prefect/settings/models/deployments.py +40 -0
  31. prefect/settings/models/flows.py +37 -0
  32. prefect/settings/models/internal.py +21 -0
  33. prefect/settings/models/logging.py +137 -0
  34. prefect/settings/models/results.py +47 -0
  35. prefect/settings/models/root.py +447 -0
  36. prefect/settings/models/runner.py +65 -0
  37. prefect/settings/models/server/__init__.py +1 -0
  38. prefect/settings/models/server/api.py +133 -0
  39. prefect/settings/models/server/database.py +202 -0
  40. prefect/settings/models/server/deployments.py +24 -0
  41. prefect/settings/models/server/ephemeral.py +34 -0
  42. prefect/settings/models/server/events.py +140 -0
  43. prefect/settings/models/server/flow_run_graph.py +34 -0
  44. prefect/settings/models/server/root.py +143 -0
  45. prefect/settings/models/server/services.py +485 -0
  46. prefect/settings/models/server/tasks.py +86 -0
  47. prefect/settings/models/server/ui.py +52 -0
  48. prefect/settings/models/tasks.py +91 -0
  49. prefect/settings/models/testing.py +52 -0
  50. prefect/settings/models/ui.py +0 -0
  51. prefect/settings/models/worker.py +46 -0
  52. prefect/settings/profiles.py +390 -0
  53. prefect/settings/sources.py +162 -0
  54. prefect/task_engine.py +24 -29
  55. prefect/task_runners.py +6 -1
  56. prefect/tasks.py +63 -28
  57. prefect/utilities/asyncutils.py +1 -1
  58. prefect/utilities/engine.py +11 -3
  59. prefect/utilities/services.py +3 -3
  60. prefect/workers/base.py +8 -2
  61. {prefect_client-3.0.10.dist-info → prefect_client-3.0.11.dist-info}/METADATA +2 -2
  62. {prefect_client-3.0.10.dist-info → prefect_client-3.0.11.dist-info}/RECORD +66 -33
  63. prefect/settings.py +0 -2172
  64. /prefect/{profiles.toml → settings/profiles.toml} +0 -0
  65. {prefect_client-3.0.10.dist-info → prefect_client-3.0.11.dist-info}/LICENSE +0 -0
  66. {prefect_client-3.0.10.dist-info → prefect_client-3.0.11.dist-info}/WHEEL +0 -0
  67. {prefect_client-3.0.10.dist-info → prefect_client-3.0.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,91 @@
1
+ from typing import Optional, Union
2
+
3
+ from pydantic import AliasChoices, AliasPath, Field
4
+ from pydantic_settings import SettingsConfigDict
5
+
6
+ from prefect.settings.base import PrefectBaseSettings
7
+
8
+
9
+ class TasksRunnerSettings(PrefectBaseSettings):
10
+ model_config = SettingsConfigDict(
11
+ env_prefix="PREFECT_TASKS_RUNNER_", env_file=".env", extra="ignore"
12
+ )
13
+
14
+ thread_pool_max_workers: Optional[int] = Field(
15
+ default=None,
16
+ gt=0,
17
+ description="The maximum number of workers for ThreadPoolTaskRunner.",
18
+ validation_alias=AliasChoices(
19
+ AliasPath("thread_pool_max_workers"),
20
+ "prefect_tasks_runner_thread_pool_max_workers",
21
+ "prefect_task_runner_thread_pool_max_workers",
22
+ ),
23
+ )
24
+
25
+
26
+ class TasksSchedulingSettings(PrefectBaseSettings):
27
+ model_config = SettingsConfigDict(
28
+ env_prefix="PREFECT_TASKS_SCHEDULING_", env_file=".env", extra="ignore"
29
+ )
30
+
31
+ default_storage_block: Optional[str] = Field(
32
+ default=None,
33
+ description="The `block-type/block-document` slug of a block to use as the default storage for autonomous tasks.",
34
+ validation_alias=AliasChoices(
35
+ AliasPath("default_storage_block"),
36
+ "prefect_tasks_scheduling_default_storage_block",
37
+ "prefect_task_scheduling_default_storage_block",
38
+ ),
39
+ )
40
+
41
+ delete_failed_submissions: bool = Field(
42
+ default=True,
43
+ description="Whether or not to delete failed task submissions from the database.",
44
+ validation_alias=AliasChoices(
45
+ AliasPath("delete_failed_submissions"),
46
+ "prefect_tasks_scheduling_delete_failed_submissions",
47
+ "prefect_task_scheduling_delete_failed_submissions",
48
+ ),
49
+ )
50
+
51
+
52
+ class TasksSettings(PrefectBaseSettings):
53
+ model_config = SettingsConfigDict(
54
+ env_prefix="PREFECT_TASKS_", env_file=".env", extra="ignore"
55
+ )
56
+
57
+ refresh_cache: bool = Field(
58
+ default=False,
59
+ description="If `True`, enables a refresh of cached results: re-executing the task will refresh the cached results.",
60
+ )
61
+
62
+ default_retries: int = Field(
63
+ default=0,
64
+ ge=0,
65
+ description="This value sets the default number of retries for all tasks.",
66
+ validation_alias=AliasChoices(
67
+ AliasPath("default_retries"),
68
+ "prefect_tasks_default_retries",
69
+ "prefect_task_default_retries",
70
+ ),
71
+ )
72
+
73
+ default_retry_delay_seconds: Union[int, float, list[float]] = Field(
74
+ default=0,
75
+ description="This value sets the default retry delay seconds for all tasks.",
76
+ validation_alias=AliasChoices(
77
+ AliasPath("default_retry_delay_seconds"),
78
+ "prefect_tasks_default_retry_delay_seconds",
79
+ "prefect_task_default_retry_delay_seconds",
80
+ ),
81
+ )
82
+
83
+ runner: TasksRunnerSettings = Field(
84
+ default_factory=TasksRunnerSettings,
85
+ description="Settings for controlling task runner behavior",
86
+ )
87
+
88
+ scheduling: TasksSchedulingSettings = Field(
89
+ default_factory=TasksSchedulingSettings,
90
+ description="Settings for controlling client-side task scheduling behavior",
91
+ )
@@ -0,0 +1,52 @@
1
+ from typing import Any, Optional
2
+
3
+ from pydantic import AliasChoices, AliasPath, Field
4
+ from pydantic_settings import SettingsConfigDict
5
+
6
+ from prefect.settings.base import PrefectBaseSettings
7
+
8
+
9
+ class TestingSettings(PrefectBaseSettings):
10
+ model_config = SettingsConfigDict(
11
+ env_prefix="PREFECT_TESTING_", env_file=".env", extra="ignore"
12
+ )
13
+
14
+ test_mode: bool = Field(
15
+ default=False,
16
+ description="If `True`, places the API in test mode. This may modify behavior to facilitate testing.",
17
+ validation_alias=AliasChoices(
18
+ AliasPath("test_mode"),
19
+ "prefect_testing_test_mode",
20
+ "prefect_test_mode",
21
+ ),
22
+ )
23
+
24
+ unit_test_mode: bool = Field(
25
+ default=False,
26
+ description="This setting only exists to facilitate unit testing. If `True`, code is executing in a unit test context. Defaults to `False`.",
27
+ validation_alias=AliasChoices(
28
+ AliasPath("unit_test_mode"),
29
+ "prefect_testing_unit_test_mode",
30
+ "prefect_unit_test_mode",
31
+ ),
32
+ )
33
+
34
+ unit_test_loop_debug: bool = Field(
35
+ default=True,
36
+ description="If `True` turns on debug mode for the unit testing event loop.",
37
+ validation_alias=AliasChoices(
38
+ AliasPath("unit_test_loop_debug"),
39
+ "prefect_testing_unit_test_loop_debug",
40
+ "prefect_unit_test_loop_debug",
41
+ ),
42
+ )
43
+
44
+ test_setting: Optional[Any] = Field(
45
+ default="FOO",
46
+ description="This setting only exists to facilitate unit testing. If in test mode, this setting will return its value. Otherwise, it returns `None`.",
47
+ validation_alias=AliasChoices(
48
+ AliasPath("test_setting"),
49
+ "prefect_testing_test_setting",
50
+ "prefect_test_setting",
51
+ ),
52
+ )
File without changes
@@ -0,0 +1,46 @@
1
+ from pydantic import Field
2
+ from pydantic_settings import SettingsConfigDict
3
+
4
+ from prefect.settings.base import PrefectBaseSettings
5
+
6
+
7
+ class WorkerWebserverSettings(PrefectBaseSettings):
8
+ model_config = SettingsConfigDict(
9
+ env_prefix="PREFECT_WORKER_WEBSERVER_", env_file=".env", extra="ignore"
10
+ )
11
+
12
+ host: str = Field(
13
+ default="0.0.0.0",
14
+ description="The host address the worker's webserver should bind to.",
15
+ )
16
+
17
+ port: int = Field(
18
+ default=8080,
19
+ description="The port the worker's webserver should bind to.",
20
+ )
21
+
22
+
23
+ class WorkerSettings(PrefectBaseSettings):
24
+ model_config = SettingsConfigDict(
25
+ env_prefix="PREFECT_WORKER_", env_file=".env", extra="ignore"
26
+ )
27
+
28
+ heartbeat_seconds: float = Field(
29
+ default=30,
30
+ description="Number of seconds a worker should wait between sending a heartbeat.",
31
+ )
32
+
33
+ query_seconds: float = Field(
34
+ default=10,
35
+ description="Number of seconds a worker should wait between queries for scheduled work.",
36
+ )
37
+
38
+ prefetch_seconds: float = Field(
39
+ default=10,
40
+ description="The number of seconds into the future a worker should query for scheduled work.",
41
+ )
42
+
43
+ webserver: WorkerWebserverSettings = Field(
44
+ default_factory=WorkerWebserverSettings,
45
+ description="Settings for a worker's webserver",
46
+ )
@@ -0,0 +1,390 @@
1
+ import inspect
2
+ import warnings
3
+ from pathlib import Path
4
+ from typing import Annotated, Any, Dict, Iterable, List, Optional, Set, Tuple, Union
5
+
6
+ import toml
7
+ from pydantic import (
8
+ BaseModel,
9
+ BeforeValidator,
10
+ ConfigDict,
11
+ Field,
12
+ TypeAdapter,
13
+ ValidationError,
14
+ )
15
+ from pydantic_settings import BaseSettings
16
+
17
+ from prefect.exceptions import ProfileSettingsValidationError
18
+ from prefect.settings.constants import DEFAULT_PROFILES_PATH
19
+ from prefect.settings.context import get_current_settings
20
+ from prefect.settings.legacy import Setting, _get_settings_fields
21
+ from prefect.settings.models.root import Settings
22
+
23
+
24
+ def _cast_settings(
25
+ settings: Union[Dict[Union[str, Setting], Any], Any],
26
+ ) -> Dict[Setting, Any]:
27
+ """For backwards compatibility, allow either Settings objects as keys or string references to settings."""
28
+ if not isinstance(settings, dict):
29
+ raise ValueError("Settings must be a dictionary.")
30
+ casted_settings = {}
31
+ for k, value in settings.items():
32
+ try:
33
+ if isinstance(k, str):
34
+ setting = _get_settings_fields(Settings)[k]
35
+ else:
36
+ setting = k
37
+ casted_settings[setting] = value
38
+ except KeyError as e:
39
+ warnings.warn(f"Setting {e} is not recognized")
40
+ continue
41
+ return casted_settings
42
+
43
+
44
+ ############################################################################
45
+ # Profiles
46
+
47
+
48
+ class Profile(BaseModel):
49
+ """A user profile containing settings."""
50
+
51
+ model_config = ConfigDict(extra="ignore", arbitrary_types_allowed=True)
52
+
53
+ name: str
54
+ settings: Annotated[Dict[Setting, Any], BeforeValidator(_cast_settings)] = Field(
55
+ default_factory=dict
56
+ )
57
+ source: Optional[Path] = None
58
+
59
+ def to_environment_variables(self) -> Dict[str, str]:
60
+ """Convert the profile settings to a dictionary of environment variables."""
61
+ return {
62
+ setting.name: str(value)
63
+ for setting, value in self.settings.items()
64
+ if value is not None
65
+ }
66
+
67
+ def validate_settings(self):
68
+ errors: List[Tuple[Setting, ValidationError]] = []
69
+ for setting, value in self.settings.items():
70
+ try:
71
+ model_fields = Settings.model_fields
72
+ annotation = None
73
+ for section in setting.accessor.split("."):
74
+ annotation = model_fields[section].annotation
75
+ if inspect.isclass(annotation) and issubclass(
76
+ annotation, BaseSettings
77
+ ):
78
+ model_fields = annotation.model_fields
79
+
80
+ TypeAdapter(annotation).validate_python(value)
81
+ except ValidationError as e:
82
+ errors.append((setting, e))
83
+ if errors:
84
+ raise ProfileSettingsValidationError(errors)
85
+
86
+
87
+ class ProfilesCollection:
88
+ """ "
89
+ A utility class for working with a collection of profiles.
90
+
91
+ Profiles in the collection must have unique names.
92
+
93
+ The collection may store the name of the active profile.
94
+ """
95
+
96
+ def __init__(
97
+ self, profiles: Iterable[Profile], active: Optional[str] = None
98
+ ) -> None:
99
+ self.profiles_by_name = {profile.name: profile for profile in profiles}
100
+ self.active_name = active
101
+
102
+ @property
103
+ def names(self) -> Set[str]:
104
+ """
105
+ Return a set of profile names in this collection.
106
+ """
107
+ return set(self.profiles_by_name.keys())
108
+
109
+ @property
110
+ def active_profile(self) -> Optional[Profile]:
111
+ """
112
+ Retrieve the active profile in this collection.
113
+ """
114
+ if self.active_name is None:
115
+ return None
116
+ return self[self.active_name]
117
+
118
+ def set_active(self, name: Optional[str], check: bool = True):
119
+ """
120
+ Set the active profile name in the collection.
121
+
122
+ A null value may be passed to indicate that this collection does not determine
123
+ the active profile.
124
+ """
125
+ if check and name is not None and name not in self.names:
126
+ raise ValueError(f"Unknown profile name {name!r}.")
127
+ self.active_name = name
128
+
129
+ def update_profile(
130
+ self,
131
+ name: str,
132
+ settings: Dict[Setting, Any],
133
+ source: Optional[Path] = None,
134
+ ) -> Profile:
135
+ """
136
+ Add a profile to the collection or update the existing on if the name is already
137
+ present in this collection.
138
+
139
+ If updating an existing profile, the settings will be merged. Settings can
140
+ be dropped from the existing profile by setting them to `None` in the new
141
+ profile.
142
+
143
+ Returns the new profile object.
144
+ """
145
+ existing = self.profiles_by_name.get(name)
146
+
147
+ # Convert the input to a `Profile` to cast settings to the correct type
148
+ profile = Profile(name=name, settings=settings, source=source)
149
+
150
+ if existing:
151
+ new_settings = {**existing.settings, **profile.settings}
152
+
153
+ # Drop null keys to restore to default
154
+ for key, value in tuple(new_settings.items()):
155
+ if value is None:
156
+ new_settings.pop(key)
157
+
158
+ new_profile = Profile(
159
+ name=profile.name,
160
+ settings=new_settings,
161
+ source=source or profile.source,
162
+ )
163
+ else:
164
+ new_profile = profile
165
+
166
+ self.profiles_by_name[new_profile.name] = new_profile
167
+
168
+ return new_profile
169
+
170
+ def add_profile(self, profile: Profile) -> None:
171
+ """
172
+ Add a profile to the collection.
173
+
174
+ If the profile name already exists, an exception will be raised.
175
+ """
176
+ if profile.name in self.profiles_by_name:
177
+ raise ValueError(
178
+ f"Profile name {profile.name!r} already exists in collection."
179
+ )
180
+
181
+ self.profiles_by_name[profile.name] = profile
182
+
183
+ def remove_profile(self, name: str) -> None:
184
+ """
185
+ Remove a profile from the collection.
186
+ """
187
+ self.profiles_by_name.pop(name)
188
+
189
+ def without_profile_source(self, path: Optional[Path]) -> "ProfilesCollection":
190
+ """
191
+ Remove profiles that were loaded from a given path.
192
+
193
+ Returns a new collection.
194
+ """
195
+ return ProfilesCollection(
196
+ [
197
+ profile
198
+ for profile in self.profiles_by_name.values()
199
+ if profile.source != path
200
+ ],
201
+ active=self.active_name,
202
+ )
203
+
204
+ def to_dict(self):
205
+ """
206
+ Convert to a dictionary suitable for writing to disk.
207
+ """
208
+ return {
209
+ "active": self.active_name,
210
+ "profiles": {
211
+ profile.name: profile.to_environment_variables()
212
+ for profile in self.profiles_by_name.values()
213
+ },
214
+ }
215
+
216
+ def __getitem__(self, name: str) -> Profile:
217
+ return self.profiles_by_name[name]
218
+
219
+ def __iter__(self):
220
+ return self.profiles_by_name.__iter__()
221
+
222
+ def items(self):
223
+ return self.profiles_by_name.items()
224
+
225
+ def __eq__(self, __o: object) -> bool:
226
+ if not isinstance(__o, ProfilesCollection):
227
+ return False
228
+
229
+ return (
230
+ self.profiles_by_name == __o.profiles_by_name
231
+ and self.active_name == __o.active_name
232
+ )
233
+
234
+ def __repr__(self) -> str:
235
+ return (
236
+ f"ProfilesCollection(profiles={list(self.profiles_by_name.values())!r},"
237
+ f" active={self.active_name!r})>"
238
+ )
239
+
240
+
241
+ def _read_profiles_from(path: Path) -> ProfilesCollection:
242
+ """
243
+ Read profiles from a path into a new `ProfilesCollection`.
244
+
245
+ Profiles are expected to be written in TOML with the following schema:
246
+ ```
247
+ active = <name: Optional[str]>
248
+
249
+ [profiles.<name: str>]
250
+ <SETTING: str> = <value: Any>
251
+ ```
252
+ """
253
+ contents = toml.loads(path.read_text())
254
+ active_profile = contents.get("active")
255
+ raw_profiles = contents.get("profiles", {})
256
+
257
+ profiles = []
258
+ for name, settings in raw_profiles.items():
259
+ profiles.append(Profile(name=name, settings=settings, source=path))
260
+
261
+ return ProfilesCollection(profiles, active=active_profile)
262
+
263
+
264
+ def _write_profiles_to(path: Path, profiles: ProfilesCollection) -> None:
265
+ """
266
+ Write profiles in the given collection to a path as TOML.
267
+
268
+ Any existing data not present in the given `profiles` will be deleted.
269
+ """
270
+ if not path.exists():
271
+ path.parent.mkdir(parents=True, exist_ok=True)
272
+ path.touch(mode=0o600)
273
+ path.write_text(toml.dumps(profiles.to_dict()))
274
+
275
+
276
+ def load_profiles(include_defaults: bool = True) -> ProfilesCollection:
277
+ """
278
+ Load profiles from the current profile path. Optionally include profiles from the
279
+ default profile path.
280
+ """
281
+ current_settings = get_current_settings()
282
+ default_profiles = _read_profiles_from(DEFAULT_PROFILES_PATH)
283
+
284
+ if current_settings.profiles_path is None:
285
+ raise RuntimeError(
286
+ "No profiles path set; please ensure `PREFECT_PROFILES_PATH` is set."
287
+ )
288
+
289
+ if not include_defaults:
290
+ if not current_settings.profiles_path.exists():
291
+ return ProfilesCollection([])
292
+ return _read_profiles_from(current_settings.profiles_path)
293
+
294
+ user_profiles_path = current_settings.profiles_path
295
+ profiles = default_profiles
296
+ if user_profiles_path.exists():
297
+ user_profiles = _read_profiles_from(user_profiles_path)
298
+
299
+ # Merge all of the user profiles with the defaults
300
+ for name in user_profiles:
301
+ if not (source := user_profiles[name].source):
302
+ raise ValueError(f"Profile {name!r} has no source.")
303
+ profiles.update_profile(
304
+ name,
305
+ settings=user_profiles[name].settings,
306
+ source=source,
307
+ )
308
+
309
+ if user_profiles.active_name:
310
+ profiles.set_active(user_profiles.active_name, check=False)
311
+
312
+ return profiles
313
+
314
+
315
+ def load_current_profile():
316
+ """
317
+ Load the current profile from the default and current profile paths.
318
+
319
+ This will _not_ include settings from the current settings context. Only settings
320
+ that have been persisted to the profiles file will be saved.
321
+ """
322
+ import prefect.context
323
+
324
+ profiles = load_profiles()
325
+ context = prefect.context.get_settings_context()
326
+
327
+ if context:
328
+ profiles.set_active(context.profile.name)
329
+
330
+ return profiles.active_profile
331
+
332
+
333
+ def save_profiles(profiles: ProfilesCollection) -> None:
334
+ """
335
+ Writes all non-default profiles to the current profiles path.
336
+ """
337
+ profiles_path = get_current_settings().profiles_path
338
+ assert profiles_path is not None, "Profiles path is not set."
339
+ profiles = profiles.without_profile_source(DEFAULT_PROFILES_PATH)
340
+ return _write_profiles_to(profiles_path, profiles)
341
+
342
+
343
+ def load_profile(name: str) -> Profile:
344
+ """
345
+ Load a single profile by name.
346
+ """
347
+ profiles = load_profiles()
348
+ try:
349
+ return profiles[name]
350
+ except KeyError:
351
+ raise ValueError(f"Profile {name!r} not found.")
352
+
353
+
354
+ def update_current_profile(
355
+ settings: Dict[Union[str, Setting], Any],
356
+ ) -> Profile:
357
+ """
358
+ Update the persisted data for the profile currently in-use.
359
+
360
+ If the profile does not exist in the profiles file, it will be created.
361
+
362
+ Given settings will be merged with the existing settings as described in
363
+ `ProfilesCollection.update_profile`.
364
+
365
+ Returns:
366
+ The new profile.
367
+ """
368
+ import prefect.context
369
+
370
+ current_profile = prefect.context.get_settings_context().profile
371
+
372
+ if not current_profile:
373
+ from prefect.exceptions import MissingProfileError
374
+
375
+ raise MissingProfileError("No profile is currently in use.")
376
+
377
+ profiles = load_profiles()
378
+
379
+ # Ensure the current profile's settings are present
380
+ profiles.update_profile(current_profile.name, current_profile.settings)
381
+ # Then merge the new settings in
382
+ new_profile = profiles.update_profile(
383
+ current_profile.name, _cast_settings(settings)
384
+ )
385
+
386
+ new_profile.validate_settings()
387
+
388
+ save_profiles(profiles)
389
+
390
+ return profiles[current_profile.name]