prefect-client 3.2.15.dev9__py3-none-any.whl → 3.2.16.dev1__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/_build_info.py +3 -3
- prefect/_internal/compatibility/deprecated.py +28 -23
- prefect/_internal/pydantic/v2_schema.py +0 -14
- prefect/_internal/schemas/bases.py +6 -3
- prefect/_internal/schemas/validators.py +9 -2
- prefect/blocks/system.py +7 -6
- prefect/client/cloud.py +0 -1
- prefect/client/orchestration/__init__.py +0 -1
- prefect/client/orchestration/_concurrency_limits/client.py +0 -4
- prefect/client/schemas/objects.py +54 -25
- prefect/client/schemas/schedules.py +6 -5
- prefect/concurrency/_asyncio.py +1 -12
- prefect/concurrency/asyncio.py +0 -4
- prefect/concurrency/services.py +1 -3
- prefect/concurrency/sync.py +1 -6
- prefect/context.py +4 -1
- prefect/events/clients.py +3 -3
- prefect/events/filters.py +7 -2
- prefect/events/related.py +5 -3
- prefect/events/schemas/events.py +4 -4
- prefect/events/utilities.py +4 -3
- prefect/exceptions.py +1 -1
- prefect/flow_engine.py +2 -11
- prefect/futures.py +3 -12
- prefect/locking/filesystem.py +3 -2
- prefect/logging/formatters.py +1 -1
- prefect/logging/handlers.py +2 -2
- prefect/main.py +5 -5
- prefect/results.py +2 -1
- prefect/runner/runner.py +5 -3
- prefect/runner/server.py +2 -2
- prefect/runtime/flow_run.py +11 -6
- prefect/server/api/concurrency_limits_v2.py +12 -8
- prefect/server/api/deployments.py +4 -2
- prefect/server/api/ui/flows.py +7 -2
- prefect/server/api/ui/task_runs.py +3 -3
- prefect/states.py +10 -35
- prefect/task_engine.py +16 -9
- prefect/task_worker.py +6 -3
- prefect/tasks.py +5 -0
- prefect/telemetry/bootstrap.py +3 -1
- prefect/telemetry/instrumentation.py +13 -4
- prefect/telemetry/logging.py +3 -1
- prefect/types/_datetime.py +190 -77
- prefect/utilities/collections.py +6 -12
- prefect/utilities/dockerutils.py +14 -5
- prefect/utilities/engine.py +3 -8
- prefect/workers/base.py +15 -10
- prefect/workers/server.py +0 -1
- {prefect_client-3.2.15.dev9.dist-info → prefect_client-3.2.16.dev1.dist-info}/METADATA +6 -3
- {prefect_client-3.2.15.dev9.dist-info → prefect_client-3.2.16.dev1.dist-info}/RECORD +53 -55
- prefect/_internal/pydantic/annotations/__init__.py +0 -0
- prefect/_internal/pydantic/annotations/pendulum.py +0 -78
- {prefect_client-3.2.15.dev9.dist-info → prefect_client-3.2.16.dev1.dist-info}/WHEEL +0 -0
- {prefect_client-3.2.15.dev9.dist-info → prefect_client-3.2.16.dev1.dist-info}/licenses/LICENSE +0 -0
prefect/task_engine.py
CHANGED
@@ -8,6 +8,7 @@ import time
|
|
8
8
|
from asyncio import CancelledError
|
9
9
|
from contextlib import ExitStack, asynccontextmanager, contextmanager, nullcontext
|
10
10
|
from dataclasses import dataclass, field
|
11
|
+
from datetime import timedelta
|
11
12
|
from functools import partial
|
12
13
|
from textwrap import dedent
|
13
14
|
from typing import (
|
@@ -32,6 +33,7 @@ import anyio
|
|
32
33
|
from opentelemetry import trace
|
33
34
|
from typing_extensions import ParamSpec, Self
|
34
35
|
|
36
|
+
import prefect.types._datetime
|
35
37
|
from prefect.cache_policies import CachePolicy
|
36
38
|
from prefect.client.orchestration import PrefectClient, SyncPrefectClient, get_client
|
37
39
|
from prefect.client.schemas import TaskRun
|
@@ -80,7 +82,6 @@ from prefect.states import (
|
|
80
82
|
)
|
81
83
|
from prefect.telemetry.run_telemetry import RunTelemetry
|
82
84
|
from prefect.transactions import IsolationLevel, Transaction, transaction
|
83
|
-
from prefect.types._datetime import DateTime, Duration
|
84
85
|
from prefect.utilities._engine import get_hook_name
|
85
86
|
from prefect.utilities.annotations import NotSet
|
86
87
|
from prefect.utilities.asyncutils import run_coro_as_sync
|
@@ -438,7 +439,7 @@ class SyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
438
439
|
if last_state.timestamp == new_state.timestamp:
|
439
440
|
# Ensure that the state timestamp is unique, or at least not equal to the last state.
|
440
441
|
# This might occur especially on Windows where the timestamp resolution is limited.
|
441
|
-
new_state.timestamp +=
|
442
|
+
new_state.timestamp += timedelta(microseconds=1)
|
442
443
|
|
443
444
|
# Ensure that the state_details are populated with the current run IDs
|
444
445
|
new_state.state_details.task_run_id = self.task_run.id
|
@@ -487,7 +488,7 @@ class SyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
487
488
|
|
488
489
|
def handle_success(self, result: R, transaction: Transaction) -> R:
|
489
490
|
if self.task.cache_expiration is not None:
|
490
|
-
expiration =
|
491
|
+
expiration = prefect.types._datetime.now("UTC") + self.task.cache_expiration
|
491
492
|
else:
|
492
493
|
expiration = None
|
493
494
|
|
@@ -536,7 +537,8 @@ class SyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
536
537
|
else self.task.retry_delay_seconds
|
537
538
|
)
|
538
539
|
new_state = AwaitingRetry(
|
539
|
-
scheduled_time=
|
540
|
+
scheduled_time=prefect.types._datetime.now("UTC")
|
541
|
+
+ timedelta(seconds=delay)
|
540
542
|
)
|
541
543
|
else:
|
542
544
|
delay = None
|
@@ -729,7 +731,9 @@ class SyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
729
731
|
async def wait_until_ready(self) -> None:
|
730
732
|
"""Waits until the scheduled time (if its the future), then enters Running."""
|
731
733
|
if scheduled_time := self.state.state_details.scheduled_time:
|
732
|
-
sleep_time = (
|
734
|
+
sleep_time = (
|
735
|
+
scheduled_time - prefect.types._datetime.now("UTC")
|
736
|
+
).total_seconds()
|
733
737
|
await anyio.sleep(sleep_time if sleep_time > 0 else 0)
|
734
738
|
new_state = Retrying() if self.state.name == "AwaitingRetry" else Running()
|
735
739
|
self.set_state(
|
@@ -968,7 +972,7 @@ class AsyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
968
972
|
if last_state.timestamp == new_state.timestamp:
|
969
973
|
# Ensure that the state timestamp is unique, or at least not equal to the last state.
|
970
974
|
# This might occur especially on Windows where the timestamp resolution is limited.
|
971
|
-
new_state.timestamp +=
|
975
|
+
new_state.timestamp += timedelta(microseconds=1)
|
972
976
|
|
973
977
|
# Ensure that the state_details are populated with the current run IDs
|
974
978
|
new_state.state_details.task_run_id = self.task_run.id
|
@@ -1018,7 +1022,7 @@ class AsyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
1018
1022
|
|
1019
1023
|
async def handle_success(self, result: R, transaction: Transaction) -> R:
|
1020
1024
|
if self.task.cache_expiration is not None:
|
1021
|
-
expiration =
|
1025
|
+
expiration = prefect.types._datetime.now("UTC") + self.task.cache_expiration
|
1022
1026
|
else:
|
1023
1027
|
expiration = None
|
1024
1028
|
|
@@ -1066,7 +1070,8 @@ class AsyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
1066
1070
|
else self.task.retry_delay_seconds
|
1067
1071
|
)
|
1068
1072
|
new_state = AwaitingRetry(
|
1069
|
-
scheduled_time=
|
1073
|
+
scheduled_time=prefect.types._datetime.now("UTC")
|
1074
|
+
+ timedelta(seconds=delay)
|
1070
1075
|
)
|
1071
1076
|
else:
|
1072
1077
|
delay = None
|
@@ -1257,7 +1262,9 @@ class AsyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
1257
1262
|
async def wait_until_ready(self) -> None:
|
1258
1263
|
"""Waits until the scheduled time (if its the future), then enters Running."""
|
1259
1264
|
if scheduled_time := self.state.state_details.scheduled_time:
|
1260
|
-
sleep_time = (
|
1265
|
+
sleep_time = (
|
1266
|
+
scheduled_time - prefect.types._datetime.now("UTC")
|
1267
|
+
).total_seconds()
|
1261
1268
|
await anyio.sleep(sleep_time if sleep_time > 0 else 0)
|
1262
1269
|
new_state = Retrying() if self.state.name == "AwaitingRetry" else Running()
|
1263
1270
|
await self.set_state(
|
prefect/task_worker.py
CHANGED
@@ -20,6 +20,7 @@ from fastapi import FastAPI
|
|
20
20
|
from typing_extensions import ParamSpec, Self, TypeVar
|
21
21
|
from websockets.exceptions import InvalidStatus
|
22
22
|
|
23
|
+
import prefect.types._datetime
|
23
24
|
from prefect import Task
|
24
25
|
from prefect._internal.concurrency.api import create_call, from_sync
|
25
26
|
from prefect.cache_policies import DEFAULT, NO_CACHE
|
@@ -34,7 +35,7 @@ from prefect.settings import (
|
|
34
35
|
)
|
35
36
|
from prefect.states import Pending
|
36
37
|
from prefect.task_engine import run_task_async, run_task_sync
|
37
|
-
from prefect.types
|
38
|
+
from prefect.types import DateTime
|
38
39
|
from prefect.utilities.annotations import NotSet
|
39
40
|
from prefect.utilities.asyncutils import asyncnullcontext, sync_compatible
|
40
41
|
from prefect.utilities.engine import emit_task_run_state_change_event
|
@@ -256,7 +257,9 @@ class TaskWorker:
|
|
256
257
|
)
|
257
258
|
|
258
259
|
async def _safe_submit_scheduled_task_run(self, task_run: TaskRun):
|
259
|
-
self.in_flight_task_runs[task_run.task_key][task_run.id] =
|
260
|
+
self.in_flight_task_runs[task_run.task_key][task_run.id] = (
|
261
|
+
prefect.types._datetime.now("UTC")
|
262
|
+
)
|
260
263
|
try:
|
261
264
|
await self._submit_scheduled_task_run(task_run)
|
262
265
|
except BaseException as exc:
|
@@ -379,7 +382,7 @@ class TaskWorker:
|
|
379
382
|
await self._exit_stack.enter_async_context(self._runs_task_group)
|
380
383
|
self._exit_stack.enter_context(self._executor)
|
381
384
|
|
382
|
-
self._started_at =
|
385
|
+
self._started_at = prefect.types._datetime.now("UTC")
|
383
386
|
return self
|
384
387
|
|
385
388
|
async def __aexit__(self, *exc_info: Any) -> None:
|
prefect/tasks.py
CHANGED
@@ -765,6 +765,11 @@ class Task(Generic[P, R]):
|
|
765
765
|
def on_rollback(
|
766
766
|
self, fn: Callable[["Transaction"], None]
|
767
767
|
) -> Callable[["Transaction"], None]:
|
768
|
+
if asyncio.iscoroutinefunction(fn):
|
769
|
+
raise ValueError(
|
770
|
+
"Asynchronous rollback hooks are not yet supported. Rollback hooks must be synchronous functions."
|
771
|
+
)
|
772
|
+
|
768
773
|
self.on_rollback_hooks.append(fn)
|
769
774
|
return fn
|
770
775
|
|
prefect/telemetry/bootstrap.py
CHANGED
@@ -10,7 +10,9 @@ if TYPE_CHECKING:
|
|
10
10
|
logger: "logging.Logger" = get_logger(__name__)
|
11
11
|
|
12
12
|
if TYPE_CHECKING:
|
13
|
-
from opentelemetry.sdk._logs import
|
13
|
+
from opentelemetry.sdk._logs import (
|
14
|
+
LoggerProvider, # pyright: ignore[reportPrivateImportUsage]
|
15
|
+
)
|
14
16
|
from opentelemetry.sdk.metrics import MeterProvider
|
15
17
|
from opentelemetry.sdk.trace import TracerProvider
|
16
18
|
|
@@ -6,10 +6,17 @@ from urllib.parse import urljoin
|
|
6
6
|
from uuid import UUID
|
7
7
|
|
8
8
|
from opentelemetry import metrics, trace
|
9
|
-
from opentelemetry._logs import
|
9
|
+
from opentelemetry._logs import (
|
10
|
+
set_logger_provider, # pyright: ignore[reportPrivateImportUsage]
|
11
|
+
)
|
10
12
|
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
|
11
|
-
from opentelemetry.sdk._logs import
|
12
|
-
|
13
|
+
from opentelemetry.sdk._logs import (
|
14
|
+
LoggerProvider, # pyright: ignore[reportPrivateImportUsage]
|
15
|
+
LoggingHandler, # pyright: ignore[reportPrivateImportUsage]
|
16
|
+
)
|
17
|
+
from opentelemetry.sdk._logs.export import (
|
18
|
+
SimpleLogRecordProcessor, # pyright: ignore[reportPrivateImportUsage]
|
19
|
+
)
|
13
20
|
from opentelemetry.sdk.metrics import MeterProvider
|
14
21
|
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
15
22
|
from opentelemetry.sdk.resources import Resource
|
@@ -20,7 +27,9 @@ from .processors import InFlightSpanProcessor
|
|
20
27
|
from .services import QueueingLogExporter, QueueingSpanExporter
|
21
28
|
|
22
29
|
if TYPE_CHECKING:
|
23
|
-
from opentelemetry.sdk._logs import
|
30
|
+
from opentelemetry.sdk._logs import (
|
31
|
+
LoggerProvider, # pyright: ignore[reportPrivateImportUsage]
|
32
|
+
)
|
24
33
|
|
25
34
|
UUID_REGEX = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
|
26
35
|
|
prefect/telemetry/logging.py
CHANGED
@@ -2,7 +2,9 @@ import logging
|
|
2
2
|
from typing import TYPE_CHECKING, Optional
|
3
3
|
|
4
4
|
if TYPE_CHECKING:
|
5
|
-
from opentelemetry.sdk._logs import
|
5
|
+
from opentelemetry.sdk._logs import (
|
6
|
+
LoggingHandler, # pyright: ignore[reportPrivateImportUsage]
|
7
|
+
)
|
6
8
|
|
7
9
|
_log_handler: Optional["LoggingHandler"] = None
|
8
10
|
|
prefect/types/_datetime.py
CHANGED
@@ -1,89 +1,119 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import datetime
|
4
|
-
|
5
|
-
|
6
|
-
import
|
7
|
-
import
|
8
|
-
from
|
9
|
-
|
10
|
-
|
11
|
-
from
|
12
|
-
from pendulum.tz.timezone import FixedTimezone, Timezone
|
13
|
-
from pydantic_extra_types.pendulum_dt import (
|
14
|
-
Date as PydanticDate,
|
15
|
-
)
|
16
|
-
from pydantic_extra_types.pendulum_dt import (
|
17
|
-
DateTime as PydanticDateTime,
|
18
|
-
)
|
19
|
-
from pydantic_extra_types.pendulum_dt import (
|
20
|
-
Duration as PydanticDuration,
|
21
|
-
)
|
4
|
+
import sys
|
5
|
+
from contextlib import contextmanager
|
6
|
+
from typing import Any, cast
|
7
|
+
from unittest import mock
|
8
|
+
from zoneinfo import ZoneInfo, available_timezones
|
9
|
+
|
10
|
+
import humanize
|
11
|
+
from dateutil.parser import parse
|
22
12
|
from typing_extensions import TypeAlias
|
23
13
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
)
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
def
|
47
|
-
|
14
|
+
if sys.version_info >= (3, 13):
|
15
|
+
DateTime: TypeAlias = datetime.datetime
|
16
|
+
Date: TypeAlias = datetime.date
|
17
|
+
Duration: TypeAlias = datetime.timedelta
|
18
|
+
else:
|
19
|
+
import pendulum
|
20
|
+
import pendulum.tz
|
21
|
+
from pydantic_extra_types.pendulum_dt import (
|
22
|
+
Date as PydanticDate,
|
23
|
+
)
|
24
|
+
from pydantic_extra_types.pendulum_dt import (
|
25
|
+
DateTime as PydanticDateTime,
|
26
|
+
)
|
27
|
+
from pydantic_extra_types.pendulum_dt import (
|
28
|
+
Duration as PydanticDuration,
|
29
|
+
)
|
30
|
+
|
31
|
+
DateTime: TypeAlias = PydanticDateTime
|
32
|
+
Date: TypeAlias = PydanticDate
|
33
|
+
Duration: TypeAlias = PydanticDuration
|
34
|
+
|
35
|
+
|
36
|
+
def parse_datetime(dt: str) -> datetime.datetime:
|
37
|
+
if sys.version_info >= (3, 13):
|
38
|
+
parsed_dt = parse(dt)
|
39
|
+
if parsed_dt.tzinfo is None:
|
40
|
+
# Assume UTC if no timezone is provided
|
41
|
+
return parsed_dt.replace(tzinfo=ZoneInfo("UTC"))
|
42
|
+
else:
|
43
|
+
return parsed_dt
|
44
|
+
else:
|
45
|
+
return cast(datetime.datetime, pendulum.parse(dt))
|
48
46
|
|
49
47
|
|
50
48
|
def get_timezones() -> tuple[str, ...]:
|
51
|
-
return
|
49
|
+
return tuple(available_timezones())
|
52
50
|
|
53
51
|
|
54
|
-
def create_datetime_instance(v: datetime.datetime) ->
|
55
|
-
|
52
|
+
def create_datetime_instance(v: datetime.datetime) -> datetime.datetime:
|
53
|
+
if sys.version_info >= (3, 13):
|
54
|
+
if v.tzinfo is None:
|
55
|
+
# Assume UTC if no timezone is provided
|
56
|
+
return v.replace(tzinfo=ZoneInfo("UTC"))
|
57
|
+
else:
|
58
|
+
return v
|
56
59
|
|
60
|
+
return DateTime.instance(v)
|
57
61
|
|
58
|
-
def from_format(
|
59
|
-
value: str,
|
60
|
-
fmt: str,
|
61
|
-
tz: str | Timezone = UTC,
|
62
|
-
locale: str | None = None,
|
63
|
-
) -> DateTime:
|
64
|
-
return DateTime.instance(pendulum.from_format(value, fmt, tz, locale))
|
65
62
|
|
63
|
+
def from_timestamp(ts: float, tz: str | Any = "UTC") -> datetime.datetime:
|
64
|
+
if sys.version_info >= (3, 13):
|
65
|
+
if not isinstance(tz, str):
|
66
|
+
# Handle pendulum edge case
|
67
|
+
tz = tz.name
|
68
|
+
return datetime.datetime.fromtimestamp(ts, ZoneInfo(tz))
|
66
69
|
|
67
|
-
|
68
|
-
return DateTime.instance(pendulum.from_timestamp(ts, tz))
|
70
|
+
return pendulum.from_timestamp(ts, tz)
|
69
71
|
|
70
72
|
|
71
|
-
def human_friendly_diff(
|
72
|
-
|
73
|
-
|
73
|
+
def human_friendly_diff(
|
74
|
+
dt: datetime.datetime | None, other: datetime.datetime | None = None
|
75
|
+
) -> str:
|
76
|
+
if dt is None:
|
77
|
+
return ""
|
78
|
+
|
79
|
+
# Handle naive datetimes consistently across Python versions
|
80
|
+
if dt.tzinfo is None:
|
81
|
+
local_tz = datetime.datetime.now().astimezone().tzinfo
|
82
|
+
dt = dt.replace(tzinfo=local_tz).astimezone(ZoneInfo("UTC"))
|
83
|
+
elif hasattr(dt.tzinfo, "name"):
|
84
|
+
dt = dt.replace(tzinfo=ZoneInfo(getattr(dt.tzinfo, "name")))
|
85
|
+
|
86
|
+
# Handle other parameter if provided
|
87
|
+
if other is not None:
|
88
|
+
if other.tzinfo is None:
|
89
|
+
local_tz = datetime.datetime.now().astimezone().tzinfo
|
90
|
+
other = other.replace(tzinfo=local_tz).astimezone(ZoneInfo("UTC"))
|
91
|
+
elif hasattr(other.tzinfo, "name"):
|
92
|
+
other = other.replace(tzinfo=ZoneInfo(getattr(other.tzinfo, "name")))
|
93
|
+
|
94
|
+
if sys.version_info >= (3, 13):
|
95
|
+
return humanize.naturaltime(dt, when=other)
|
96
|
+
|
97
|
+
return DateTime.instance(dt).diff_for_humans(
|
98
|
+
other=DateTime.instance(other) if other else None
|
99
|
+
)
|
100
|
+
|
101
|
+
|
102
|
+
def now(
|
103
|
+
tz: str | Any = "UTC",
|
104
|
+
) -> datetime.datetime:
|
105
|
+
if sys.version_info >= (3, 13):
|
106
|
+
from whenever import ZonedDateTime
|
107
|
+
|
108
|
+
if isinstance(getattr(tz, "name", None), str):
|
109
|
+
tz = tz.name
|
110
|
+
|
111
|
+
return ZonedDateTime.now(tz).py_datetime()
|
74
112
|
else:
|
75
|
-
return
|
76
|
-
|
77
|
-
|
78
|
-
def now(tz: str | Timezone = UTC) -> DateTime:
|
79
|
-
return DateTime.now(tz)
|
113
|
+
return pendulum.now(tz)
|
80
114
|
|
81
115
|
|
82
|
-
def
|
83
|
-
return dt.add(years=years)
|
84
|
-
|
85
|
-
|
86
|
-
def end_of_period(dt: DateTime, period: str) -> DateTime:
|
116
|
+
def end_of_period(dt: datetime.datetime, period: str) -> datetime.datetime:
|
87
117
|
"""
|
88
118
|
Returns the end of the specified unit of time.
|
89
119
|
|
@@ -91,7 +121,7 @@ def end_of_period(dt: DateTime, period: str) -> DateTime:
|
|
91
121
|
dt: The datetime to get the end of.
|
92
122
|
period: The period to get the end of.
|
93
123
|
Valid values: 'second', 'minute', 'hour', 'day',
|
94
|
-
'week'
|
124
|
+
'week'
|
95
125
|
|
96
126
|
Returns:
|
97
127
|
DateTime: A new DateTime representing the end of the specified unit.
|
@@ -99,27 +129,110 @@ def end_of_period(dt: DateTime, period: str) -> DateTime:
|
|
99
129
|
Raises:
|
100
130
|
ValueError: If an invalid unit is specified.
|
101
131
|
"""
|
102
|
-
|
132
|
+
if sys.version_info >= (3, 13):
|
133
|
+
from whenever import Weekday, ZonedDateTime
|
134
|
+
|
135
|
+
if not isinstance(dt.tzinfo, ZoneInfo):
|
136
|
+
zdt = ZonedDateTime.from_py_datetime(
|
137
|
+
dt.replace(tzinfo=ZoneInfo(dt.tzname() or "UTC"))
|
138
|
+
)
|
139
|
+
else:
|
140
|
+
zdt = ZonedDateTime.from_py_datetime(dt)
|
141
|
+
if period == "second":
|
142
|
+
zdt = zdt.replace(nanosecond=999999999)
|
143
|
+
elif period == "minute":
|
144
|
+
zdt = zdt.replace(second=59, nanosecond=999999999)
|
145
|
+
elif period == "hour":
|
146
|
+
zdt = zdt.replace(minute=59, second=59, nanosecond=999999999)
|
147
|
+
elif period == "day":
|
148
|
+
zdt = zdt.replace(hour=23, minute=59, second=59, nanosecond=999999999)
|
149
|
+
elif period == "week":
|
150
|
+
days_till_end_of_week: int = (
|
151
|
+
Weekday.SUNDAY.value - zdt.date().day_of_week().value
|
152
|
+
)
|
153
|
+
zdt = zdt.replace(
|
154
|
+
day=zdt.day + days_till_end_of_week,
|
155
|
+
hour=23,
|
156
|
+
minute=59,
|
157
|
+
second=59,
|
158
|
+
nanosecond=999999999,
|
159
|
+
)
|
160
|
+
else:
|
161
|
+
raise ValueError(f"Invalid period: {period}")
|
162
|
+
|
163
|
+
return zdt.py_datetime()
|
164
|
+
else:
|
165
|
+
return DateTime.instance(dt).end_of(period)
|
103
166
|
|
104
167
|
|
105
|
-
def
|
168
|
+
def start_of_day(dt: datetime.datetime | DateTime) -> datetime.datetime:
|
106
169
|
"""
|
107
170
|
Returns the start of the specified unit of time.
|
108
171
|
|
109
172
|
Args:
|
110
173
|
dt: The datetime to get the start of.
|
111
|
-
period: The period to get the start of.
|
112
|
-
Valid values: 'second', 'minute', 'hour', 'day',
|
113
|
-
'week', 'month', 'quarter', 'year'
|
114
174
|
|
115
175
|
Returns:
|
116
|
-
|
176
|
+
datetime.datetime: A new datetime.datetime representing the start of the specified unit.
|
117
177
|
|
118
178
|
Raises:
|
119
179
|
ValueError: If an invalid unit is specified.
|
120
180
|
"""
|
121
|
-
|
181
|
+
if sys.version_info >= (3, 13):
|
182
|
+
from whenever import ZonedDateTime
|
122
183
|
|
184
|
+
if hasattr(dt, "tz"):
|
185
|
+
zdt = ZonedDateTime.from_timestamp(
|
186
|
+
dt.timestamp(), tz=dt.tz.name if dt.tz else "UTC"
|
187
|
+
)
|
188
|
+
else:
|
189
|
+
zdt = ZonedDateTime.from_py_datetime(dt)
|
123
190
|
|
124
|
-
|
125
|
-
|
191
|
+
return zdt.start_of_day().py_datetime()
|
192
|
+
else:
|
193
|
+
return DateTime.instance(dt).start_of("day")
|
194
|
+
|
195
|
+
|
196
|
+
def earliest_possible_datetime() -> datetime.datetime:
|
197
|
+
return datetime.datetime.min.replace(tzinfo=ZoneInfo("UTC"))
|
198
|
+
|
199
|
+
|
200
|
+
@contextmanager
|
201
|
+
def travel_to(dt: Any):
|
202
|
+
if sys.version_info >= (3, 13):
|
203
|
+
with mock.patch("prefect.types._datetime.now", return_value=dt):
|
204
|
+
yield
|
205
|
+
|
206
|
+
else:
|
207
|
+
from pendulum import travel_to
|
208
|
+
|
209
|
+
with travel_to(dt, freeze=True):
|
210
|
+
yield
|
211
|
+
|
212
|
+
|
213
|
+
def in_local_tz(dt: datetime.datetime) -> datetime.datetime:
|
214
|
+
if sys.version_info >= (3, 13):
|
215
|
+
from whenever import LocalDateTime, ZonedDateTime
|
216
|
+
|
217
|
+
if dt.tzinfo is None:
|
218
|
+
wdt = LocalDateTime.from_py_datetime(dt)
|
219
|
+
else:
|
220
|
+
if not isinstance(dt.tzinfo, ZoneInfo):
|
221
|
+
if key := getattr(dt.tzinfo, "key", None):
|
222
|
+
dt = dt.replace(tzinfo=ZoneInfo(key))
|
223
|
+
else:
|
224
|
+
utc_dt = dt.astimezone(datetime.timezone.utc)
|
225
|
+
dt = utc_dt.replace(tzinfo=ZoneInfo("UTC"))
|
226
|
+
|
227
|
+
wdt = ZonedDateTime.from_py_datetime(dt).to_system_tz()
|
228
|
+
|
229
|
+
return wdt.py_datetime()
|
230
|
+
|
231
|
+
return DateTime.instance(dt).in_tz(pendulum.tz.local_timezone())
|
232
|
+
|
233
|
+
|
234
|
+
def to_datetime_string(dt: datetime.datetime, include_tz: bool = True) -> str:
|
235
|
+
if include_tz:
|
236
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S %Z")
|
237
|
+
else:
|
238
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
prefect/utilities/collections.py
CHANGED
@@ -5,7 +5,6 @@ Utilities for extensions of and operations on Python collections.
|
|
5
5
|
import io
|
6
6
|
import itertools
|
7
7
|
import types
|
8
|
-
import warnings
|
9
8
|
from collections import OrderedDict
|
10
9
|
from collections.abc import (
|
11
10
|
Callable,
|
@@ -509,20 +508,15 @@ def visit_collection(
|
|
509
508
|
|
510
509
|
elif isinstance(expr, pydantic.BaseModel):
|
511
510
|
# when extra=allow, fields not in model_fields may be in model_fields_set
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
warnings.simplefilter("ignore", category=DeprecationWarning)
|
517
|
-
|
518
|
-
updated_data = {
|
519
|
-
field: visit_nested(getattr(expr, field)) for field in model_fields
|
520
|
-
}
|
511
|
+
original_data = dict(expr)
|
512
|
+
updated_data = {
|
513
|
+
field: visit_nested(value) for field, value in original_data.items()
|
514
|
+
}
|
521
515
|
|
522
516
|
if return_data:
|
523
517
|
modified = any(
|
524
|
-
|
525
|
-
for field in
|
518
|
+
original_data[field] is not updated_data[field]
|
519
|
+
for field in updated_data
|
526
520
|
)
|
527
521
|
if modified:
|
528
522
|
# Use construct to avoid validation and handle immutability
|
prefect/utilities/dockerutils.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import hashlib
|
2
2
|
import os
|
3
3
|
import shutil
|
4
|
+
import subprocess
|
4
5
|
import sys
|
5
6
|
import warnings
|
6
7
|
from collections.abc import Generator, Iterable, Iterator
|
@@ -15,7 +16,7 @@ from packaging.version import Version
|
|
15
16
|
from typing_extensions import Self
|
16
17
|
|
17
18
|
import prefect
|
18
|
-
|
19
|
+
import prefect.types._datetime
|
19
20
|
from prefect.utilities.importtools import lazy_import
|
20
21
|
from prefect.utilities.slugify import slugify
|
21
22
|
|
@@ -54,10 +55,18 @@ def get_prefect_image_name(
|
|
54
55
|
"""
|
55
56
|
parsed_version = Version(prefect_version or prefect.__version__)
|
56
57
|
is_prod_build = parsed_version.local is None
|
58
|
+
try:
|
59
|
+
# Try to get the short SHA from git because it can add additional characters to avoid ambiguity
|
60
|
+
short_sha = (
|
61
|
+
subprocess.check_output(["git", "rev-parse", "--short=7", "HEAD"])
|
62
|
+
.decode("utf-8")
|
63
|
+
.strip()
|
64
|
+
)
|
65
|
+
except Exception:
|
66
|
+
# If git is not available, fallback to the first 7 characters of the full revision ID
|
67
|
+
short_sha = prefect.__version_info__["full-revisionid"][:7]
|
57
68
|
prefect_version = (
|
58
|
-
parsed_version.base_version
|
59
|
-
if is_prod_build
|
60
|
-
else "sha-" + prefect.__version_info__["full-revisionid"][:7]
|
69
|
+
parsed_version.base_version if is_prod_build else f"sha-{short_sha}"
|
61
70
|
)
|
62
71
|
|
63
72
|
python_version = python_version or python_version_minor()
|
@@ -428,7 +437,7 @@ def push_image(
|
|
428
437
|
"""
|
429
438
|
|
430
439
|
if not tag:
|
431
|
-
tag = slugify(now("UTC").isoformat())
|
440
|
+
tag = slugify(prefect.types._datetime.now("UTC").isoformat())
|
432
441
|
|
433
442
|
_, registry, _, _, _ = urlsplit(registry_url)
|
434
443
|
repository = f"{registry}/{name}"
|
prefect/utilities/engine.py
CHANGED
@@ -22,9 +22,7 @@ from opentelemetry import propagate, trace
|
|
22
22
|
from typing_extensions import TypeIs
|
23
23
|
|
24
24
|
import prefect
|
25
|
-
import prefect.context
|
26
25
|
import prefect.exceptions
|
27
|
-
import prefect.plugins
|
28
26
|
from prefect._internal.concurrency.cancellation import get_deadline
|
29
27
|
from prefect.client.schemas import OrchestrationResult, TaskRun
|
30
28
|
from prefect.client.schemas.objects import TaskRunInput, TaskRunResult
|
@@ -50,7 +48,6 @@ from prefect.settings import PREFECT_LOGGING_LOG_PRINTS
|
|
50
48
|
from prefect.states import State
|
51
49
|
from prefect.tasks import Task
|
52
50
|
from prefect.utilities.annotations import allow_failure, quote
|
53
|
-
from prefect.utilities.asyncutils import run_coro_as_sync
|
54
51
|
from prefect.utilities.collections import StopVisiting, visit_collection
|
55
52
|
from prefect.utilities.text import truncated_to
|
56
53
|
|
@@ -221,9 +218,9 @@ async def resolve_inputs(
|
|
221
218
|
finished_states = [state for state in states if state.is_final()]
|
222
219
|
|
223
220
|
state_results = [
|
224
|
-
state.
|
225
|
-
for state in finished_states
|
221
|
+
state.aresult(raise_on_failure=False) for state in finished_states
|
226
222
|
]
|
223
|
+
state_results = await asyncio.gather(*state_results)
|
227
224
|
|
228
225
|
for state, result in zip(finished_states, state_results):
|
229
226
|
result_by_state[state] = result
|
@@ -749,9 +746,7 @@ def resolve_to_final_result(expr: Any, context: dict[str, Any]) -> Any:
|
|
749
746
|
" 'COMPLETED' state."
|
750
747
|
)
|
751
748
|
|
752
|
-
result = state.result(raise_on_failure=False,
|
753
|
-
if asyncio.iscoroutine(result):
|
754
|
-
result = run_coro_as_sync(result)
|
749
|
+
result: Any = state.result(raise_on_failure=False, _sync=True) # pyright: ignore[reportCallIssue] _sync messes up type inference and can be removed once async_dispatch is removed
|
755
750
|
|
756
751
|
if state.state_details.traceparent:
|
757
752
|
parameter_context = propagate.extract(
|