prefect-client 3.0.1__py3-none-any.whl → 3.0.3__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 (48) hide show
  1. prefect/_internal/compatibility/deprecated.py +1 -1
  2. prefect/blocks/core.py +5 -4
  3. prefect/blocks/notifications.py +21 -0
  4. prefect/blocks/webhook.py +17 -1
  5. prefect/cache_policies.py +98 -28
  6. prefect/client/orchestration.py +42 -20
  7. prefect/client/schemas/actions.py +10 -2
  8. prefect/client/schemas/filters.py +4 -2
  9. prefect/client/schemas/objects.py +48 -6
  10. prefect/client/schemas/responses.py +15 -1
  11. prefect/client/types/flexible_schedule_list.py +1 -1
  12. prefect/concurrency/asyncio.py +45 -6
  13. prefect/concurrency/services.py +1 -1
  14. prefect/concurrency/sync.py +21 -27
  15. prefect/concurrency/v1/asyncio.py +3 -0
  16. prefect/concurrency/v1/sync.py +4 -5
  17. prefect/context.py +6 -6
  18. prefect/deployments/runner.py +43 -5
  19. prefect/events/actions.py +6 -0
  20. prefect/flow_engine.py +12 -4
  21. prefect/flows.py +15 -11
  22. prefect/locking/filesystem.py +243 -0
  23. prefect/logging/handlers.py +0 -2
  24. prefect/logging/loggers.py +0 -18
  25. prefect/logging/logging.yml +1 -0
  26. prefect/main.py +19 -5
  27. prefect/plugins.py +9 -1
  28. prefect/records/base.py +12 -0
  29. prefect/records/filesystem.py +6 -2
  30. prefect/records/memory.py +6 -0
  31. prefect/records/result_store.py +6 -0
  32. prefect/results.py +192 -29
  33. prefect/runner/runner.py +74 -6
  34. prefect/settings.py +31 -1
  35. prefect/states.py +34 -17
  36. prefect/task_engine.py +58 -43
  37. prefect/transactions.py +113 -52
  38. prefect/utilities/asyncutils.py +7 -0
  39. prefect/utilities/collections.py +3 -2
  40. prefect/utilities/engine.py +20 -9
  41. prefect/utilities/importtools.py +1 -0
  42. prefect/utilities/urls.py +70 -12
  43. prefect/workers/base.py +10 -8
  44. {prefect_client-3.0.1.dist-info → prefect_client-3.0.3.dist-info}/METADATA +1 -1
  45. {prefect_client-3.0.1.dist-info → prefect_client-3.0.3.dist-info}/RECORD +48 -47
  46. {prefect_client-3.0.1.dist-info → prefect_client-3.0.3.dist-info}/LICENSE +0 -0
  47. {prefect_client-3.0.1.dist-info → prefect_client-3.0.3.dist-info}/WHEEL +0 -0
  48. {prefect_client-3.0.1.dist-info → prefect_client-3.0.3.dist-info}/top_level.txt +0 -0
prefect/results.py CHANGED
@@ -5,6 +5,7 @@ import socket
5
5
  import threading
6
6
  import uuid
7
7
  from functools import partial
