prefect-client 3.0.0rc1__py3-none-any.whl → 3.0.0rc3__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.
- prefect/_internal/compatibility/migration.py +124 -0
- prefect/_internal/concurrency/__init__.py +2 -2
- prefect/_internal/concurrency/primitives.py +1 -0
- prefect/_internal/pydantic/annotations/pendulum.py +2 -2
- prefect/_internal/pytz.py +1 -1
- prefect/blocks/core.py +1 -1
- prefect/blocks/redis.py +168 -0
- prefect/client/orchestration.py +113 -23
- prefect/client/schemas/actions.py +1 -1
- prefect/client/schemas/filters.py +6 -0
- prefect/client/schemas/objects.py +22 -11
- prefect/client/subscriptions.py +3 -2
- prefect/concurrency/asyncio.py +1 -1
- prefect/concurrency/services.py +1 -1
- prefect/context.py +1 -27
- prefect/deployments/__init__.py +3 -0
- prefect/deployments/base.py +11 -3
- prefect/deployments/deployments.py +3 -0
- prefect/deployments/steps/pull.py +1 -0
- prefect/deployments/steps/utility.py +2 -1
- prefect/engine.py +3 -0
- prefect/events/cli/automations.py +1 -1
- prefect/events/clients.py +7 -1
- prefect/events/schemas/events.py +2 -0
- prefect/exceptions.py +9 -0
- prefect/filesystems.py +22 -11
- prefect/flow_engine.py +118 -156
- prefect/flow_runs.py +2 -2
- prefect/flows.py +91 -35
- prefect/futures.py +44 -43
- prefect/infrastructure/provisioners/container_instance.py +1 -0
- prefect/infrastructure/provisioners/ecs.py +2 -2
- prefect/input/__init__.py +4 -0
- prefect/input/run_input.py +4 -2
- prefect/logging/formatters.py +2 -2
- prefect/logging/handlers.py +2 -2
- prefect/logging/loggers.py +1 -1
- prefect/plugins.py +1 -0
- prefect/records/cache_policies.py +179 -0
- prefect/records/result_store.py +10 -3
- prefect/results.py +27 -55
- prefect/runner/runner.py +1 -1
- prefect/runner/server.py +1 -1
- prefect/runtime/__init__.py +1 -0
- prefect/runtime/deployment.py +1 -0
- prefect/runtime/flow_run.py +1 -0
- prefect/runtime/task_run.py +1 -0
- prefect/settings.py +21 -5
- prefect/states.py +17 -4
- prefect/task_engine.py +337 -209
- prefect/task_runners.py +15 -5
- prefect/task_runs.py +203 -0
- prefect/{task_server.py → task_worker.py} +66 -36
- prefect/tasks.py +180 -77
- prefect/transactions.py +92 -16
- prefect/types/__init__.py +1 -1
- prefect/utilities/asyncutils.py +3 -3
- prefect/utilities/callables.py +90 -7
- prefect/utilities/dockerutils.py +5 -3
- prefect/utilities/engine.py +11 -0
- prefect/utilities/filesystem.py +4 -5
- prefect/utilities/importtools.py +34 -5
- prefect/utilities/services.py +2 -2
- prefect/utilities/urls.py +195 -0
- prefect/utilities/visualization.py +1 -0
- prefect/variables.py +19 -10
- prefect/workers/base.py +46 -1
- {prefect_client-3.0.0rc1.dist-info → prefect_client-3.0.0rc3.dist-info}/METADATA +3 -2
- {prefect_client-3.0.0rc1.dist-info → prefect_client-3.0.0rc3.dist-info}/RECORD +72 -66
- {prefect_client-3.0.0rc1.dist-info → prefect_client-3.0.0rc3.dist-info}/LICENSE +0 -0
- {prefect_client-3.0.0rc1.dist-info → prefect_client-3.0.0rc3.dist-info}/WHEEL +0 -0
- {prefect_client-3.0.0rc1.dist-info → prefect_client-3.0.0rc3.dist-info}/top_level.txt +0 -0
prefect/utilities/callables.py
CHANGED
@@ -44,11 +44,23 @@ def get_call_parameters(
|
|
44
44
|
apply_defaults: bool = True,
|
45
45
|
) -> Dict[str, Any]:
|
46
46
|
"""
|
47
|
-
Bind a call to a function to get parameter/value mapping. Default values on
|
48
|
-
signature will be included if not overridden.
|
49
|
-
|
50
|
-
|
47
|
+
Bind a call to a function to get parameter/value mapping. Default values on
|
48
|
+
the signature will be included if not overridden.
|
49
|
+
|
50
|
+
If the function has a `__prefect_self__` attribute, it will be included as
|
51
|
+
the first parameter. This attribute is set when Prefect decorates a bound
|
52
|
+
method, so this approach allows Prefect to work with bound methods in a way
|
53
|
+
that is consistent with how Python handles them (i.e. users don't have to
|
54
|
+
pass the instance argument to the method) while still making the implicit self
|
55
|
+
argument visible to all of Prefect's parameter machinery (such as cache key
|
56
|
+
functions).
|
57
|
+
|
58
|
+
Raises a ParameterBindError if the arguments/kwargs are not valid for the
|
59
|
+
function
|
51
60
|
"""
|
61
|
+
if hasattr(fn, "__prefect_self__"):
|
62
|
+
call_args = (fn.__prefect_self__,) + call_args
|
63
|
+
|
52
64
|
try:
|
53
65
|
bound_signature = inspect.signature(fn).bind(*call_args, **call_kwargs)
|
54
66
|
except TypeError as exc:
|
@@ -456,7 +468,14 @@ def _generate_signature_from_source(
|
|
456
468
|
(
|
457
469
|
node
|
458
470
|
for node in ast.walk(parsed_code)
|
459
|
-
if isinstance(
|
471
|
+
if isinstance(
|
472
|
+
node,
|
473
|
+
(
|
474
|
+
ast.FunctionDef,
|
475
|
+
ast.AsyncFunctionDef,
|
476
|
+
),
|
477
|
+
)
|
478
|
+
and node.name == func_name
|
460
479
|
),
|
461
480
|
None,
|
462
481
|
)
|
@@ -464,6 +483,26 @@ def _generate_signature_from_source(
|
|
464
483
|
raise ValueError(f"Function {func_name} not found in source code")
|
465
484
|
parameters = []
|
466
485
|
|
486
|
+
# Handle annotations for positional only args e.g. def func(a, /, b, c)
|
487
|
+
for arg in func_def.args.posonlyargs:
|
488
|
+
name = arg.arg
|
489
|
+
annotation = arg.annotation
|
490
|
+
if annotation is not None:
|
491
|
+
try:
|
492
|
+
ann_code = compile(ast.Expression(annotation), "<string>", "eval")
|
493
|
+
annotation = eval(ann_code, namespace)
|
494
|
+
except Exception as e:
|
495
|
+
logger.debug("Failed to evaluate annotation for %s: %s", name, e)
|
496
|
+
annotation = inspect.Parameter.empty
|
497
|
+
else:
|
498
|
+
annotation = inspect.Parameter.empty
|
499
|
+
|
500
|
+
param = inspect.Parameter(
|
501
|
+
name, inspect.Parameter.POSITIONAL_ONLY, annotation=annotation
|
502
|
+
)
|
503
|
+
parameters.append(param)
|
504
|
+
|
505
|
+
# Determine the annotations for args e.g. def func(a: int, b: str, c: float)
|
467
506
|
for arg in func_def.args.args:
|
468
507
|
name = arg.arg
|
469
508
|
annotation = arg.annotation
|
@@ -486,6 +525,7 @@ def _generate_signature_from_source(
|
|
486
525
|
)
|
487
526
|
parameters.append(param)
|
488
527
|
|
528
|
+
# Handle default values for args e.g. def func(a=1, b="hello", c=3.14)
|
489
529
|
defaults = [None] * (
|
490
530
|
len(func_def.args.args) - len(func_def.args.defaults)
|
491
531
|
) + func_def.args.defaults
|
@@ -501,6 +541,42 @@ def _generate_signature_from_source(
|
|
501
541
|
default = None # Set to None if evaluation fails
|
502
542
|
parameters[parameters.index(param)] = param.replace(default=default)
|
503
543
|
|
544
|
+
# Handle annotations for keyword only args e.g. def func(*, a: int, b: str)
|
545
|
+
for kwarg in func_def.args.kwonlyargs:
|
546
|
+
name = kwarg.arg
|
547
|
+
annotation = kwarg.annotation
|
548
|
+
if annotation is not None:
|
549
|
+
try:
|
550
|
+
ann_code = compile(ast.Expression(annotation), "<string>", "eval")
|
551
|
+
annotation = eval(ann_code, namespace)
|
552
|
+
except Exception as e:
|
553
|
+
logger.debug("Failed to evaluate annotation for %s: %s", name, e)
|
554
|
+
annotation = inspect.Parameter.empty
|
555
|
+
else:
|
556
|
+
annotation = inspect.Parameter.empty
|
557
|
+
|
558
|
+
param = inspect.Parameter(
|
559
|
+
name, inspect.Parameter.KEYWORD_ONLY, annotation=annotation
|
560
|
+
)
|
561
|
+
parameters.append(param)
|
562
|
+
|
563
|
+
# Handle default values for keyword only args e.g. def func(*, a=1, b="hello")
|
564
|
+
defaults = [None] * (
|
565
|
+
len(func_def.args.kwonlyargs) - len(func_def.args.kw_defaults)
|
566
|
+
) + func_def.args.kw_defaults
|
567
|
+
for param, default in zip(parameters[-len(func_def.args.kwonlyargs) :], defaults):
|
568
|
+
if default is not None:
|
569
|
+
try:
|
570
|
+
def_code = compile(ast.Expression(default), "<string>", "eval")
|
571
|
+
default = eval(def_code, namespace)
|
572
|
+
except Exception as e:
|
573
|
+
logger.debug(
|
574
|
+
"Failed to evaluate default value for %s: %s", param.name, e
|
575
|
+
)
|
576
|
+
default = None
|
577
|
+
parameters[parameters.index(param)] = param.replace(default=default)
|
578
|
+
|
579
|
+
# Handle annotations for varargs and kwargs e.g. def func(*args: int, **kwargs: str)
|
504
580
|
if func_def.args.vararg:
|
505
581
|
parameters.append(
|
506
582
|
inspect.Parameter(
|
@@ -512,7 +588,7 @@ def _generate_signature_from_source(
|
|
512
588
|
inspect.Parameter(func_def.args.kwarg.arg, inspect.Parameter.VAR_KEYWORD)
|
513
589
|
)
|
514
590
|
|
515
|
-
# Handle return annotation
|
591
|
+
# Handle return annotation e.g. def func() -> int
|
516
592
|
return_annotation = func_def.returns
|
517
593
|
if return_annotation is not None:
|
518
594
|
try:
|
@@ -544,7 +620,14 @@ def _get_docstring_from_source(source_code: str, func_name: str) -> Optional[str
|
|
544
620
|
(
|
545
621
|
node
|
546
622
|
for node in ast.walk(parsed_code)
|
547
|
-
if isinstance(
|
623
|
+
if isinstance(
|
624
|
+
node,
|
625
|
+
(
|
626
|
+
ast.FunctionDef,
|
627
|
+
ast.AsyncFunctionDef,
|
628
|
+
),
|
629
|
+
)
|
630
|
+
and node.name == func_name
|
548
631
|
),
|
549
632
|
None,
|
550
633
|
)
|
prefect/utilities/dockerutils.py
CHANGED
@@ -41,7 +41,9 @@ def python_version_micro() -> str:
|
|
41
41
|
|
42
42
|
|
43
43
|
def get_prefect_image_name(
|
44
|
-
prefect_version: str = None,
|
44
|
+
prefect_version: Optional[str] = None,
|
45
|
+
python_version: Optional[str] = None,
|
46
|
+
flavor: Optional[str] = None,
|
45
47
|
) -> str:
|
46
48
|
"""
|
47
49
|
Get the Prefect image name matching the current Prefect and Python versions.
|
@@ -138,7 +140,7 @@ def build_image(
|
|
138
140
|
dockerfile: str = "Dockerfile",
|
139
141
|
tag: Optional[str] = None,
|
140
142
|
pull: bool = False,
|
141
|
-
platform: str = None,
|
143
|
+
platform: Optional[str] = None,
|
142
144
|
stream_progress_to: Optional[TextIO] = None,
|
143
145
|
**kwargs,
|
144
146
|
) -> str:
|
@@ -209,7 +211,7 @@ class ImageBuilder:
|
|
209
211
|
self,
|
210
212
|
base_image: str,
|
211
213
|
base_directory: Path = None,
|
212
|
-
platform: str = None,
|
214
|
+
platform: Optional[str] = None,
|
213
215
|
context: Path = None,
|
214
216
|
):
|
215
217
|
"""Create an ImageBuilder
|
prefect/utilities/engine.py
CHANGED
@@ -786,6 +786,17 @@ def resolve_to_final_result(expr, context):
|
|
786
786
|
raise StopVisiting()
|
787
787
|
|
788
788
|
if isinstance(expr, NewPrefectFuture):
|
789
|
+
upstream_task_run = context.get("current_task_run")
|
790
|
+
upstream_task = context.get("current_task")
|
791
|
+
if (
|
792
|
+
upstream_task
|
793
|
+
and upstream_task_run
|
794
|
+
and expr.task_run_id == upstream_task_run.id
|
795
|
+
):
|
796
|
+
raise ValueError(
|
797
|
+
f"Discovered a task depending on itself. Raising to avoid a deadlock. Please inspect the inputs and dependencies of {upstream_task.name}."
|
798
|
+
)
|
799
|
+
|
789
800
|
expr.wait()
|
790
801
|
state = expr.state
|
791
802
|
elif isinstance(expr, State):
|
prefect/utilities/filesystem.py
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
"""
|
2
2
|
Utilities for working with file systems
|
3
3
|
"""
|
4
|
+
|
4
5
|
import os
|
5
6
|
import pathlib
|
6
7
|
import threading
|
7
8
|
from contextlib import contextmanager
|
8
9
|
from pathlib import Path, PureWindowsPath
|
9
|
-
from typing import Union
|
10
|
+
from typing import Optional, Union
|
10
11
|
|
11
12
|
import fsspec
|
12
13
|
import pathspec
|
@@ -32,7 +33,7 @@ def create_default_ignore_file(path: str) -> bool:
|
|
32
33
|
|
33
34
|
|
34
35
|
def filter_files(
|
35
|
-
root: str = ".", ignore_patterns: list = None, include_dirs: bool = True
|
36
|
+
root: str = ".", ignore_patterns: Optional[list] = None, include_dirs: bool = True
|
36
37
|
) -> set:
|
37
38
|
"""
|
38
39
|
This function accepts a root directory path and a list of file patterns to ignore, and returns
|
@@ -40,9 +41,7 @@ def filter_files(
|
|
40
41
|
|
41
42
|
The specification matches that of [.gitignore files](https://git-scm.com/docs/gitignore).
|
42
43
|
"""
|
43
|
-
|
44
|
-
ignore_patterns = []
|
45
|
-
spec = pathspec.PathSpec.from_lines("gitwildmatch", ignore_patterns)
|
44
|
+
spec = pathspec.PathSpec.from_lines("gitwildmatch", ignore_patterns or [])
|
46
45
|
ignored_files = {p.path for p in spec.match_tree_entries(root)}
|
47
46
|
if include_dirs:
|
48
47
|
all_files = {p.path for p in pathspec.util.iter_tree_entries(root)}
|
prefect/utilities/importtools.py
CHANGED
@@ -378,7 +378,16 @@ def safe_load_namespace(source_code: str):
|
|
378
378
|
"""
|
379
379
|
parsed_code = ast.parse(source_code)
|
380
380
|
|
381
|
-
namespace = {}
|
381
|
+
namespace = {"__name__": "prefect_safe_namespace_loader"}
|
382
|
+
|
383
|
+
# Remove the body of the if __name__ == "__main__": block from the AST to prevent
|
384
|
+
# execution of guarded code
|
385
|
+
new_body = []
|
386
|
+
for node in parsed_code.body:
|
387
|
+
if _is_main_block(node):
|
388
|
+
continue
|
389
|
+
new_body.append(node)
|
390
|
+
parsed_code.body = new_body
|
382
391
|
|
383
392
|
# Walk through the AST and find all import statements
|
384
393
|
for node in ast.walk(parsed_code):
|
@@ -412,11 +421,11 @@ def safe_load_namespace(source_code: str):
|
|
412
421
|
except ImportError as e:
|
413
422
|
logger.debug("Failed to import from %s: %s", node.module, e)
|
414
423
|
|
415
|
-
# Handle local
|
424
|
+
# Handle local definitions
|
416
425
|
for node in ast.walk(parsed_code):
|
417
|
-
if isinstance(node, (ast.ClassDef, ast.FunctionDef)):
|
426
|
+
if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.Assign)):
|
418
427
|
try:
|
419
|
-
# Compile and execute each class and function definition
|
428
|
+
# Compile and execute each class and function definition and assignment
|
420
429
|
code = compile(
|
421
430
|
ast.Module(body=[node], type_ignores=[]),
|
422
431
|
filename="<ast>",
|
@@ -424,5 +433,25 @@ def safe_load_namespace(source_code: str):
|
|
424
433
|
)
|
425
434
|
exec(code, namespace)
|
426
435
|
except Exception as e:
|
427
|
-
logger.debug("Failed to compile
|
436
|
+
logger.debug("Failed to compile: %s", e)
|
428
437
|
return namespace
|
438
|
+
|
439
|
+
|
440
|
+
def _is_main_block(node: ast.AST):
|
441
|
+
"""
|
442
|
+
Check if the node is an `if __name__ == "__main__":` block.
|
443
|
+
"""
|
444
|
+
if isinstance(node, ast.If):
|
445
|
+
try:
|
446
|
+
# Check if the condition is `if __name__ == "__main__":`
|
447
|
+
if (
|
448
|
+
isinstance(node.test, ast.Compare)
|
449
|
+
and isinstance(node.test.left, ast.Name)
|
450
|
+
and node.test.left.id == "__name__"
|
451
|
+
and isinstance(node.test.comparators[0], ast.Constant)
|
452
|
+
and node.test.comparators[0].value == "__main__"
|
453
|
+
):
|
454
|
+
return True
|
455
|
+
except AttributeError:
|
456
|
+
pass
|
457
|
+
return False
|
prefect/utilities/services.py
CHANGED
@@ -2,7 +2,7 @@ import sys
|
|
2
2
|
from collections import deque
|
3
3
|
from traceback import format_exception
|
4
4
|
from types import TracebackType
|
5
|
-
from typing import Callable, Coroutine, Deque, Tuple
|
5
|
+
from typing import Callable, Coroutine, Deque, Optional, Tuple
|
6
6
|
|
7
7
|
import anyio
|
8
8
|
import httpx
|
@@ -22,7 +22,7 @@ async def critical_service_loop(
|
|
22
22
|
backoff: int = 1,
|
23
23
|
printer: Callable[..., None] = print,
|
24
24
|
run_once: bool = False,
|
25
|
-
jitter_range: float = None,
|
25
|
+
jitter_range: Optional[float] = None,
|
26
26
|
):
|
27
27
|
"""
|
28
28
|
Runs the given `workload` function on the specified `interval`, while being
|
@@ -0,0 +1,195 @@
|
|
1
|
+
import inspect
|
2
|
+
import urllib.parse
|
3
|
+
from typing import Any, Literal, Optional, Union
|
4
|
+
from uuid import UUID
|
5
|
+
|
6
|
+
from pydantic import BaseModel
|
7
|
+
|
8
|
+
from prefect import settings
|
9
|
+
from prefect.blocks.core import Block
|
10
|
+
from prefect.events.schemas.automations import Automation
|
11
|
+
from prefect.events.schemas.events import ReceivedEvent, Resource
|
12
|
+
from prefect.futures import PrefectFuture
|
13
|
+
from prefect.logging.loggers import get_logger
|
14
|
+
from prefect.variables import Variable
|
15
|
+
|
16
|
+
logger = get_logger("utilities.urls")
|
17
|
+
|
18
|
+
# The following objects are excluded from UI URL generation because we lack a
|
19
|
+
# directly-addressable URL:
|
20
|
+
# worker
|
21
|
+
# artifact
|
22
|
+
# variable
|
23
|
+
# saved-search
|
24
|
+
UI_URL_FORMATS = {
|
25
|
+
"flow": "flows/flow/{obj_id}",
|
26
|
+
"flow-run": "runs/flow-run/{obj_id}",
|
27
|
+
"task-run": "runs/task-run/{obj_id}",
|
28
|
+
"block": "blocks/block/{obj_id}",
|
29
|
+
"block-document": "blocks/block/{obj_id}",
|
30
|
+
"work-pool": "work-pools/work-pool/{obj_id}",
|
31
|
+
"work-queue": "work-queues/work-queue/{obj_id}",
|
32
|
+
"concurrency-limit": "concurrency-limits/concurrency-limit/{obj_id}",
|
33
|
+
"deployment": "deployments/deployment/{obj_id}",
|
34
|
+
"automation": "automations/automation/{obj_id}",
|
35
|
+
"received-event": "events/event/{occurred}/{obj_id}",
|
36
|
+
}
|
37
|
+
|
38
|
+
# The following objects are excluded from API URL generation because we lack a
|
39
|
+
# directly-addressable URL:
|
40
|
+
# worker
|
41
|
+
# artifact
|
42
|
+
# saved-search
|
43
|
+
# received-event
|
44
|
+
API_URL_FORMATS = {
|
45
|
+
"flow": "flows/{obj_id}",
|
46
|
+
"flow-run": "flow_runs/{obj_id}",
|
47
|
+
"task-run": "task_runs/{obj_id}",
|
48
|
+
"variable": "variables/name/{obj_id}",
|
49
|
+
"block": "blocks/{obj_id}",
|
50
|
+
"work-pool": "work_pools/{obj_id}",
|
51
|
+
"work-queue": "work_queues/{obj_id}",
|
52
|
+
"concurrency-limit": "concurrency_limits/{obj_id}",
|
53
|
+
"deployment": "deployments/{obj_id}",
|
54
|
+
"automation": "automations/{obj_id}",
|
55
|
+
}
|
56
|
+
|
57
|
+
URLType = Literal["ui", "api"]
|
58
|
+
RUN_TYPES = {"flow-run", "task-run"}
|
59
|
+
|
60
|
+
|
61
|
+
def convert_class_to_name(obj: Any) -> str:
|
62
|
+
"""
|
63
|
+
Convert CamelCase class name to dash-separated lowercase name
|
64
|
+
"""
|
65
|
+
cls = obj if inspect.isclass(obj) else obj.__class__
|
66
|
+
name = cls.__name__
|
67
|
+
return "".join(["-" + i.lower() if i.isupper() else i for i in name]).lstrip("-")
|
68
|
+
|
69
|
+
|
70
|
+
def url_for(
|
71
|
+
obj: Union[
|
72
|
+
PrefectFuture,
|
73
|
+
Block,
|
74
|
+
Variable,
|
75
|
+
Automation,
|
76
|
+
Resource,
|
77
|
+
ReceivedEvent,
|
78
|
+
BaseModel,
|
79
|
+
str,
|
80
|
+
],
|
81
|
+
obj_id: Optional[Union[str, UUID]] = None,
|
82
|
+
url_type: URLType = "ui",
|
83
|
+
default_base_url: Optional[str] = None,
|
84
|
+
) -> Optional[str]:
|
85
|
+
"""
|
86
|
+
Returns the URL for a Prefect object.
|
87
|
+
|
88
|
+
Pass in a supported object directly or provide an object name and ID.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
obj (Union[PrefectFuture, Block, Variable, Automation, Resource, ReceivedEvent, BaseModel, str]):
|
92
|
+
A Prefect object to get the URL for, or its URL name and ID.
|
93
|
+
obj_id (Union[str, UUID], optional):
|
94
|
+
The UUID of the object.
|
95
|
+
url_type (Literal["ui", "api"], optional):
|
96
|
+
Whether to return the URL for the UI (default) or API.
|
97
|
+
default_base_url (str, optional):
|
98
|
+
The default base URL to use if no URL is configured.
|
99
|
+
|
100
|
+
Returns:
|
101
|
+
Optional[str]: The URL for the given object or None if the object is not supported.
|
102
|
+
|
103
|
+
Examples:
|
104
|
+
url_for(my_flow_run)
|
105
|
+
url_for(obj=my_flow_run)
|
106
|
+
url_for("flow-run", obj_id="123e4567-e89b-12d3-a456-426614174000")
|
107
|
+
"""
|
108
|
+
if isinstance(obj, PrefectFuture):
|
109
|
+
name = "task-run"
|
110
|
+
elif isinstance(obj, Block):
|
111
|
+
name = "block"
|
112
|
+
elif isinstance(obj, Automation):
|
113
|
+
name = "automation"
|
114
|
+
elif isinstance(obj, ReceivedEvent):
|
115
|
+
name = "received-event"
|
116
|
+
elif isinstance(obj, Resource):
|
117
|
+
if obj.id.startswith("prefect."):
|
118
|
+
name = obj.id.split(".")[1]
|
119
|
+
else:
|
120
|
+
logger.warning(f"No URL known for resource with ID: {obj.id}")
|
121
|
+
return None
|
122
|
+
elif isinstance(obj, str):
|
123
|
+
name = obj
|
124
|
+
else:
|
125
|
+
name = convert_class_to_name(obj)
|
126
|
+
|
127
|
+
# Can't do an isinstance check here because the client build
|
128
|
+
# doesn't have access to that server schema.
|
129
|
+
if name == "work-queue-with-status":
|
130
|
+
name = "work-queue"
|
131
|
+
|
132
|
+
if url_type != "ui" and url_type != "api":
|
133
|
+
raise ValueError(f"Invalid URL type: {url_type}. Use 'ui' or 'api'.")
|
134
|
+
|
135
|
+
if url_type == "ui" and name not in UI_URL_FORMATS:
|
136
|
+
logger.error("No UI URL known for this object: %s", name)
|
137
|
+
return None
|
138
|
+
elif url_type == "api" and name not in API_URL_FORMATS:
|
139
|
+
logger.error("No API URL known for this object: %s", name)
|
140
|
+
return None
|
141
|
+
|
142
|
+
if isinstance(obj, str) and not obj_id:
|
143
|
+
raise ValueError(
|
144
|
+
"If passing an object name, you must also provide an object ID."
|
145
|
+
)
|
146
|
+
|
147
|
+
base_url = (
|
148
|
+
settings.PREFECT_UI_URL.value()
|
149
|
+
if url_type == "ui"
|
150
|
+
else settings.PREFECT_API_URL.value()
|
151
|
+
)
|
152
|
+
base_url = base_url or default_base_url
|
153
|
+
|
154
|
+
if not base_url:
|
155
|
+
logger.warning(
|
156
|
+
f"No URL found for the Prefect {'UI' if url_type == 'ui' else 'API'}, "
|
157
|
+
f"and no default base path provided."
|
158
|
+
)
|
159
|
+
return None
|
160
|
+
|
161
|
+
if not obj_id:
|
162
|
+
# We treat PrefectFuture as if it was the underlying task run,
|
163
|
+
# so we need to check the object type here instead of name.
|
164
|
+
if isinstance(obj, PrefectFuture):
|
165
|
+
obj_id = getattr(obj, "task_run_id", None)
|
166
|
+
elif name == "block":
|
167
|
+
# Blocks are client-side objects whose API representation is a
|
168
|
+
# BlockDocument.
|
169
|
+
obj_id = obj._block_document_id
|
170
|
+
elif name in ("variable", "work-pool"):
|
171
|
+
obj_id = obj.name
|
172
|
+
elif isinstance(obj, Resource):
|
173
|
+
obj_id = obj.id.rpartition(".")[2]
|
174
|
+
else:
|
175
|
+
obj_id = getattr(obj, "id", None)
|
176
|
+
if not obj_id:
|
177
|
+
logger.error(
|
178
|
+
"An ID is required to build a URL, but object did not have one: %s", obj
|
179
|
+
)
|
180
|
+
return ""
|
181
|
+
|
182
|
+
url_format = (
|
183
|
+
UI_URL_FORMATS.get(name) if url_type == "ui" else API_URL_FORMATS.get(name)
|
184
|
+
)
|
185
|
+
|
186
|
+
if isinstance(obj, ReceivedEvent):
|
187
|
+
url = url_format.format(
|
188
|
+
occurred=obj.occurred.strftime("%Y-%m-%d"), obj_id=obj_id
|
189
|
+
)
|
190
|
+
else:
|
191
|
+
url = url_format.format(obj_id=obj_id)
|
192
|
+
|
193
|
+
if not base_url.endswith("/"):
|
194
|
+
base_url += "/"
|
195
|
+
return urllib.parse.urljoin(base_url, url)
|
prefect/variables.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from typing import List, Optional, Union
|
2
2
|
|
3
|
+
from prefect._internal.compatibility.migration import getattr_migration
|
3
4
|
from prefect.client.schemas.actions import VariableCreate as VariableRequest
|
4
5
|
from prefect.client.schemas.actions import VariableUpdate as VariableUpdateRequest
|
5
6
|
from prefect.client.schemas.objects import Variable as VariableResponse
|
@@ -28,17 +29,19 @@ class Variable(VariableRequest):
|
|
28
29
|
value: StrictVariableValue,
|
29
30
|
tags: Optional[List[str]] = None,
|
30
31
|
overwrite: bool = False,
|
32
|
+
as_object: bool = False,
|
31
33
|
):
|
32
34
|
"""
|
33
|
-
Sets a new variable. If one exists with the same name,
|
35
|
+
Sets a new variable. If one exists with the same name, must pass `overwrite=True`
|
34
36
|
|
35
|
-
Returns `True
|
37
|
+
Returns the newly set value. If `as_object=True`, return the full Variable object
|
36
38
|
|
37
39
|
Args:
|
38
40
|
- name: The name of the variable to set.
|
39
41
|
- value: The value of the variable to set.
|
40
42
|
- tags: An optional list of strings to associate with the variable.
|
41
43
|
- overwrite: Whether to overwrite the variable if it already exists.
|
44
|
+
- as_object: Whether to return the full Variable object.
|
42
45
|
|
43
46
|
Example:
|
44
47
|
Set a new variable and overwrite it if it already exists.
|
@@ -51,17 +54,22 @@ class Variable(VariableRequest):
|
|
51
54
|
```
|
52
55
|
"""
|
53
56
|
client, _ = get_or_create_client()
|
54
|
-
|
57
|
+
variable_exists = await client.read_variable_by_name(name)
|
55
58
|
var_dict = {"name": name, "value": value, "tags": tags or []}
|
56
|
-
|
59
|
+
|
60
|
+
if variable_exists:
|
57
61
|
if not overwrite:
|
58
62
|
raise ValueError(
|
59
|
-
"
|
60
|
-
"If you would like to overwrite it, pass `overwrite=True`."
|
63
|
+
f"Variable {name!r} already exists. Use `overwrite=True` to update it."
|
61
64
|
)
|
62
65
|
await client.update_variable(variable=VariableUpdateRequest(**var_dict))
|
66
|
+
variable = await client.read_variable_by_name(name)
|
63
67
|
else:
|
64
|
-
await client.create_variable(
|
68
|
+
variable = await client.create_variable(
|
69
|
+
variable=VariableRequest(**var_dict)
|
70
|
+
)
|
71
|
+
|
72
|
+
return variable if as_object else variable.value
|
65
73
|
|
66
74
|
@classmethod
|
67
75
|
@sync_compatible
|
@@ -96,10 +104,8 @@ class Variable(VariableRequest):
|
|
96
104
|
"""
|
97
105
|
client, _ = get_or_create_client()
|
98
106
|
variable = await client.read_variable_by_name(name)
|
99
|
-
if as_object:
|
100
|
-
return variable
|
101
107
|
|
102
|
-
return variable.value if variable else default
|
108
|
+
return variable if as_object else (variable.value if variable else default)
|
103
109
|
|
104
110
|
@classmethod
|
105
111
|
@sync_compatible
|
@@ -129,3 +135,6 @@ class Variable(VariableRequest):
|
|
129
135
|
return True
|
130
136
|
except ObjectNotFound:
|
131
137
|
return False
|
138
|
+
|
139
|
+
|
140
|
+
__getattr__ = getattr_migration(__name__)
|
prefect/workers/base.py
CHANGED
@@ -8,6 +8,8 @@ import anyio
|
|
8
8
|
import anyio.abc
|
9
9
|
import pendulum
|
10
10
|
from pydantic import BaseModel, Field, PrivateAttr, field_validator
|
11
|
+
from pydantic.json_schema import GenerateJsonSchema
|
12
|
+
from typing_extensions import Literal
|
11
13
|
|
12
14
|
import prefect
|
13
15
|
from prefect._internal.compatibility.experimental import (
|
@@ -42,8 +44,10 @@ from prefect.exceptions import (
|
|
42
44
|
from prefect.logging.loggers import PrefectLogAdapter, flow_run_logger, get_logger
|
43
45
|
from prefect.plugins import load_prefect_collections
|
44
46
|
from prefect.settings import (
|
47
|
+
PREFECT_API_URL,
|
45
48
|
PREFECT_EXPERIMENTAL_WARN,
|
46
49
|
PREFECT_EXPERIMENTAL_WARN_ENHANCED_CANCELLATION,
|
50
|
+
PREFECT_TEST_MODE,
|
47
51
|
PREFECT_WORKER_HEARTBEAT_SECONDS,
|
48
52
|
PREFECT_WORKER_PREFETCH_SECONDS,
|
49
53
|
get_current_settings,
|
@@ -106,12 +110,22 @@ class BaseJobConfiguration(BaseModel):
|
|
106
110
|
def _coerce_command(cls, v):
|
107
111
|
return return_v_or_none(v)
|
108
112
|
|
113
|
+
@field_validator("env", mode="before")
|
114
|
+
@classmethod
|
115
|
+
def _coerce_env(cls, v):
|
116
|
+
return {k: str(v) if v is not None else None for k, v in v.items()}
|
117
|
+
|
109
118
|
@staticmethod
|
110
119
|
def _get_base_config_defaults(variables: dict) -> dict:
|
111
120
|
"""Get default values from base config for all variables that have them."""
|
112
121
|
defaults = dict()
|
113
122
|
for variable_name, attrs in variables.items():
|
114
|
-
|
123
|
+
# We remote `None` values because we don't want to use them in templating.
|
124
|
+
# The currently logic depends on keys not existing to populate the correct value
|
125
|
+
# in some cases.
|
126
|
+
# Pydantic will provide default values if the keys are missing when creating
|
127
|
+
# a configuration class.
|
128
|
+
if "default" in attrs and attrs.get("default") is not None:
|
115
129
|
defaults[variable_name] = attrs["default"]
|
116
130
|
|
117
131
|
return defaults
|
@@ -323,6 +337,33 @@ class BaseVariables(BaseModel):
|
|
323
337
|
),
|
324
338
|
)
|
325
339
|
|
340
|
+
@classmethod
|
341
|
+
def model_json_schema(
|
342
|
+
cls,
|
343
|
+
by_alias: bool = True,
|
344
|
+
ref_template: str = "#/definitions/{model}",
|
345
|
+
schema_generator: Type[GenerateJsonSchema] = GenerateJsonSchema,
|
346
|
+
mode: Literal["validation", "serialization"] = "validation",
|
347
|
+
) -> Dict[str, Any]:
|
348
|
+
"""TODO: stop overriding this method - use GenerateSchema in ConfigDict instead?"""
|
349
|
+
schema = super().model_json_schema(
|
350
|
+
by_alias, ref_template, schema_generator, mode
|
351
|
+
)
|
352
|
+
|
353
|
+
# ensure backwards compatibility by copying $defs into definitions
|
354
|
+
if "$defs" in schema:
|
355
|
+
schema["definitions"] = schema.pop("$defs")
|
356
|
+
|
357
|
+
# we aren't expecting these additional fields in the schema
|
358
|
+
if "additionalProperties" in schema:
|
359
|
+
schema.pop("additionalProperties")
|
360
|
+
|
361
|
+
for _, definition in schema.get("definitions", {}).items():
|
362
|
+
if "additionalProperties" in definition:
|
363
|
+
definition.pop("additionalProperties")
|
364
|
+
|
365
|
+
return schema
|
366
|
+
|
326
367
|
|
327
368
|
class BaseWorkerResult(BaseModel, abc.ABC):
|
328
369
|
identifier: str
|
@@ -511,6 +552,10 @@ class BaseWorker(abc.ABC):
|
|
511
552
|
self._limiter = (
|
512
553
|
anyio.CapacityLimiter(self._limit) if self._limit is not None else None
|
513
554
|
)
|
555
|
+
|
556
|
+
if not PREFECT_TEST_MODE and not PREFECT_API_URL.value():
|
557
|
+
raise ValueError("`PREFECT_API_URL` must be set to start a Worker.")
|
558
|
+
|
514
559
|
self._client = get_client()
|
515
560
|
await self._client.__aenter__()
|
516
561
|
await self._runs_task_group.__aenter__()
|