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 CHANGED
@@ -1,5 +1,5 @@
1
1
  # Generated by versioningit
2
- __version__ = "3.7.8.dev2"
3
- __build_date__ = "2026-07-07 09:14:19.574605+00:00"
4
- __git_commit__ = "3f47d765fea655c92f2d700fedee8635ea2d4d2f"
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).replace(tzinfo=None)
405
- elif now.tzinfo is not None:
406
- now = now.astimezone(datetime.timezone.utc).replace(tzinfo=None)
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(rrule)
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
- for event in events_to_resend:
440
- await self.emit(event)
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 self._websocket and self._unconfirmed_events:
459
- try:
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
- "Time-based checkpoint failed, will retry next interval.",
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
- shutil.rmtree(self.destination)
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
- shutil.rmtree(self.destination)
495
+ _rmtree_including_read_only(self.destination)
453
496
  await self._clone_repo()
454
497
 
455
498
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prefect
3
- Version: 3.7.8.dev2
3
+ Version: 3.7.8.dev4
4
4
  Summary: Workflow orchestration and management.
5
5
  Project-URL: Changelog, https://github.com/PrefectHQ/prefect/releases
6
6
  Project-URL: Documentation, https://docs.prefect.io
@@ -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=w7179WVe7MRkK0cIlgyfo5rc890eQ_MHOvQYRx8UBBU,185
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=YxOIn06SCWVk07ihLva86VTF-T1xinKi8ysMEFm3SXc,23978
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=gL8EClV_ryULkXvnR8DU0nrOH1kTd8FrMPYKy3c8SdQ,33354
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=rG0P79LQhACcU05roMbcG3WgZXRmPL-SMfh3LlCr44Q,42470
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.dev2.dist-info/METADATA,sha256=PEjGW1KfvJ7mbVRcxw1R6mSUQFhx9GsK0yOqeoS4pvQ,14030
1398
- prefect-3.7.8.dev2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
1399
- prefect-3.7.8.dev2.dist-info/entry_points.txt,sha256=HlY8up83iIq2vU2r33a0qSis4eOFSyb1mRH4l7Xt9X8,126
1400
- prefect-3.7.8.dev2.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
1401
- prefect-3.7.8.dev2.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.30.1
2
+ Generator: hatchling 1.31.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any