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.
Files changed (55) hide show
  1. prefect/_build_info.py +3 -3
  2. prefect/_internal/compatibility/deprecated.py +28 -23
  3. prefect/_internal/pydantic/v2_schema.py +0 -14
  4. prefect/_internal/schemas/bases.py +6 -3
  5. prefect/_internal/schemas/validators.py +9 -2
  6. prefect/blocks/system.py +7 -6
  7. prefect/client/cloud.py +0 -1
  8. prefect/client/orchestration/__init__.py +0 -1
  9. prefect/client/orchestration/_concurrency_limits/client.py +0 -4
  10. prefect/client/schemas/objects.py +54 -25
  11. prefect/client/schemas/schedules.py +6 -5
  12. prefect/concurrency/_asyncio.py +1 -12
  13. prefect/concurrency/asyncio.py +0 -4
  14. prefect/concurrency/services.py +1 -3
  15. prefect/concurrency/sync.py +1 -6
  16. prefect/context.py +4 -1
  17. prefect/events/clients.py +3 -3
  18. prefect/events/filters.py +7 -2
  19. prefect/events/related.py +5 -3
  20. prefect/events/schemas/events.py +4 -4
  21. prefect/events/utilities.py +4 -3
  22. prefect/exceptions.py +1 -1
  23. prefect/flow_engine.py +2 -11
  24. prefect/futures.py +3 -12
  25. prefect/locking/filesystem.py +3 -2
  26. prefect/logging/formatters.py +1 -1
  27. prefect/logging/handlers.py +2 -2
  28. prefect/main.py +5 -5
  29. prefect/results.py +2 -1
  30. prefect/runner/runner.py +5 -3
  31. prefect/runner/server.py +2 -2
  32. prefect/runtime/flow_run.py +11 -6
  33. prefect/server/api/concurrency_limits_v2.py +12 -8
  34. prefect/server/api/deployments.py +4 -2
  35. prefect/server/api/ui/flows.py +7 -2
  36. prefect/server/api/ui/task_runs.py +3 -3
  37. prefect/states.py +10 -35
  38. prefect/task_engine.py +16 -9
  39. prefect/task_worker.py +6 -3
  40. prefect/tasks.py +5 -0
  41. prefect/telemetry/bootstrap.py +3 -1
  42. prefect/telemetry/instrumentation.py +13 -4
  43. prefect/telemetry/logging.py +3 -1
  44. prefect/types/_datetime.py +190 -77
  45. prefect/utilities/collections.py +6 -12
  46. prefect/utilities/dockerutils.py +14 -5
  47. prefect/utilities/engine.py +3 -8
  48. prefect/workers/base.py +15 -10
  49. prefect/workers/server.py +0 -1
  50. {prefect_client-3.2.15.dev9.dist-info → prefect_client-3.2.16.dev1.dist-info}/METADATA +6 -3
  51. {prefect_client-3.2.15.dev9.dist-info → prefect_client-3.2.16.dev1.dist-info}/RECORD +53 -55
  52. prefect/_internal/pydantic/annotations/__init__.py +0 -0
  53. prefect/_internal/pydantic/annotations/pendulum.py +0 -78
  54. {prefect_client-3.2.15.dev9.dist-info → prefect_client-3.2.16.dev1.dist-info}/WHEEL +0 -0
  55. {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 += Duration(microseconds=1)
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 = DateTime.now("utc") + self.task.cache_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=DateTime.now("utc").add(seconds=delay)
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 = (scheduled_time - DateTime.now("utc")).total_seconds()
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 += Duration(microseconds=1)
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 = DateTime.now("utc") + self.task.cache_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=DateTime.now("utc").add(seconds=delay)
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 = (scheduled_time - DateTime.now("utc")).total_seconds()
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._datetime import DateTime
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] = DateTime.now()
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 = DateTime.now()
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
 
@@ -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 LoggerProvider
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 set_logger_provider
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 LoggerProvider, LoggingHandler
12
- from opentelemetry.sdk._logs.export import SimpleLogRecordProcessor
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 LoggerProvider
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
 
@@ -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 LoggingHandler
5
+ from opentelemetry.sdk._logs import (
6
+ LoggingHandler, # pyright: ignore[reportPrivateImportUsage]
7
+ )
6
8
 
7
9
  _log_handler: Optional["LoggingHandler"] = None
8
10
 
@@ -1,89 +1,119 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import datetime
4
- from typing import Any
5
-
6
- import pendulum
7
- import pendulum.tz
8
- from pendulum.date import Date as PendulumDate
9
- from pendulum.datetime import DateTime as PendulumDateTime
10
- from pendulum.duration import Duration as PendulumDuration
11
- from pendulum.time import Time as PendulumTime
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
- DateTime: TypeAlias = PydanticDateTime
25
- Date: TypeAlias = PydanticDate
26
- Duration: TypeAlias = PydanticDuration
27
- UTC: pendulum.tz.Timezone = pendulum.tz.UTC
28
-
29
-
30
- def parse_datetime(
31
- value: str,
32
- **options: Any,
33
- ) -> PendulumDateTime | PendulumDate | PendulumTime | PendulumDuration:
34
- return pendulum.parse(value, **options)
35
-
36
-
37
- def format_diff(
38
- diff: PendulumDuration,
39
- is_now: bool = True,
40
- absolute: bool = False,
41
- locale: str | None = None,
42
- ) -> str:
43
- return pendulum.format_diff(diff, is_now, absolute, locale)
44
-
45
-
46
- def local_timezone() -> Timezone | FixedTimezone:
47
- return pendulum.tz.local_timezone()
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 pendulum.tz.timezones()
49
+ return tuple(available_timezones())
52
50
 
53
51
 
54
- def create_datetime_instance(v: datetime.datetime) -> DateTime:
55
- return DateTime.instance(v)
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
- def from_timestamp(ts: float, tz: str | pendulum.tz.Timezone = UTC) -> DateTime:
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(dt: DateTime | datetime.datetime) -> str:
72
- if isinstance(dt, DateTime):
73
- return dt.diff_for_humans()
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 DateTime.instance(dt).diff_for_humans()
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 add_years(dt: DateTime, years: int) -> DateTime:
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', 'month', 'quarter', 'year'
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
- return dt.end_of(period)
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 start_of_period(dt: DateTime, period: str) -> DateTime:
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
- DateTime: A new DateTime representing the start of the specified unit.
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
- return dt.start_of(period)
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
- def earliest_possible_datetime() -> DateTime:
125
- return DateTime.instance(datetime.datetime.min)
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")
@@ -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
- model_fields = expr.model_fields_set.union(expr.model_fields.keys())
513
-
514
- # We may encounter a deprecated field here, but this isn't the caller's fault
515
- with warnings.catch_warnings():
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
- getattr(expr, field) is not updated_data[field]
525
- for field in model_fields
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
@@ -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
- from prefect.types._datetime import now
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}"
@@ -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.result(raise_on_failure=False, fetch=True)
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, fetch=True)
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(