prefect-client 2.16.4__py3-none-any.whl → 2.16.6__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/concurrency/calls.py +3 -2
- prefect/_internal/pydantic/__init__.py +2 -0
- prefect/_internal/pydantic/_compat.py +213 -0
- prefect/_internal/pydantic/v1_schema.py +48 -0
- prefect/_internal/pydantic/v2_schema.py +6 -2
- prefect/artifacts.py +185 -0
- prefect/client/base.py +80 -10
- prefect/client/cloud.py +5 -2
- prefect/client/orchestration.py +17 -1
- prefect/client/schemas/objects.py +13 -0
- prefect/client/subscriptions.py +3 -3
- prefect/engine.py +10 -6
- prefect/events/related.py +1 -1
- prefect/events/schemas.py +45 -1
- prefect/flows.py +12 -4
- prefect/server/api/collections_data/views/aggregate-worker-metadata.json +14 -2
- prefect/settings.py +62 -8
- prefect/task_server.py +9 -3
- prefect/tasks.py +13 -8
- prefect/utilities/callables.py +5 -3
- prefect/utilities/schema_tools/hydration.py +14 -6
- {prefect_client-2.16.4.dist-info → prefect_client-2.16.6.dist-info}/METADATA +1 -1
- {prefect_client-2.16.4.dist-info → prefect_client-2.16.6.dist-info}/RECORD +26 -23
- {prefect_client-2.16.4.dist-info → prefect_client-2.16.6.dist-info}/LICENSE +0 -0
- {prefect_client-2.16.4.dist-info → prefect_client-2.16.6.dist-info}/WHEEL +0 -0
- {prefect_client-2.16.4.dist-info → prefect_client-2.16.6.dist-info}/top_level.txt +0 -0
prefect/client/orchestration.py
CHANGED
@@ -15,6 +15,7 @@ from typing import (
|
|
15
15
|
)
|
16
16
|
from uuid import UUID, uuid4
|
17
17
|
|
18
|
+
import certifi
|
18
19
|
import httpcore
|
19
20
|
import httpx
|
20
21
|
import pendulum
|
@@ -134,8 +135,10 @@ from prefect.settings import (
|
|
134
135
|
PREFECT_API_ENABLE_HTTP2,
|
135
136
|
PREFECT_API_KEY,
|
136
137
|
PREFECT_API_REQUEST_TIMEOUT,
|
138
|
+
PREFECT_API_SSL_CERT_FILE,
|
137
139
|
PREFECT_API_TLS_INSECURE_SKIP_VERIFY,
|
138
140
|
PREFECT_API_URL,
|
141
|
+
PREFECT_CLIENT_CSRF_SUPPORT_ENABLED,
|
139
142
|
PREFECT_CLOUD_API_URL,
|
140
143
|
PREFECT_UNIT_TEST_MODE,
|
141
144
|
)
|
@@ -220,6 +223,11 @@ class PrefectClient:
|
|
220
223
|
|
221
224
|
if PREFECT_API_TLS_INSECURE_SKIP_VERIFY:
|
222
225
|
httpx_settings.setdefault("verify", False)
|
226
|
+
else:
|
227
|
+
cert_file = PREFECT_API_SSL_CERT_FILE.value()
|
228
|
+
if not cert_file:
|
229
|
+
cert_file = certifi.where()
|
230
|
+
httpx_settings.setdefault("verify", cert_file)
|
223
231
|
|
224
232
|
if api_version is None:
|
225
233
|
api_version = SERVER_API_VERSION
|
@@ -316,7 +324,15 @@ class PrefectClient:
|
|
316
324
|
|
317
325
|
if not PREFECT_UNIT_TEST_MODE:
|
318
326
|
httpx_settings.setdefault("follow_redirects", True)
|
319
|
-
|
327
|
+
|
328
|
+
enable_csrf_support = (
|
329
|
+
self.server_type != ServerType.CLOUD
|
330
|
+
and PREFECT_CLIENT_CSRF_SUPPORT_ENABLED.value()
|
331
|
+
)
|
332
|
+
|
333
|
+
self._client = PrefectHttpxClient(
|
334
|
+
**httpx_settings, enable_csrf_support=enable_csrf_support
|
335
|
+
)
|
320
336
|
self._loop = None
|
321
337
|
|
322
338
|
# See https://www.python-httpx.org/advanced/#custom-transports
|
@@ -1632,3 +1632,16 @@ class GlobalConcurrencyLimit(ObjectBaseModel):
|
|
1632
1632
|
" is used as a rate limit."
|
1633
1633
|
),
|
1634
1634
|
)
|
1635
|
+
|
1636
|
+
|
1637
|
+
class CsrfToken(ObjectBaseModel):
|
1638
|
+
token: str = Field(
|
1639
|
+
default=...,
|
1640
|
+
description="The CSRF token",
|
1641
|
+
)
|
1642
|
+
client: str = Field(
|
1643
|
+
default=..., description="The client id associated with the CSRF token"
|
1644
|
+
)
|
1645
|
+
expiration: DateTimeTZ = Field(
|
1646
|
+
default=..., description="The expiration time of the CSRF token"
|
1647
|
+
)
|
prefect/client/subscriptions.py
CHANGED
@@ -74,9 +74,9 @@ class Subscription(Generic[S]):
|
|
74
74
|
auth: Dict[str, Any] = orjson.loads(await websocket.recv())
|
75
75
|
assert auth["type"] == "auth_success", auth.get("message")
|
76
76
|
|
77
|
-
message = {"type": "subscribe", "keys": self.keys}
|
78
|
-
|
79
|
-
|
77
|
+
message = {"type": "subscribe", "keys": self.keys}
|
78
|
+
if self.client_id:
|
79
|
+
message.update({"client_id": self.client_id})
|
80
80
|
|
81
81
|
await websocket.send(orjson.dumps(message).decode())
|
82
82
|
except (
|
prefect/engine.py
CHANGED
@@ -2002,10 +2002,14 @@ async def orchestrate_task_run(
|
|
2002
2002
|
)
|
2003
2003
|
|
2004
2004
|
# Emit an event to capture that the task run was in the `PENDING` state.
|
2005
|
-
last_event =
|
2005
|
+
last_event = emit_task_run_state_change_event(
|
2006
2006
|
task_run=task_run, initial_state=None, validated_state=task_run.state
|
2007
2007
|
)
|
2008
|
-
last_state =
|
2008
|
+
last_state = (
|
2009
|
+
Pending()
|
2010
|
+
if flow_run_context and flow_run_context.autonomous_task_run
|
2011
|
+
else task_run.state
|
2012
|
+
)
|
2009
2013
|
|
2010
2014
|
# Completed states with persisted results should have result data. If it's missing,
|
2011
2015
|
# this could be a manual state transition, so we should use the Unknown result type
|
@@ -2094,7 +2098,7 @@ async def orchestrate_task_run(
|
|
2094
2098
|
break
|
2095
2099
|
|
2096
2100
|
# Emit an event to capture the result of proposing a `RUNNING` state.
|
2097
|
-
last_event =
|
2101
|
+
last_event = emit_task_run_state_change_event(
|
2098
2102
|
task_run=task_run,
|
2099
2103
|
initial_state=last_state,
|
2100
2104
|
validated_state=state,
|
@@ -2187,7 +2191,7 @@ async def orchestrate_task_run(
|
|
2187
2191
|
await _check_task_failure_retriable(task, task_run, terminal_state)
|
2188
2192
|
)
|
2189
2193
|
state = await propose_state(client, terminal_state, task_run_id=task_run.id)
|
2190
|
-
last_event =
|
2194
|
+
last_event = emit_task_run_state_change_event(
|
2191
2195
|
task_run=task_run,
|
2192
2196
|
initial_state=last_state,
|
2193
2197
|
validated_state=state,
|
@@ -2220,7 +2224,7 @@ async def orchestrate_task_run(
|
|
2220
2224
|
)
|
2221
2225
|
# Attempt to enter a running state again
|
2222
2226
|
state = await propose_state(client, Running(), task_run_id=task_run.id)
|
2223
|
-
last_event =
|
2227
|
+
last_event = emit_task_run_state_change_event(
|
2224
2228
|
task_run=task_run,
|
2225
2229
|
initial_state=last_state,
|
2226
2230
|
validated_state=state,
|
@@ -2896,7 +2900,7 @@ async def check_api_reachable(client: PrefectClient, fail_message: str):
|
|
2896
2900
|
API_HEALTHCHECKS[api_url] = get_deadline(60 * 10)
|
2897
2901
|
|
2898
2902
|
|
2899
|
-
def
|
2903
|
+
def emit_task_run_state_change_event(
|
2900
2904
|
task_run: TaskRun,
|
2901
2905
|
initial_state: Optional[State],
|
2902
2906
|
validated_state: State,
|
prefect/events/related.py
CHANGED
prefect/events/schemas.py
CHANGED
@@ -372,9 +372,53 @@ class MetricTrigger(ResourceTrigger):
|
|
372
372
|
)
|
373
373
|
|
374
374
|
|
375
|
-
|
375
|
+
class CompositeTrigger(Trigger, abc.ABC):
|
376
|
+
"""
|
377
|
+
Requires some number of triggers to have fired within the given time period.
|
378
|
+
"""
|
379
|
+
|
380
|
+
type: Literal["compound", "sequence"]
|
381
|
+
triggers: List["TriggerTypes"]
|
382
|
+
within: Optional[timedelta]
|
383
|
+
|
384
|
+
|
385
|
+
class CompoundTrigger(CompositeTrigger):
|
386
|
+
"""A composite trigger that requires some number of triggers to have
|
387
|
+
fired within the given time period"""
|
388
|
+
|
389
|
+
type: Literal["compound"] = "compound"
|
390
|
+
require: Union[int, Literal["any", "all"]]
|
391
|
+
|
392
|
+
@root_validator
|
393
|
+
def validate_require(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
394
|
+
require = values.get("require")
|
395
|
+
|
396
|
+
if isinstance(require, int):
|
397
|
+
if require < 1:
|
398
|
+
raise ValueError("required must be at least 1")
|
399
|
+
if require > len(values["triggers"]):
|
400
|
+
raise ValueError(
|
401
|
+
"required must be less than or equal to the number of triggers"
|
402
|
+
)
|
403
|
+
|
404
|
+
return values
|
405
|
+
|
406
|
+
|
407
|
+
class SequenceTrigger(CompositeTrigger):
|
408
|
+
"""A composite trigger that requires some number of triggers to have fired
|
409
|
+
within the given time period in a specific order"""
|
410
|
+
|
411
|
+
type: Literal["sequence"] = "sequence"
|
412
|
+
|
413
|
+
|
414
|
+
TriggerTypes: TypeAlias = Union[
|
415
|
+
EventTrigger, MetricTrigger, CompoundTrigger, SequenceTrigger
|
416
|
+
]
|
376
417
|
"""The union of all concrete trigger types that a user may actually create"""
|
377
418
|
|
419
|
+
CompoundTrigger.update_forward_refs()
|
420
|
+
SequenceTrigger.update_forward_refs()
|
421
|
+
|
378
422
|
|
379
423
|
class Automation(PrefectBaseModel):
|
380
424
|
"""Defines an action a user wants to take when a certain number of events
|
prefect/flows.py
CHANGED
@@ -1345,8 +1345,12 @@ def flow(
|
|
1345
1345
|
result_serializer: Optional[ResultSerializer] = None,
|
1346
1346
|
cache_result_in_memory: bool = True,
|
1347
1347
|
log_prints: Optional[bool] = None,
|
1348
|
-
on_completion: Optional[
|
1349
|
-
|
1348
|
+
on_completion: Optional[
|
1349
|
+
List[Callable[[FlowSchema, FlowRun, State], Union[Awaitable[None], None]]]
|
1350
|
+
] = None,
|
1351
|
+
on_failure: Optional[
|
1352
|
+
List[Callable[[FlowSchema, FlowRun, State], Union[Awaitable[None], None]]]
|
1353
|
+
] = None,
|
1350
1354
|
on_cancellation: Optional[
|
1351
1355
|
List[Callable[[FlowSchema, FlowRun, State], None]]
|
1352
1356
|
] = None,
|
@@ -1373,8 +1377,12 @@ def flow(
|
|
1373
1377
|
result_serializer: Optional[ResultSerializer] = None,
|
1374
1378
|
cache_result_in_memory: bool = True,
|
1375
1379
|
log_prints: Optional[bool] = None,
|
1376
|
-
on_completion: Optional[
|
1377
|
-
|
1380
|
+
on_completion: Optional[
|
1381
|
+
List[Callable[[FlowSchema, FlowRun, State], Union[Awaitable[None], None]]]
|
1382
|
+
] = None,
|
1383
|
+
on_failure: Optional[
|
1384
|
+
List[Callable[[FlowSchema, FlowRun, State], Union[Awaitable[None], None]]]
|
1385
|
+
] = None,
|
1378
1386
|
on_cancellation: Optional[
|
1379
1387
|
List[Callable[[FlowSchema, FlowRun, State], None]]
|
1380
1388
|
] = None,
|
@@ -912,8 +912,7 @@
|
|
912
912
|
"metadata": {
|
913
913
|
"name": "{{ name }}",
|
914
914
|
"annotations": {
|
915
|
-
"run.googleapis.com/launch-stage": "BETA"
|
916
|
-
"run.googleapis.com/vpc-access-connector": "{{ vpc_connector_name }}"
|
915
|
+
"run.googleapis.com/launch-stage": "BETA"
|
917
916
|
}
|
918
917
|
},
|
919
918
|
"spec": {
|
@@ -941,6 +940,11 @@
|
|
941
940
|
"serviceAccountName": "{{ service_account_name }}"
|
942
941
|
}
|
943
942
|
}
|
943
|
+
},
|
944
|
+
"metadata": {
|
945
|
+
"annotations": {
|
946
|
+
"run.googleapis.com/vpc-access-connector": "{{ vpc_connector_name }}"
|
947
|
+
}
|
944
948
|
}
|
945
949
|
}
|
946
950
|
}
|
@@ -1093,8 +1097,10 @@
|
|
1093
1097
|
"launchStage": "{{ launch_stage }}",
|
1094
1098
|
"template": {
|
1095
1099
|
"template": {
|
1100
|
+
"serviceAccount": "{{ service_account_name }}",
|
1096
1101
|
"maxRetries": "{{ max_retries }}",
|
1097
1102
|
"timeout": "{{ timeout }}",
|
1103
|
+
"vpcAccess": "{{ vpc_connector_name }}",
|
1098
1104
|
"containers": [
|
1099
1105
|
{
|
1100
1106
|
"env": [],
|
@@ -1229,6 +1235,12 @@
|
|
1229
1235
|
"title": "VPC Connector Name",
|
1230
1236
|
"description": "The name of the VPC connector to use for the Cloud Run job.",
|
1231
1237
|
"type": "string"
|
1238
|
+
},
|
1239
|
+
"service_account_name": {
|
1240
|
+
"title": "Service Account Name",
|
1241
|
+
"description": "The name of the service account to use for the task execution of Cloud Run Job. By default Cloud Run jobs run as the default Compute Engine Service Account.",
|
1242
|
+
"example": "service-account@example.iam.gserviceaccount.com",
|
1243
|
+
"type": "string"
|
1232
1244
|
}
|
1233
1245
|
},
|
1234
1246
|
"definitions": {
|
prefect/settings.py
CHANGED
@@ -102,7 +102,10 @@ T = TypeVar("T")
|
|
102
102
|
|
103
103
|
DEFAULT_PROFILES_PATH = Path(__file__).parent.joinpath("profiles.toml")
|
104
104
|
|
105
|
-
REMOVED_EXPERIMENTAL_FLAGS = {
|
105
|
+
REMOVED_EXPERIMENTAL_FLAGS = {
|
106
|
+
"PREFECT_EXPERIMENTAL_ENABLE_ENHANCED_SCHEDULING_UI",
|
107
|
+
"PREFECT_EXPERIMENTAL_ENABLE_ENHANCED_DEPLOYMENT_PARAMETERS",
|
108
|
+
}
|
106
109
|
|
107
110
|
|
108
111
|
class Setting(Generic[T]):
|
@@ -592,6 +595,16 @@ PREFECT_API_TLS_INSECURE_SKIP_VERIFY = Setting(
|
|
592
595
|
This is recommended only during development, e.g. when using self-signed certificates.
|
593
596
|
"""
|
594
597
|
|
598
|
+
PREFECT_API_SSL_CERT_FILE = Setting(
|
599
|
+
str,
|
600
|
+
default=os.environ.get("SSL_CERT_FILE"),
|
601
|
+
)
|
602
|
+
"""
|
603
|
+
This configuration settings option specifies the path to an SSL certificate file.
|
604
|
+
When set, it allows the application to use the specified certificate for secure communication.
|
605
|
+
If left unset, the setting will default to the value provided by the `SSL_CERT_FILE` environment variable.
|
606
|
+
"""
|
607
|
+
|
595
608
|
PREFECT_API_URL = Setting(
|
596
609
|
str,
|
597
610
|
default=None,
|
@@ -657,6 +670,21 @@ A comma-separated list of extra HTTP status codes to retry on. Defaults to an em
|
|
657
670
|
may result in unexpected behavior.
|
658
671
|
"""
|
659
672
|
|
673
|
+
PREFECT_CLIENT_CSRF_SUPPORT_ENABLED = Setting(bool, default=True)
|
674
|
+
"""
|
675
|
+
Determines if CSRF token handling is active in the Prefect client for API
|
676
|
+
requests.
|
677
|
+
|
678
|
+
When enabled (`True`), the client automatically manages CSRF tokens by
|
679
|
+
retrieving, storing, and including them in applicable state-changing requests
|
680
|
+
(POST, PUT, PATCH, DELETE) to the API.
|
681
|
+
|
682
|
+
Disabling this setting (`False`) means the client will not handle CSRF tokens,
|
683
|
+
which might be suitable for environments where CSRF protection is disabled.
|
684
|
+
|
685
|
+
Defaults to `True`, ensuring CSRF protection is enabled by default.
|
686
|
+
"""
|
687
|
+
|
660
688
|
PREFECT_CLOUD_API_URL = Setting(
|
661
689
|
str,
|
662
690
|
default="https://api.prefect.cloud/api",
|
@@ -1207,6 +1235,33 @@ Note this setting only applies when calling `prefect server start`; if hosting t
|
|
1207
1235
|
API with another tool you will need to configure this there instead.
|
1208
1236
|
"""
|
1209
1237
|
|
1238
|
+
PREFECT_SERVER_CSRF_PROTECTION_ENABLED = Setting(bool, default=True)
|
1239
|
+
"""
|
1240
|
+
Controls the activation of CSRF protection for the Prefect server API.
|
1241
|
+
|
1242
|
+
When enabled (`True`), the server enforces CSRF validation checks on incoming
|
1243
|
+
state-changing requests (POST, PUT, PATCH, DELETE), requiring a valid CSRF
|
1244
|
+
token to be included in the request headers or body. This adds a layer of
|
1245
|
+
security by preventing unauthorized or malicious sites from making requests on
|
1246
|
+
behalf of authenticated users.
|
1247
|
+
|
1248
|
+
It is recommended to enable this setting in production environments where the
|
1249
|
+
API is exposed to web clients to safeguard against CSRF attacks.
|
1250
|
+
|
1251
|
+
Note: Enabling this setting requires corresponding support in the client for
|
1252
|
+
CSRF token management. See PREFECT_CLIENT_CSRF_SUPPORT_ENABLED for more.
|
1253
|
+
"""
|
1254
|
+
|
1255
|
+
PREFECT_SERVER_CSRF_TOKEN_EXPIRATION = Setting(timedelta, default=timedelta(hours=1))
|
1256
|
+
"""
|
1257
|
+
Specifies the duration for which a CSRF token remains valid after being issued
|
1258
|
+
by the server.
|
1259
|
+
|
1260
|
+
The default expiration time is set to 1 hour, which offers a reasonable
|
1261
|
+
compromise. Adjust this setting based on your specific security requirements
|
1262
|
+
and usage patterns.
|
1263
|
+
"""
|
1264
|
+
|
1210
1265
|
PREFECT_UI_ENABLED = Setting(
|
1211
1266
|
bool,
|
1212
1267
|
default=True,
|
@@ -1292,12 +1347,12 @@ PREFECT_API_MAX_FLOW_RUN_GRAPH_ARTIFACTS = Setting(int, default=10000)
|
|
1292
1347
|
The maximum number of artifacts to show on a flow run graph on the v2 API
|
1293
1348
|
"""
|
1294
1349
|
|
1295
|
-
PREFECT_EXPERIMENTAL_ENABLE_ARTIFACTS_ON_FLOW_RUN_GRAPH = Setting(bool, default=
|
1350
|
+
PREFECT_EXPERIMENTAL_ENABLE_ARTIFACTS_ON_FLOW_RUN_GRAPH = Setting(bool, default=True)
|
1296
1351
|
"""
|
1297
1352
|
Whether or not to enable artifacts on the flow run graph.
|
1298
1353
|
"""
|
1299
1354
|
|
1300
|
-
PREFECT_EXPERIMENTAL_ENABLE_STATES_ON_FLOW_RUN_GRAPH = Setting(bool, default=
|
1355
|
+
PREFECT_EXPERIMENTAL_ENABLE_STATES_ON_FLOW_RUN_GRAPH = Setting(bool, default=True)
|
1301
1356
|
"""
|
1302
1357
|
Whether or not to enable flow run states on the flow run graph.
|
1303
1358
|
"""
|
@@ -1342,11 +1397,6 @@ PREFECT_EXPERIMENTAL_ENABLE_ENHANCED_CANCELLATION = Setting(bool, default=True)
|
|
1342
1397
|
Whether or not to enable experimental enhanced flow run cancellation.
|
1343
1398
|
"""
|
1344
1399
|
|
1345
|
-
PREFECT_EXPERIMENTAL_ENABLE_ENHANCED_DEPLOYMENT_PARAMETERS = Setting(bool, default=True)
|
1346
|
-
"""
|
1347
|
-
Whether or not to enable enhanced deployment parameters.
|
1348
|
-
"""
|
1349
|
-
|
1350
1400
|
PREFECT_EXPERIMENTAL_WARN_ENHANCED_CANCELLATION = Setting(bool, default=False)
|
1351
1401
|
"""
|
1352
1402
|
Whether or not to warn when experimental enhanced flow run cancellation is used.
|
@@ -1525,6 +1575,10 @@ PREFECT_EXPERIMENTAL_ENABLE_WORK_QUEUE_STATUS = Setting(bool, default=True)
|
|
1525
1575
|
Whether or not to enable experimental work queue status in-place of work queue health.
|
1526
1576
|
"""
|
1527
1577
|
|
1578
|
+
PREFECT_EXPERIMENTAL_ENABLE_PYDANTIC_V2_INTERNALS = Setting(bool, default=False)
|
1579
|
+
"""
|
1580
|
+
Whether or not to enable internal experimental Pydantic v2 behavior.
|
1581
|
+
"""
|
1528
1582
|
|
1529
1583
|
# Defaults -----------------------------------------------------------------------------
|
1530
1584
|
|
prefect/task_server.py
CHANGED
@@ -6,7 +6,7 @@ import socket
|
|
6
6
|
import sys
|
7
7
|
from contextlib import AsyncExitStack
|
8
8
|
from functools import partial
|
9
|
-
from typing import Optional, Type
|
9
|
+
from typing import List, Optional, Type
|
10
10
|
|
11
11
|
import anyio
|
12
12
|
from websockets.exceptions import InvalidStatusCode
|
@@ -15,7 +15,7 @@ from prefect import Task, get_client
|
|
15
15
|
from prefect._internal.concurrency.api import create_call, from_sync
|
16
16
|
from prefect.client.schemas.objects import TaskRun
|
17
17
|
from prefect.client.subscriptions import Subscription
|
18
|
-
from prefect.engine import propose_state
|
18
|
+
from prefect.engine import emit_task_run_state_change_event, propose_state
|
19
19
|
from prefect.logging.loggers import get_logger
|
20
20
|
from prefect.results import ResultFactory
|
21
21
|
from prefect.settings import (
|
@@ -72,7 +72,7 @@ class TaskServer:
|
|
72
72
|
*tasks: Task,
|
73
73
|
task_runner: Optional[Type[BaseTaskRunner]] = None,
|
74
74
|
):
|
75
|
-
self.tasks:
|
75
|
+
self.tasks: List[Task] = tasks
|
76
76
|
|
77
77
|
self.task_runner: BaseTaskRunner = task_runner or ConcurrentTaskRunner()
|
78
78
|
self.started: bool = False
|
@@ -205,6 +205,12 @@ class TaskServer:
|
|
205
205
|
" Task run may have already begun execution."
|
206
206
|
)
|
207
207
|
|
208
|
+
emit_task_run_state_change_event(
|
209
|
+
task_run=task_run,
|
210
|
+
initial_state=task_run.state,
|
211
|
+
validated_state=state,
|
212
|
+
)
|
213
|
+
|
208
214
|
self._runs_task_group.start_soon(
|
209
215
|
partial(
|
210
216
|
submit_autonomous_task_run_to_engine,
|
prefect/tasks.py
CHANGED
@@ -283,13 +283,14 @@ class Task(Generic[P, R]):
|
|
283
283
|
if not hasattr(self.fn, "__qualname__"):
|
284
284
|
self.task_key = to_qualified_name(type(self.fn))
|
285
285
|
else:
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
self.name, os.path.abspath(task_definition_path)
|
286
|
+
try:
|
287
|
+
task_origin_hash = hash_objects(
|
288
|
+
self.name, os.path.abspath(inspect.getsourcefile(self.fn))
|
290
289
|
)
|
291
|
-
|
292
|
-
|
290
|
+
except TypeError:
|
291
|
+
task_origin_hash = "unknown-source-file"
|
292
|
+
|
293
|
+
self.task_key = f"{self.fn.__qualname__}-{task_origin_hash}"
|
293
294
|
|
294
295
|
self.cache_key_fn = cache_key_fn
|
295
296
|
self.cache_expiration = cache_expiration
|
@@ -390,8 +391,12 @@ class Task(Generic[P, R]):
|
|
390
391
|
timeout_seconds: Union[int, float] = None,
|
391
392
|
log_prints: Optional[bool] = NotSet,
|
392
393
|
refresh_cache: Optional[bool] = NotSet,
|
393
|
-
on_completion: Optional[
|
394
|
-
|
394
|
+
on_completion: Optional[
|
395
|
+
List[Callable[["Task", TaskRun, State], Union[Awaitable[None], None]]]
|
396
|
+
] = None,
|
397
|
+
on_failure: Optional[
|
398
|
+
List[Callable[["Task", TaskRun, State], Union[Awaitable[None], None]]]
|
399
|
+
] = None,
|
395
400
|
retry_condition_fn: Optional[Callable[["Task", TaskRun, State], bool]] = None,
|
396
401
|
viz_return_value: Optional[Any] = None,
|
397
402
|
):
|
prefect/utilities/callables.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""
|
2
2
|
Utilities for working with Python callables.
|
3
3
|
"""
|
4
|
+
|
4
5
|
import inspect
|
5
6
|
from functools import partial
|
6
7
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
|
@@ -8,13 +9,13 @@ from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
|
|
8
9
|
import cloudpickle
|
9
10
|
|
10
11
|
from prefect._internal.pydantic import HAS_PYDANTIC_V2
|
12
|
+
from prefect._internal.pydantic.v1_schema import has_v1_type_as_param
|
11
13
|
|
12
14
|
if HAS_PYDANTIC_V2:
|
13
15
|
import pydantic.v1 as pydantic
|
14
16
|
|
15
17
|
from prefect._internal.pydantic.v2_schema import (
|
16
18
|
create_v2_schema,
|
17
|
-
has_v2_type_as_param,
|
18
19
|
process_v2_params,
|
19
20
|
)
|
20
21
|
else:
|
@@ -312,8 +313,9 @@ def parameter_schema(fn: Callable) -> ParameterSchema:
|
|
312
313
|
ParameterSchema: the argument schema
|
313
314
|
"""
|
314
315
|
try:
|
315
|
-
signature = inspect.signature(fn, eval_str=True)
|
316
|
+
signature = inspect.signature(fn, eval_str=True) # novm
|
316
317
|
except (NameError, TypeError):
|
318
|
+
# `eval_str` is not available in Python < 3.10
|
317
319
|
signature = inspect.signature(fn)
|
318
320
|
|
319
321
|
model_fields = {}
|
@@ -323,7 +325,7 @@ def parameter_schema(fn: Callable) -> ParameterSchema:
|
|
323
325
|
class ModelConfig:
|
324
326
|
arbitrary_types_allowed = True
|
325
327
|
|
326
|
-
if HAS_PYDANTIC_V2 and
|
328
|
+
if HAS_PYDANTIC_V2 and not has_v1_type_as_param(signature):
|
327
329
|
create_schema = create_v2_schema
|
328
330
|
process_params = process_v2_params
|
329
331
|
else:
|
@@ -149,9 +149,13 @@ def null_handler(obj: Dict, ctx: HydrationContext):
|
|
149
149
|
@handler("json")
|
150
150
|
def json_handler(obj: Dict, ctx: HydrationContext):
|
151
151
|
if "value" in obj:
|
152
|
+
if isinstance(obj["value"], dict):
|
153
|
+
dehydrated_json = _hydrate(obj["value"], ctx)
|
154
|
+
else:
|
155
|
+
dehydrated_json = obj["value"]
|
152
156
|
try:
|
153
|
-
return json.loads(
|
154
|
-
except json.decoder.JSONDecodeError as e:
|
157
|
+
return json.loads(dehydrated_json)
|
158
|
+
except (json.decoder.JSONDecodeError, TypeError) as e:
|
155
159
|
return InvalidJSON(detail=str(e))
|
156
160
|
else:
|
157
161
|
# If `value` is not in the object, we need special handling to help
|
@@ -166,11 +170,15 @@ def json_handler(obj: Dict, ctx: HydrationContext):
|
|
166
170
|
@handler("workspace_variable")
|
167
171
|
def workspace_variable_handler(obj: Dict, ctx: HydrationContext):
|
168
172
|
if "variable_name" in obj:
|
169
|
-
|
170
|
-
|
171
|
-
|
173
|
+
if isinstance(obj["variable_name"], dict):
|
174
|
+
dehydrated_variable = _hydrate(obj["variable_name"], ctx)
|
175
|
+
else:
|
176
|
+
dehydrated_variable = obj["variable_name"]
|
177
|
+
|
178
|
+
if dehydrated_variable in ctx.workspace_variables:
|
179
|
+
return ctx.workspace_variables[dehydrated_variable]
|
172
180
|
else:
|
173
|
-
return WorkspaceVariableNotFound(detail=
|
181
|
+
return WorkspaceVariableNotFound(detail=dehydrated_variable)
|
174
182
|
else:
|
175
183
|
# Special handling if `variable_name` is not in the object.
|
176
184
|
# If an object looks like {"__prefect_kind": "workspace_variable"}
|