8
+ from pathlib import Path
8
9
  from typing import (
9
10
  TYPE_CHECKING,
10
11
  Any,
@@ -19,6 +20,8 @@ from typing import (
19
20
  )
20
21
  from uuid import UUID
21
22
 
23
+ import pendulum
24
+ from cachetools import LRUCache
22
25
  from pydantic import (
23
26
  BaseModel,
24
27
  ConfigDict,
@@ -33,6 +36,8 @@ from pydantic_extra_types.pendulum_dt import DateTime
33
36
  from typing_extensions import ParamSpec, Self
34
37
 
35
38
  import prefect
39
+ from prefect._internal.compatibility import deprecated
40
+ from prefect._internal.compatibility.deprecated import deprecated_field
36
41
  from prefect.blocks.core import Block
37
42
  from prefect.client.utilities import inject_client
38
43
  from prefect.exceptions import (
@@ -86,18 +91,26 @@ async def get_default_result_storage() -> WritableFileSystem:
86
91
  Generate a default file system for result storage.
87
92
  """
88
93
  default_block = PREFECT_DEFAULT_RESULT_STORAGE_BLOCK.value()
94
+ basepath = PREFECT_LOCAL_STORAGE_PATH.value()
95
+
96
+ cache_key = (str(default_block), str(basepath))
97
+
98
+ if cache_key in _default_storages:
99
+ return _default_storages[cache_key]
89
100
 
90
101
  if default_block is not None:
91
- return await resolve_result_storage(default_block)
102
+ storage = await resolve_result_storage(default_block)
103
+ else:
104
+ # Use the local file system
105
+ storage = LocalFileSystem(basepath=str(basepath))
92
106
 
93
- # otherwise, use the local file system
94
- basepath = PREFECT_LOCAL_STORAGE_PATH.value()
95
- return LocalFileSystem(basepath=str(basepath))
107
+ _default_storages[cache_key] = storage
108
+ return storage
96
109
 
97
110
 
98
111
  @sync_compatible
99
112
  async def resolve_result_storage(
100
- result_storage: ResultStorage,
113
+ result_storage: Union[ResultStorage, UUID, Path],
101
114
  ) -> WritableFileSystem:
102
115
  """
103
116
  Resolve one of the valid `ResultStorage` input types into a saved block
@@ -114,10 +127,15 @@ async def resolve_result_storage(
114
127
  storage_block_id = storage_block._block_document_id
115
128
  else:
116
129
  storage_block_id = None
130
+ elif isinstance(result_storage, Path):
131
+ storage_block = LocalFileSystem(basepath=str(result_storage))
117
132
  elif isinstance(result_storage, str):
118
133
  storage_block = await Block.load(result_storage, client=client)
119
134
  storage_block_id = storage_block._block_document_id
120
135
  assert storage_block_id is not None, "Loaded storage blocks must have ids"
136
+ elif isinstance(result_storage, UUID):
137
+ block_document = await client.read_block_document(result_storage)
138
+ storage_block = Block._from_block_document(block_document)
121
139
  else:
122
140
  raise TypeError(
123
141
  "Result storage must be one of the following types: 'UUID', 'Block', "
@@ -171,6 +189,25 @@ def get_default_persist_setting() -> bool:
171
189
  return PREFECT_RESULTS_PERSIST_BY_DEFAULT.value()
172
190
 
173
191
 
192
+ def should_persist_result() -> bool:
193
+ """
194
+ Return the default option for result persistence determined by the current run context.
195
+
196
+ If there is no current run context, the default value set by
197
+ `PREFECT_RESULTS_PERSIST_BY_DEFAULT` will be returned.
198
+ """
199
+ from prefect.context import FlowRunContext, TaskRunContext
200
+
201
+ task_run_context = TaskRunContext.get()
202
+ if task_run_context is not None:
203
+ return task_run_context.persist_result
204
+ flow_run_context = FlowRunContext.get()
205
+ if flow_run_context is not None:
206
+ return flow_run_context.persist_result
207
+
208
+ return PREFECT_RESULTS_PERSIST_BY_DEFAULT.value()
209
+
210
+
174
211
  def _format_user_supplied_storage_key(key: str) -> str:
175
212
  # Note here we are pinning to task runs since flow runs do not support storage keys
176
213
  # yet; we'll need to split logic in the future or have two separate functions
@@ -178,6 +215,16 @@ def _format_user_supplied_storage_key(key: str) -> str:
178
215
  return key.format(**runtime_vars, parameters=prefect.runtime.task_run.parameters)
179
216
 
180
217
 
218
+ T = TypeVar("T")
219
+
220
+
221
+ @deprecated_field(
222
+ "persist_result",
223
+ when=lambda x: x is not None,
224
+ when_message="use the `should_persist_result` utility function instead",
225
+ start_date="Sep 2024",
226
+ end_date="Nov 2024",
227
+ )
181
228
  class ResultStore(BaseModel):
182
229
  """
183
230
  Manages the storage and retrieval of results.
@@ -200,10 +247,13 @@ class ResultStore(BaseModel):
200
247
  result_storage: Optional[WritableFileSystem] = Field(default=None)
201
248
  metadata_storage: Optional[WritableFileSystem] = Field(default=None)
202
249
  lock_manager: Optional[LockManager] = Field(default=None)
203
- persist_result: bool = Field(default_factory=get_default_persist_setting)
204
250
  cache_result_in_memory: bool = Field(default=True)
205
251
  serializer: Serializer = Field(default_factory=get_default_result_serializer)
206
252
  storage_key_fn: Callable[[], str] = Field(default=DEFAULT_STORAGE_KEY_FN)
253
+ cache: LRUCache = Field(default_factory=lambda: LRUCache(maxsize=1000))
254
+
255
+ # Deprecated fields
256
+ persist_result: Optional[bool] = Field(default=None)
207
257
 
208
258
  @property
209
259
  def result_storage_block_id(self) -> Optional[UUID]:
@@ -227,8 +277,6 @@ class ResultStore(BaseModel):
227
277
  update["result_storage"] = await resolve_result_storage(flow.result_storage)
228
278
  if flow.result_serializer is not None:
229
279
  update["serializer"] = resolve_serializer(flow.result_serializer)
230
- if flow.persist_result is not None:
231
- update["persist_result"] = flow.persist_result
232
280
  if flow.cache_result_in_memory is not None:
233
281
  update["cache_result_in_memory"] = flow.cache_result_in_memory
234
282
  if self.result_storage is None and update.get("result_storage") is None:
@@ -251,14 +299,21 @@ class ResultStore(BaseModel):
251
299
  update["result_storage"] = await resolve_result_storage(task.result_storage)
252
300
  if task.result_serializer is not None:
253
301
  update["serializer"] = resolve_serializer(task.result_serializer)
254
- if task.persist_result is not None:
255
- update["persist_result"] = task.persist_result
256
302
  if task.cache_result_in_memory is not None:
257
303
  update["cache_result_in_memory"] = task.cache_result_in_memory
258
304
  if task.result_storage_key is not None:
259
305
  update["storage_key_fn"] = partial(
260
306
  _format_user_supplied_storage_key, task.result_storage_key
261
307
  )
308
+ if task.cache_policy is not None and task.cache_policy is not NotSet:
309
+ if task.cache_policy.key_storage is not None:
310
+ storage = task.cache_policy.key_storage
311
+ if isinstance(storage, str) and not len(storage.split("/")) == 2:
312
+ storage = Path(storage)
313
+ update["metadata_storage"] = await resolve_result_storage(storage)
314
+ if task.cache_policy.lock_manager is not None:
315
+ update["lock_manager"] = task.cache_policy.lock_manager
316
+
262
317
  if self.result_storage is None and update.get("result_storage") is None:
263
318
  update["result_storage"] = await get_default_result_storage()
264
319
  return self.model_copy(update=update)
@@ -293,16 +348,30 @@ class ResultStore(BaseModel):
293
348
  # so the entire payload doesn't need to be read
294
349
  try:
295
350
  metadata_content = await self.metadata_storage.read_path(key)
296
- return metadata_content is not None
351
+ if metadata_content is None:
352
+ return False
353
+ metadata = ResultRecordMetadata.load_bytes(metadata_content)
354
+
297
355
  except Exception:
298
356
  return False
299
357
  else:
300
358
  try:
301
359
  content = await self.result_storage.read_path(key)
302
- return content is not None
360
+ if content is None:
361
+ return False
362
+ record = ResultRecord.deserialize(content)
363
+ metadata = record.metadata
303
364
  except Exception:
304
365
  return False
305
366
 
367
+ if metadata.expiration:
368
+ # if the result has an expiration,
369
+ # check if it is still in the future
370
+ exists = metadata.expiration > pendulum.now("utc")
371
+ else:
372
+ exists = True
373
+ return exists
374
+
306
375
  def exists(self, key: str) -> bool:
307
376
  """
308
377
  Check if a result record exists in storage.
@@ -342,9 +411,13 @@ class ResultStore(BaseModel):
342
411
  Returns:
343
412
  A result record.
344
413
  """
414
+
345
415
  if self.lock_manager is not None and not self.is_lock_holder(key, holder):
346
416
  await self.await_for_lock(key)
347
417
 
418
+ if key in self.cache:
419
+ return self.cache[key]
420
+
348
421
  if self.result_storage is None:
349
422
  self.result_storage = await get_default_result_storage()
350
423
 
@@ -355,12 +428,23 @@ class ResultStore(BaseModel):
355
428
  metadata.storage_key is not None
356
429
  ), "Did not find storage key in metadata"
357
430
  result_content = await self.result_storage.read_path(metadata.storage_key)
358
- return ResultRecord.deserialize_from_result_and_metadata(
431
+ result_record = ResultRecord.deserialize_from_result_and_metadata(
359
432
  result=result_content, metadata=metadata_content
360
433
  )
361
434
  else:
362
435
  content = await self.result_storage.read_path(key)
363
- return ResultRecord.deserialize(content)
436
+ result_record = ResultRecord.deserialize(content)
437
+
438
+ if self.cache_result_in_memory:
439
+ if self.result_storage_block_id is None and hasattr(
440
+ self.result_storage, "_resolve_path"
441
+ ):
442
+ cache_key = str(self.result_storage._resolve_path(key))
443
+ else:
444
+ cache_key = key
445
+
446
+ self.cache[cache_key] = result_record
447
+ return result_record
364
448
 
365
449
  def read(self, key: str, holder: Optional[str] = None) -> "ResultRecord":
366
450
  """
@@ -390,10 +474,10 @@ class ResultStore(BaseModel):
390
474
 
391
475
  def create_result_record(
392
476
  self,
393
- key: str,
394
477
  obj: Any,
478
+ key: Optional[str] = None,
395
479
  expiration: Optional[DateTime] = None,
396
- ):
480
+ ) -> "ResultRecord":
397
481
  """
398
482
  Create a result record.
399
483
 
@@ -404,6 +488,13 @@ class ResultStore(BaseModel):
404
488
  """
405
489
  key = key or self.storage_key_fn()
406
490
 
491
+ if self.result_storage is None:
492
+ self.result_storage = get_default_result_storage(_sync=True)
493
+
494
+ if self.result_storage_block_id is None:
495
+ if hasattr(self.result_storage, "_resolve_path"):
496
+ key = str(self.result_storage._resolve_path(key))
497
+
407
498
  return ResultRecord(
408
499
  result=obj,
409
500
  metadata=ResultRecordMetadata(
@@ -416,8 +507,8 @@ class ResultStore(BaseModel):
416
507
 
417
508
  def write(
418
509
  self,
419
- key: str,
420
510
  obj: Any,
511
+ key: Optional[str] = None,
421
512
  expiration: Optional[DateTime] = None,
422
513
  holder: Optional[str] = None,
423
514
  ):
@@ -433,17 +524,18 @@ class ResultStore(BaseModel):
433
524
  holder: The holder of the lock if a lock was set on the record.
434
525
  """
435
526
  holder = holder or self.generate_default_holder()
527
+ result_record = self.create_result_record(
528
+ key=key, obj=obj, expiration=expiration
529
+ )
436
530
  return self.persist_result_record(
437
- result_record=self.create_result_record(
438
- key=key, obj=obj, expiration=expiration
439
- ),
531
+ result_record=result_record,
440
532
  holder=holder,
441
533
  )
442
534
 
443
535
  async def awrite(
444
536
  self,
445
- key: str,
446
537
  obj: Any,
538
+ key: Optional[str] = None,
447
539
  expiration: Optional[DateTime] = None,
448
540
  holder: Optional[str] = None,
449
541
  ):
@@ -478,13 +570,22 @@ class ResultStore(BaseModel):
478
570
  ), "Storage key is required on result record"
479
571
 
480
572
  key = result_record.metadata.storage_key
573
+ if result_record.metadata.storage_block_id is None:
574
+ basepath = (
575
+ self.result_storage._resolve_path("")
576
+ if hasattr(self.result_storage, "_resolve_path")
577
+ else Path(".").resolve()
578
+ )
579
+ base_key = str(Path(key).relative_to(basepath))
580
+ else:
581
+ base_key = key
481
582
  if (
482
583
  self.lock_manager is not None
483
- and self.is_locked(key)
484
- and not self.is_lock_holder(key, holder)
584
+ and self.is_locked(base_key)
585
+ and not self.is_lock_holder(base_key, holder)
485
586
  ):
486
587
  raise RuntimeError(
487
- f"Cannot write to result record with key {key} because it is locked by "
588
+ f"Cannot write to result record with key {base_key} because it is locked by "
488
589
  f"another holder."
489
590
  )
490
591
  if self.result_storage is None:
@@ -497,7 +598,7 @@ class ResultStore(BaseModel):
497
598
  content=result_record.serialize_result(),
498
599
  )
499
600
  await self.metadata_storage.write_path(
500
- result_record.metadata.storage_key,
601
+ base_key,
501
602
  content=result_record.serialize_metadata(),
502
603
  )
503
604
  # Otherwise, write the result metadata and result together
@@ -506,6 +607,9 @@ class ResultStore(BaseModel):
506
607
  result_record.metadata.storage_key, content=result_record.serialize()
507
608
  )
508
609
 
610
+ if self.cache_result_in_memory:
611
+ self.cache[key] = result_record
612
+
509
613
  def persist_result_record(
510
614
  self, result_record: "ResultRecord", holder: Optional[str] = None
511
615
  ):
@@ -671,6 +775,11 @@ class ResultStore(BaseModel):
671
775
  )
672
776
  return await self.lock_manager.await_for_lock(key, timeout)
673
777
 
778
+ @deprecated.deprecated_callable(
779
+ start_date="Sep 2024",
780
+ end_date="Nov 2024",
781
+ help="Use `create_result_record` instead.",
782
+ )
674
783
  @sync_compatible
675
784
  async def create_result(
676
785
  self,
@@ -683,6 +792,11 @@ class ResultStore(BaseModel):
683
792
  """
684
793
  # Null objects are "cached" in memory at no cost
685
794
  should_cache_object = self.cache_result_in_memory or obj is None
795
+ should_persist_result = (
796
+ self.persist_result
797
+ if self.persist_result is not None
798
+ else not should_cache_object
799
+ )
686
800
 
687
801
  if key:
688
802
 
@@ -704,7 +818,7 @@ class ResultStore(BaseModel):
704
818
  serializer=self.serializer,
705
819
  cache_object=should_cache_object,
706
820
  expiration=expiration,
707
- serialize_to_none=not self.persist_result,
821
+ serialize_to_none=not should_persist_result,
708
822
  )
709
823
 
710
824
  # TODO: These two methods need to find a new home
@@ -729,7 +843,7 @@ class ResultStore(BaseModel):
729
843
  return record.result
730
844
 
731
845
 
732
- def get_current_result_store() -> ResultStore:
846
+ def get_result_store() -> ResultStore:
733
847
  """
734
848
  Get the current result store.
735
849
  """
@@ -779,6 +893,17 @@ class ResultRecordMetadata(BaseModel):
779
893
  """
780
894
  return cls.model_validate_json(data)
781
895
 
896
+ def __eq__(self, other):
897
+ if not isinstance(other, ResultRecordMetadata):
898
+ return False
899
+ return (
900
+ self.storage_key == other.storage_key
901
+ and self.expiration == other.expiration
902
+ and self.serializer == other.serializer
903
+ and self.prefect_version == other.prefect_version
904
+ and self.storage_block_id == other.storage_block_id
905
+ )
906
+
782
907
 
783
908
  class ResultRecord(BaseModel, Generic[R]):
784
909
  """
@@ -856,6 +981,31 @@ class ResultRecord(BaseModel, Generic[R]):
856
981
  value["metadata"]["prefect_version"] = value.pop("prefect_version")
857
982
  return value
858
983
 
984
+ @classmethod
985
+ async def _from_metadata(cls, metadata: ResultRecordMetadata) -> "ResultRecord[R]":
986
+ """
987
+ Create a result record from metadata.
988
+
989
+ Will use the result record metadata to fetch data via a result store.
990
+
991
+ Args:
992
+ metadata: The metadata to create the result record from.
993
+
994
+ Returns:
995
+ ResultRecord: The result record.
996
+ """
997
+ if metadata.storage_block_id is None:
998
+ storage_block = None
999
+ else:
1000
+ storage_block = await resolve_result_storage(
1001
+ metadata.storage_block_id, _sync=False
1002
+ )
1003
+ store = ResultStore(
1004
+ result_storage=storage_block, serializer=metadata.serializer
1005
+ )
1006
+ result = await store.aread(metadata.storage_key)
1007
+ return result
1008
+
859
1009
  def serialize_metadata(self) -> bytes:
860
1010
  return self.metadata.dump_bytes()
861
1011
 
@@ -913,7 +1063,15 @@ class ResultRecord(BaseModel, Generic[R]):
913
1063
  result=result_record_metadata.serializer.loads(result),
914
1064
  )
915
1065
 
1066
+ def __eq__(self, other):
1067
+ if not isinstance(other, ResultRecord):
1068
+ return False
1069
+ return self.metadata == other.metadata and self.result == other.result
1070
+
916
1071
 
1072
+ @deprecated.deprecated_class(
1073
+ start_date="Sep 2024", end_date="Nov 2024", help="Use `ResultRecord` instead."
1074
+ )
917
1075
  @register_base_type
918
1076
  class BaseResult(BaseModel, abc.ABC, Generic[R]):
919
1077
  model_config = ConfigDict(extra="forbid")
@@ -964,6 +1122,9 @@ class BaseResult(BaseModel, abc.ABC, Generic[R]):
964
1122
  return cls.__name__ if isinstance(default, PydanticUndefinedType) else default
965
1123
 
966
1124
 
1125
+ @deprecated.deprecated_class(
1126
+ start_date="Sep 2024", end_date="Nov 2024", help="Use `ResultRecord` instead."
1127
+ )
967
1128
  class PersistedResult(BaseResult):
968
1129
  """
969
1130
  Result type which stores a reference to a persisted result.
@@ -1057,7 +1218,6 @@ class PersistedResult(BaseResult):
1057
1218
  """
1058
1219
  Write the result to the storage block.
1059
1220
  """
1060
-
1061
1221
  if self._persisted or self.serialize_to_none:
1062
1222
  # don't double write or overwrite
1063
1223
  return
@@ -1078,7 +1238,10 @@ class PersistedResult(BaseResult):
1078
1238
  # this could error if the serializer requires kwargs
1079
1239
  serializer = Serializer(type=self.serializer_type)
1080
1240
 
1081
- result_store = ResultStore(result_storage=storage_block, serializer=serializer)
1241
+ result_store = ResultStore(
1242
+ result_storage=storage_block,
1243
+ serializer=serializer,
1244
+ )
1082
1245
  await result_store.awrite(
1083
1246
  obj=obj, key=self.storage_key, expiration=self.expiration
1084
1247
  )
prefect/runner/runner.py CHANGED
@@ -64,8 +64,18 @@ from prefect.client.schemas.filters import (
64
64
  FlowRunFilterStateName,
65
65
  FlowRunFilterStateType,
66
66
  )
67
+ from prefect.client.schemas.objects import (
68
+ ConcurrencyLimitConfig,
69
+ FlowRun,
70
+ State,
71
+ StateType,
72
+ )
67
73
  from prefect.client.schemas.objects import Flow as APIFlow
68
- from prefect.client.schemas.objects import FlowRun, State, StateType
74
+ from prefect.concurrency.asyncio import (
75
+ AcquireConcurrencySlotTimeoutError,
76
+ ConcurrencySlotAcquisitionError,
77
+ concurrency,
78
+ )
69
79
  from prefect.events import DeploymentTriggerTypes, TriggerTypes
70
80
  from prefect.events.related import tags_as_related_resources
71
81
  from prefect.events.schemas.events import RelatedResource
@@ -81,7 +91,12 @@ from prefect.settings import (
81
91
  PREFECT_RUNNER_SERVER_ENABLE,
82
92
  get_current_settings,
83
93
  )
84
- from prefect.states import Crashed, Pending, exception_to_failed_state
94
+ from prefect.states import (
95
+ AwaitingConcurrencySlot,
96
+ Crashed,
97
+ Pending,
98
+ exception_to_failed_state,
99
+ )
85
100
  from prefect.types.entrypoint import EntrypointType
86
101
  from prefect.utilities.asyncutils import (
87
102
  asyncnullcontext,
@@ -226,6 +241,7 @@ class Runner:
226
241
  rrule: Optional[Union[Iterable[str], str]] = None,
227
242
  paused: Optional[bool] = None,
228
243
  schedules: Optional["FlexibleScheduleList"] = None,
244
+ concurrency_limit: Optional[Union[int, ConcurrencyLimitConfig, None]] = None,
229
245
  parameters: Optional[dict] = None,
230
246
  triggers: Optional[List[Union[DeploymentTriggerTypes, TriggerTypes]]] = None,
231
247
  description: Optional[str] = None,
@@ -248,6 +264,10 @@ class Runner:
248
264
  or a timedelta object. If a number is given, it will be interpreted as seconds.
249
265
  cron: A cron schedule of when to execute runs of this flow.
250
266
  rrule: An rrule schedule of when to execute runs of this flow.
267
+ paused: Whether or not to set the created deployment as paused.
268
+ schedules: A list of schedule objects defining when to execute runs of this flow.
269
+ Used to define multiple schedules or additional scheduling options like `timezone`.
270
+ concurrency_limit: The maximum number of concurrent runs of this flow to allow.
251
271
  triggers: A list of triggers that should kick of a run of this flow.
252
272
  parameters: A dictionary of default parameter values to pass to runs of this flow.
253
273
  description: A description for the created deployment. Defaults to the flow's
@@ -280,6 +300,7 @@ class Runner:
280
300
  version=version,
281
301
  enforce_parameter_schema=enforce_parameter_schema,
282
302
  entrypoint_type=entrypoint_type,
303
+ concurrency_limit=concurrency_limit,
283
304
  )
284
305
  return await self.add_deployment(deployment)
285
306
 
@@ -959,6 +980,7 @@ class Runner:
959
980
  """
960
981
  submittable_flow_runs = flow_run_response
961
982
  submittable_flow_runs.sort(key=lambda run: run.next_scheduled_start_time)
983
+
962
984
  for i, flow_run in enumerate(submittable_flow_runs):
963
985
  if flow_run.id in self._submitting_flow_run_ids:
964
986
  continue
@@ -1025,12 +1047,38 @@ class Runner:
1025
1047
  ) -> Union[Optional[int], Exception]:
1026
1048
  run_logger = self._get_flow_run_logger(flow_run)
1027
1049
 
1050
+ if flow_run.deployment_id:
1051
+ deployment = await self._client.read_deployment(flow_run.deployment_id)
1052
+ if deployment and deployment.global_concurrency_limit:
1053
+ limit_name = deployment.global_concurrency_limit.name
1054
+ concurrency_ctx = concurrency
1055
+ else:
1056
+ limit_name = ""
1057
+ concurrency_ctx = asyncnullcontext
1058
+
1028
1059
  try:
1029
- status_code = await self._run_process(
1030
- flow_run=flow_run,
1031
- task_status=task_status,
1032
- entrypoint=entrypoint,
1060
+ async with concurrency_ctx(limit_name, max_retries=0, strict=True):
1061
+ status_code = await self._run_process(
1062
+ flow_run=flow_run,
1063
+ task_status=task_status,
1064
+ entrypoint=entrypoint,
1065
+ )
1066
+ except (
1067
+ AcquireConcurrencySlotTimeoutError,
1068
+ ConcurrencySlotAcquisitionError,
1069
+ ) as exc:
1070
+ self._logger.info(
1071
+ (
1072
+ "Deployment %s reached its concurrency limit when attempting to execute flow run %s. Will attempt to execute later."
1073
+ ),
1074
+ flow_run.deployment_id,
1075
+ flow_run.name,
1033
1076
  )
1077
+ await self._propose_scheduled_state(flow_run)
1078
+
1079
+ if not task_status._future.done():
1080
+ task_status.started(exc)
1081
+ return exc
1034
1082
  except Exception as exc:
1035
1083
  if not task_status._future.done():
1036
1084
  # This flow run was being submitted and did not start successfully
@@ -1116,6 +1164,26 @@ class Runner:
1116
1164
  exc_info=True,
1117
1165
  )
1118
1166
 
1167
+ async def _propose_scheduled_state(self, flow_run: "FlowRun") -> None:
1168
+ run_logger = self._get_flow_run_logger(flow_run)
1169
+ try:
1170
+ state = await propose_state(
1171
+ self._client,
1172
+ AwaitingConcurrencySlot(),
1173
+ flow_run_id=flow_run.id,
1174
+ )
1175
+ self._logger.info(f"Flow run {flow_run.id} now has state {state.name}")
1176
+ except Abort as exc:
1177
+ run_logger.info(
1178
+ (
1179
+ f"Aborted rescheduling of flow run '{flow_run.id}'. "
1180
+ f"Server sent an abort signal: {exc}"
1181
+ ),
1182
+ )
1183
+ pass
1184
+ except Exception:
1185
+ run_logger.exception(f"Failed to update state of flow run '{flow_run.id}'")
1186
+
1119
1187
  async def _propose_crashed_state(self, flow_run: "FlowRun", message: str) -> None:
1120
1188
  run_logger = self._get_flow_run_logger(flow_run)
1121
1189
  try:
prefect/settings.py CHANGED
@@ -637,7 +637,7 @@ PREFECT_API_KEY = Setting(
637
637
  )
638
638
  """API key used to authenticate with a the Prefect API. Defaults to `None`."""
639
639
 
640
- PREFECT_API_ENABLE_HTTP2 = Setting(bool, default=True)
640
+ PREFECT_API_ENABLE_HTTP2 = Setting(bool, default=False)
641
641
  """
642
642
  If true, enable support for HTTP/2 for communicating with an API.
643
643
 
@@ -1288,6 +1288,36 @@ compromise. Adjust this setting based on your specific security requirements
1288
1288
  and usage patterns.
1289
1289
  """
1290
1290
 
1291
+ PREFECT_SERVER_CORS_ALLOWED_ORIGINS = Setting(
1292
+ str,
1293
+ default="*",
1294
+ )
1295
+ """
1296
+ A comma-separated list of origins that are authorized to make cross-origin requests to the API.
1297
+
1298
+ By default, this is set to `*`, which allows requests from all origins.
1299
+ """
1300
+
1301
+ PREFECT_SERVER_CORS_ALLOWED_METHODS = Setting(
1302
+ str,
1303
+ default="*",
1304
+ )
1305
+ """
1306
+ A comma-separated list of methods that are authorized to make cross-origin requests to the API.
1307
+
1308
+ By default, this is set to `*`, which allows requests with all methods.
1309
+ """
1310
+
1311
+ PREFECT_SERVER_CORS_ALLOWED_HEADERS = Setting(
1312
+ str,
1313
+ default="*",
1314
+ )
1315
+ """
1316
+ A comma-separated list of headers that are authorized to make cross-origin requests to the API.
1317
+
1318
+ By default, this is set to `*`, which allows requests with all headers.
1319
+ """
1320
+
1291
1321
  PREFECT_SERVER_ALLOW_EPHEMERAL_MODE = Setting(bool, default=False)
1292
1322
  """
1293
1323
  Controls whether or not a subprocess server can be started when no API URL is provided.