prefect 3.7.8.dev2__py3-none-any.whl → 3.7.8.dev4__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/schemas/validators.py +27 -4
- prefect/events/clients.py +42 -6
- prefect/runner/storage.py +45 -2
- {prefect-3.7.8.dev2.dist-info → prefect-3.7.8.dev4.dist-info}/METADATA +1 -1
- {prefect-3.7.8.dev2.dist-info → prefect-3.7.8.dev4.dist-info}/RECORD +9 -9
- {prefect-3.7.8.dev2.dist-info → prefect-3.7.8.dev4.dist-info}/WHEEL +1 -1
- {prefect-3.7.8.dev2.dist-info → prefect-3.7.8.dev4.dist-info}/entry_points.txt +0 -0
- {prefect-3.7.8.dev2.dist-info → prefect-3.7.8.dev4.dist-info}/licenses/LICENSE +0 -0
prefect/_build_info.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Generated by versioningit
|
|
2
|
-
__version__ = "3.7.8.
|
|
3
|
-
__build_date__ = "2026-07-
|
|
4
|
-
__git_commit__ = "
|
|
2
|
+
__version__ = "3.7.8.dev4"
|
|
3
|
+
__build_date__ = "2026-07-09 09:25:29.436344+00:00"
|
|
4
|
+
__git_commit__ = "467544da6cd2526a549c1003d8a991c95a514392"
|
|
5
5
|
__dirty__ = False
|
|
@@ -321,6 +321,7 @@ def normalize_rrule_string(
|
|
|
321
321
|
rrule_string: str,
|
|
322
322
|
*,
|
|
323
323
|
now: Optional[datetime.datetime] = None,
|
|
324
|
+
timezone: Optional[str] = None,
|
|
324
325
|
) -> str:
|
|
325
326
|
"""Inject a `DTSTART` line into an rrule string if it doesn't already have one.
|
|
326
327
|
|
|
@@ -350,8 +351,16 @@ def normalize_rrule_string(
|
|
|
350
351
|
datetimes, ~3 MB).
|
|
351
352
|
|
|
352
353
|
Strings that already contain a `DTSTART` are returned unchanged.
|
|
354
|
+
|
|
355
|
+
The injected anchor is written as a floating (offset-less) `DTSTART`,
|
|
356
|
+
which `RRuleSchedule.to_rrule` later relabels with the schedule's
|
|
357
|
+
timezone. `timezone` is the schedule's IANA timezone (defaulting to
|
|
358
|
+
UTC); the recent anchor's wall-clock digits are computed in it so the
|
|
359
|
+
floating anchor lands on the intended local instant rather than being
|
|
360
|
+
shifted by the timezone's UTC offset (PrefectHQ/prefect#22455).
|
|
353
361
|
"""
|
|
354
362
|
import dateutil.rrule
|
|
363
|
+
import dateutil.tz
|
|
355
364
|
|
|
356
365
|
# Already anchored — leave it alone.
|
|
357
366
|
if "DTSTART" in rrule_string.upper():
|
|
@@ -400,10 +409,22 @@ def normalize_rrule_string(
|
|
|
400
409
|
unit_seconds = 1 if freq == dateutil.rrule.SECONDLY else 60
|
|
401
410
|
period_seconds = unit_seconds * interval
|
|
402
411
|
|
|
412
|
+
# The injected `DTSTART` is serialized as floating (no offset/`Z`) and
|
|
413
|
+
# `RRuleSchedule.to_rrule` later relabels it with the schedule's timezone
|
|
414
|
+
# via `.replace(tzinfo=...)`, which keeps the wall-clock digits without
|
|
415
|
+
# converting the offset. The anchor's wall-clock digits must therefore be
|
|
416
|
+
# computed in the schedule's own timezone; computing them in UTC shifts
|
|
417
|
+
# every occurrence forward by the timezone's offset, suppressing runs on
|
|
418
|
+
# the deploy day (PrefectHQ/prefect#22455).
|
|
419
|
+
anchor_tz = dateutil.tz.gettz(timezone) if timezone else datetime.timezone.utc
|
|
420
|
+
if anchor_tz is None:
|
|
421
|
+
anchor_tz = datetime.timezone.utc
|
|
422
|
+
|
|
403
423
|
if now is None:
|
|
404
|
-
now = datetime.datetime.now(datetime.timezone.utc)
|
|
405
|
-
|
|
406
|
-
now = now.
|
|
424
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
425
|
+
if now.tzinfo is None:
|
|
426
|
+
now = now.replace(tzinfo=datetime.timezone.utc)
|
|
427
|
+
now = now.astimezone(anchor_tz).replace(tzinfo=None)
|
|
407
428
|
|
|
408
429
|
delta_seconds = (now - DEFAULT_RRULE_ANCHOR).total_seconds()
|
|
409
430
|
if delta_seconds <= 0:
|
|
@@ -443,7 +464,9 @@ def normalize_schedule_rrule(schedule: _T) -> _T:
|
|
|
443
464
|
rrule = getattr(schedule, "rrule", None)
|
|
444
465
|
if not isinstance(rrule, str):
|
|
445
466
|
return schedule
|
|
446
|
-
normalized = normalize_rrule_string(
|
|
467
|
+
normalized = normalize_rrule_string(
|
|
468
|
+
rrule, timezone=getattr(schedule, "timezone", None)
|
|
469
|
+
)
|
|
447
470
|
if normalized == rrule:
|
|
448
471
|
return schedule
|
|
449
472
|
return type(schedule)(rrule=normalized, timezone=schedule.timezone) # type: ignore[call-arg]
|
prefect/events/clients.py
CHANGED
|
@@ -436,8 +436,17 @@ class PrefectEventsClient(EventsClient):
|
|
|
436
436
|
# Clear the unconfirmed events here, because they are going back through emit
|
|
437
437
|
# and will be added again through the normal checkpointing process
|
|
438
438
|
self._unconfirmed_events = []
|
|
439
|
-
|
|
440
|
-
|
|
439
|
+
try:
|
|
440
|
+
while events_to_resend:
|
|
441
|
+
event = events_to_resend.pop(0)
|
|
442
|
+
await self.emit(event)
|
|
443
|
+
except Exception:
|
|
444
|
+
# If a resend fails partway through (emit() has its own retry
|
|
445
|
+
# budget), restore the events that were never attempted so a later
|
|
446
|
+
# reconnect can retry them. The event whose emit() failed was
|
|
447
|
+
# already added back to the buffer by emit() itself.
|
|
448
|
+
self._unconfirmed_events.extend(events_to_resend)
|
|
449
|
+
raise
|
|
441
450
|
logger.debug("Finished resending unconfirmed events.")
|
|
442
451
|
self._start_checkpoint_task()
|
|
443
452
|
|
|
@@ -452,17 +461,44 @@ class PrefectEventsClient(EventsClient):
|
|
|
452
461
|
|
|
453
462
|
async def _checkpoint_loop(self) -> None:
|
|
454
463
|
"""Periodically checkpoint unconfirmed events on a time interval,
|
|
455
|
-
independent of the count-based checkpoint in _checkpoint.
|
|
464
|
+
independent of the count-based checkpoint in _checkpoint.
|
|
465
|
+
|
|
466
|
+
If the connection has been lost (e.g. a server restart closed it),
|
|
467
|
+
reconnects and resends the unconfirmed events rather than leaving them
|
|
468
|
+
stranded until the next emit."""
|
|
456
469
|
while True:
|
|
457
470
|
await asyncio.sleep(self._checkpoint_interval)
|
|
458
|
-
if
|
|
459
|
-
|
|
471
|
+
if not self._unconfirmed_events:
|
|
472
|
+
continue
|
|
473
|
+
try:
|
|
474
|
+
if self._websocket:
|
|
460
475
|
await self._force_checkpoint()
|
|
476
|
+
continue
|
|
477
|
+
# The connection was lost while events were still unconfirmed;
|
|
478
|
+
# reconnect to resend them rather than waiting for the next emit.
|
|
479
|
+
await self._reconnect()
|
|
480
|
+
except ConnectionClosed:
|
|
481
|
+
# The connection died since the last checkpoint; reconnect and
|
|
482
|
+
# resend the unconfirmed events. _reconnect() tears down the
|
|
483
|
+
# dead connection before connecting again.
|
|
484
|
+
try:
|
|
485
|
+
await self._reconnect()
|
|
461
486
|
except Exception:
|
|
462
487
|
logger.debug(
|
|
463
|
-
"
|
|
488
|
+
"Reconnecting during time-based checkpoint failed, "
|
|
489
|
+
"will retry next interval.",
|
|
464
490
|
exc_info=True,
|
|
465
491
|
)
|
|
492
|
+
continue
|
|
493
|
+
except Exception:
|
|
494
|
+
logger.debug(
|
|
495
|
+
"Time-based checkpoint failed, will retry next interval.",
|
|
496
|
+
exc_info=True,
|
|
497
|
+
)
|
|
498
|
+
continue
|
|
499
|
+
# A successful _reconnect() resent the unconfirmed events and
|
|
500
|
+
# started a replacement checkpoint task, so this task is done.
|
|
501
|
+
return
|
|
466
502
|
|
|
467
503
|
async def _force_checkpoint(self) -> None:
|
|
468
504
|
"""Checkpoint all unconfirmed events unconditionally."""
|
prefect/runner/storage.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import re
|
|
4
5
|
import shutil
|
|
6
|
+
import stat
|
|
5
7
|
import subprocess
|
|
8
|
+
import sys
|
|
6
9
|
import warnings
|
|
7
10
|
from copy import deepcopy
|
|
8
11
|
from pathlib import Path
|
|
@@ -31,6 +34,41 @@ from prefect.logging.loggers import get_logger
|
|
|
31
34
|
from prefect.utilities.collections import visit_collection
|
|
32
35
|
|
|
33
36
|
|
|
37
|
+
def _clear_read_only_attributes(path: Path) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Recursively clear the read-only attribute on files within a directory.
|
|
40
|
+
|
|
41
|
+
On Windows, git writes pack and loose object files as read-only (mode
|
|
42
|
+
`0o444`). A subsequent `git pull` that needs to repack or replace those
|
|
43
|
+
files then fails with `[WinError 5] Access is denied`. Marking the files
|
|
44
|
+
as writable beforehand lets git manage them. This is a no-op on
|
|
45
|
+
non-Windows platforms, where read-only object files do not block git.
|
|
46
|
+
"""
|
|
47
|
+
if sys.platform != "win32":
|
|
48
|
+
return
|
|
49
|
+
if not path.exists():
|
|
50
|
+
return
|
|
51
|
+
for entry in path.rglob("*"):
|
|
52
|
+
try:
|
|
53
|
+
if entry.is_file() and not entry.is_symlink():
|
|
54
|
+
os.chmod(entry, stat.S_IWRITE | stat.S_IREAD)
|
|
55
|
+
except OSError:
|
|
56
|
+
# Best effort: if a single file can't be made writable, let git
|
|
57
|
+
# surface the underlying error rather than masking it here.
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _rmtree_including_read_only(path: Path) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Remove a directory tree, first clearing read-only attributes so that
|
|
64
|
+
read-only git object files (mode `0o444` on Windows) do not raise
|
|
65
|
+
`[WinError 5] Access is denied` during deletion. This is a plain
|
|
66
|
+
`shutil.rmtree` on non-Windows platforms.
|
|
67
|
+
"""
|
|
68
|
+
_clear_read_only_attributes(path)
|
|
69
|
+
shutil.rmtree(path)
|
|
70
|
+
|
|
71
|
+
|
|
34
72
|
@runtime_checkable
|
|
35
73
|
class RunnerStorage(Protocol):
|
|
36
74
|
"""
|
|
@@ -393,6 +431,11 @@ class GitRepository:
|
|
|
393
431
|
f"does not match the configured repository {configured_repo_url}"
|
|
394
432
|
)
|
|
395
433
|
|
|
434
|
+
# On Windows, git object files are read-only, which prevents git
|
|
435
|
+
# from repacking or replacing them during a pull. Clear the
|
|
436
|
+
# read-only attribute first to avoid `[WinError 5] Access is denied`.
|
|
437
|
+
_clear_read_only_attributes(git_dir)
|
|
438
|
+
|
|
396
439
|
# Sparsely checkout the repository if directories are specified and the repo is not in sparse-checkout mode already
|
|
397
440
|
if self._directories and not await self.is_sparsely_checked_out():
|
|
398
441
|
await run_process(
|
|
@@ -423,7 +466,7 @@ class GitRepository:
|
|
|
423
466
|
self._logger.error(
|
|
424
467
|
f"Failed to fetch latest changes with exit code {exc}"
|
|
425
468
|
)
|
|
426
|
-
|
|
469
|
+
_rmtree_including_read_only(self.destination)
|
|
427
470
|
await self._clone_repo()
|
|
428
471
|
|
|
429
472
|
await run_process(
|
|
@@ -449,7 +492,7 @@ class GitRepository:
|
|
|
449
492
|
self._logger.error(
|
|
450
493
|
f"Failed to pull latest changes with exit code {exc}"
|
|
451
494
|
)
|
|
452
|
-
|
|
495
|
+
_rmtree_including_read_only(self.destination)
|
|
453
496
|
await self._clone_repo()
|
|
454
497
|
|
|
455
498
|
else:
|
|
@@ -2,7 +2,7 @@ prefect/.prefectignore,sha256=awSprvKT0vI8a64mEOLrMxhxqcO-b0ERQeYpA2rNKVQ,390
|
|
|
2
2
|
prefect/AGENTS.md,sha256=H7j73ZMACxsIfHXO6pTAdyQy2yi7XGnADtgwcMqMVO4,11049
|
|
3
3
|
prefect/__init__.py,sha256=Z8rwfLbEOLh-5WcznTZP3FG2-9UgGZxY-prj8sL0-Qk,6828
|
|
4
4
|
prefect/__main__.py,sha256=WFjw3kaYJY6pOTA7WDOgqjsz8zUEUZHCcj3P5wyVa-g,66
|
|
5
|
-
prefect/_build_info.py,sha256=
|
|
5
|
+
prefect/_build_info.py,sha256=pCAplbg5rvzGJCWVsSox7cI-qWxlq6-WNV935RCmdrA,185
|
|
6
6
|
prefect/_flow_run_suspension.py,sha256=5zTTB7ZIBHzoS0pVrhNn23-9hK51qZ3CQA6C-azluC0,4144
|
|
7
7
|
prefect/agent.py,sha256=dPvG1jDGD5HSH7aM2utwtk6RaJ9qg13XjkA0lAIgQmY,287
|
|
8
8
|
prefect/artifacts.py,sha256=ZdMLJeJGK82hibtRzbsVa-g95dMa0D2UP1LiESoXmf4,23951
|
|
@@ -117,7 +117,7 @@ prefect/_internal/schemas/_registry.py,sha256=PVI2no_ghoA4IE7gRD_HZqM6ENRrCOnGpE
|
|
|
117
117
|
prefect/_internal/schemas/bases.py,sha256=wYBIa5f5BwiKid7Rwp90gqxs7mt4qGBURdtv5dgWJxk,4583
|
|
118
118
|
prefect/_internal/schemas/fields.py,sha256=m4LrFNz8rA9uBhMk9VyQT6FIXmV_EVAW92hdXeSvHbY,837
|
|
119
119
|
prefect/_internal/schemas/serializers.py,sha256=G_RGHfObjisUiRvd29p-zc6W4bwt5rE1OdR6TXNrRhQ,825
|
|
120
|
-
prefect/_internal/schemas/validators.py,sha256=
|
|
120
|
+
prefect/_internal/schemas/validators.py,sha256=_MY6z9x7hcyFhdCZtSgCPhuxn0RmptGAw-qdiQwPuv8,25203
|
|
121
121
|
prefect/_sdk/__init__.py,sha256=Q90VEXD-AXAYunVyAYNs0uC8eOWtmPq-McMA0AdZblI,82
|
|
122
122
|
prefect/_sdk/fetcher.py,sha256=V0hmDpCAiRCxP8G9jU1hZHmioDxJOw31g4WnqcCjif0,14886
|
|
123
123
|
prefect/_sdk/generator.py,sha256=lDwLcZtzkl7F9RzYDAedXI3r-5LUz_jFudH3NPmvj8E,4203
|
|
@@ -310,7 +310,7 @@ prefect/docker/docker_image.py,sha256=afAOYLv8pIGBV-rDuMbzfnnfGlu7qMUb6l3jXGCAe_
|
|
|
310
310
|
prefect/events/AGENTS.md,sha256=ZCrjaA67BQGdDzO5R7yJB7HO5Ahb8z7A3sT8yuKQ6V0,3049
|
|
311
311
|
prefect/events/__init__.py,sha256=e3aIcHzTfseOmq_A3hM_jSPNYP_OdndJErzjcdrMuAc,2202
|
|
312
312
|
prefect/events/actions.py,sha256=Ii5P4FX10Xu1nzU0e7okA1KC1eVIHhzExubFurrOr_Q,9785
|
|
313
|
-
prefect/events/clients.py,sha256=
|
|
313
|
+
prefect/events/clients.py,sha256=yJHHrH0czVNaqD58f33Zz5Iq90At3jP1b-gCZu_Vl2Y,35056
|
|
314
314
|
prefect/events/filters.py,sha256=YiM5A9E3KWeVL7HoMHYbATXUK1_pnChg1-g746GaMds,9999
|
|
315
315
|
prefect/events/related.py,sha256=CTeexYUmmA93V4gsR33GIFmw-SS-X_ouOpRg-oeq-BU,6672
|
|
316
316
|
prefect/events/subscribers.py,sha256=OvT6vh6YVS5y_ktqCRM_ayNWQOFzZA_dvakKcSUl-4U,6714
|
|
@@ -368,7 +368,7 @@ prefect/runner/_workspace_resolver.py,sha256=HweB3dIfS-lOSdRnH1Bu3c8GcYXkM8KSfsu
|
|
|
368
368
|
prefect/runner/_workspace_starter.py,sha256=P9tX_iv0zROc6k6YB68L5eNiyuc_HoP2clOSqE--rsg,12096
|
|
369
369
|
prefect/runner/runner.py,sha256=ZNgmuD19yNldoCDCXynNp1NoB8idIbZQb24vdKCnMvM,75785
|
|
370
370
|
prefect/runner/server.py,sha256=YqvQjlxZZHyhSsqyaLvOy2NwTDg1hLSZB2PK3t8FJUg,3636
|
|
371
|
-
prefect/runner/storage.py,sha256=
|
|
371
|
+
prefect/runner/storage.py,sha256=ufGKaruyhirzZjhFD4LaQUpMRY2mmUwraEtaE-G9PSg,44168
|
|
372
372
|
prefect/runtime/__init__.py,sha256=JswiTlYRup2zXOYu8AqJ7czKtgcw9Kxo0tTbS6aWCqY,407
|
|
373
373
|
prefect/runtime/deployment.py,sha256=jYD-jkbb1dAl0wQuiY5mulVTqAEnF4xE3D5Iard6yms,5552
|
|
374
374
|
prefect/runtime/flow_run.py,sha256=KZRWdcqKOHvlK1g4OucFTH_o3XonoI1rzF81uQfjdu0,10793
|
|
@@ -1394,8 +1394,8 @@ prefect/workers/_worker_channel/_protocol.py,sha256=GpCNh1o3qmmqHA_UOOTge1QVC6IR
|
|
|
1394
1394
|
prefect/workers/_worker_channel/_state.py,sha256=eQTFZtAVDZH1vVWps3SdeY6aW3qu2wx1UKYQXK3AyuE,5369
|
|
1395
1395
|
prefect/workers/_worker_channel/_sync.py,sha256=G5G8_UaQYbeLebi5Mb1Z_KgGfXfyjXo5uT9la5_zwqY,14984
|
|
1396
1396
|
prefect/workers/_worker_channel/_transport.py,sha256=_8aWX16tdbZcpqBg4HCX_vCzl2FX47jsYPKMPLAANRA,9181
|
|
1397
|
-
prefect-3.7.8.
|
|
1398
|
-
prefect-3.7.8.
|
|
1399
|
-
prefect-3.7.8.
|
|
1400
|
-
prefect-3.7.8.
|
|
1401
|
-
prefect-3.7.8.
|
|
1397
|
+
prefect-3.7.8.dev4.dist-info/METADATA,sha256=ey2zeTcX-GQV6b73zJ8ZoTh4EK9XdkMFek8CSK86ROo,14030
|
|
1398
|
+
prefect-3.7.8.dev4.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
1399
|
+
prefect-3.7.8.dev4.dist-info/entry_points.txt,sha256=HlY8up83iIq2vU2r33a0qSis4eOFSyb1mRH4l7Xt9X8,126
|
|
1400
|
+
prefect-3.7.8.dev4.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
|
|
1401
|
+
prefect-3.7.8.dev4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|