prefect-client 2.20.4__py3-none-any.whl → 3.0.0__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 (288) hide show
  1. prefect/__init__.py +74 -110
  2. prefect/_internal/compatibility/deprecated.py +6 -115
  3. prefect/_internal/compatibility/experimental.py +4 -79
  4. prefect/_internal/compatibility/migration.py +166 -0
  5. prefect/_internal/concurrency/__init__.py +2 -2
  6. prefect/_internal/concurrency/api.py +1 -35
  7. prefect/_internal/concurrency/calls.py +0 -6
  8. prefect/_internal/concurrency/cancellation.py +0 -3
  9. prefect/_internal/concurrency/event_loop.py +0 -20
  10. prefect/_internal/concurrency/inspection.py +3 -3
  11. prefect/_internal/concurrency/primitives.py +1 -0
  12. prefect/_internal/concurrency/services.py +23 -0
  13. prefect/_internal/concurrency/threads.py +35 -0
  14. prefect/_internal/concurrency/waiters.py +0 -28
  15. prefect/_internal/integrations.py +7 -0
  16. prefect/_internal/pydantic/__init__.py +0 -45
  17. prefect/_internal/pydantic/annotations/pendulum.py +2 -2
  18. prefect/_internal/pydantic/v1_schema.py +21 -22
  19. prefect/_internal/pydantic/v2_schema.py +0 -2
  20. prefect/_internal/pydantic/v2_validated_func.py +18 -23
  21. prefect/_internal/pytz.py +1 -1
  22. prefect/_internal/retries.py +61 -0
  23. prefect/_internal/schemas/bases.py +45 -177
  24. prefect/_internal/schemas/fields.py +1 -43
  25. prefect/_internal/schemas/validators.py +47 -233
  26. prefect/agent.py +3 -695
  27. prefect/artifacts.py +173 -14
  28. prefect/automations.py +39 -4
  29. prefect/blocks/abstract.py +1 -1
  30. prefect/blocks/core.py +405 -153
  31. prefect/blocks/fields.py +2 -57
  32. prefect/blocks/notifications.py +43 -28
  33. prefect/blocks/redis.py +168 -0
  34. prefect/blocks/system.py +67 -20
  35. prefect/blocks/webhook.py +2 -9
  36. prefect/cache_policies.py +239 -0
  37. prefect/client/__init__.py +4 -0
  38. prefect/client/base.py +33 -27
  39. prefect/client/cloud.py +65 -20
  40. prefect/client/collections.py +1 -1
  41. prefect/client/orchestration.py +650 -442
  42. prefect/client/schemas/actions.py +115 -100
  43. prefect/client/schemas/filters.py +46 -52
  44. prefect/client/schemas/objects.py +228 -178
  45. prefect/client/schemas/responses.py +18 -36
  46. prefect/client/schemas/schedules.py +55 -36
  47. prefect/client/schemas/sorting.py +2 -0
  48. prefect/client/subscriptions.py +8 -7
  49. prefect/client/types/flexible_schedule_list.py +11 -0
  50. prefect/client/utilities.py +9 -6
  51. prefect/concurrency/asyncio.py +60 -11
  52. prefect/concurrency/context.py +24 -0
  53. prefect/concurrency/events.py +2 -2
  54. prefect/concurrency/services.py +46 -16
  55. prefect/concurrency/sync.py +51 -7
  56. prefect/concurrency/v1/asyncio.py +143 -0
  57. prefect/concurrency/v1/context.py +27 -0
  58. prefect/concurrency/v1/events.py +61 -0
  59. prefect/concurrency/v1/services.py +116 -0
  60. prefect/concurrency/v1/sync.py +92 -0
  61. prefect/context.py +246 -149
  62. prefect/deployments/__init__.py +33 -18
  63. prefect/deployments/base.py +10 -15
  64. prefect/deployments/deployments.py +2 -1048
  65. prefect/deployments/flow_runs.py +178 -0
  66. prefect/deployments/runner.py +72 -173
  67. prefect/deployments/schedules.py +31 -25
  68. prefect/deployments/steps/__init__.py +0 -1
  69. prefect/deployments/steps/core.py +7 -0
  70. prefect/deployments/steps/pull.py +15 -21
  71. prefect/deployments/steps/utility.py +2 -1
  72. prefect/docker/__init__.py +20 -0
  73. prefect/docker/docker_image.py +82 -0
  74. prefect/engine.py +15 -2475
  75. prefect/events/actions.py +17 -23
  76. prefect/events/cli/automations.py +20 -7
  77. prefect/events/clients.py +142 -80
  78. prefect/events/filters.py +14 -18
  79. prefect/events/related.py +74 -75
  80. prefect/events/schemas/__init__.py +0 -5
  81. prefect/events/schemas/automations.py +55 -46
  82. prefect/events/schemas/deployment_triggers.py +7 -197
  83. prefect/events/schemas/events.py +46 -65
  84. prefect/events/schemas/labelling.py +10 -14
  85. prefect/events/utilities.py +4 -5
  86. prefect/events/worker.py +23 -8
  87. prefect/exceptions.py +15 -0
  88. prefect/filesystems.py +30 -529
  89. prefect/flow_engine.py +827 -0
  90. prefect/flow_runs.py +379 -7
  91. prefect/flows.py +470 -360
  92. prefect/futures.py +382 -331
  93. prefect/infrastructure/__init__.py +5 -26
  94. prefect/infrastructure/base.py +3 -320
  95. prefect/infrastructure/provisioners/__init__.py +5 -3
  96. prefect/infrastructure/provisioners/cloud_run.py +13 -8
  97. prefect/infrastructure/provisioners/container_instance.py +14 -9
  98. prefect/infrastructure/provisioners/ecs.py +10 -8
  99. prefect/infrastructure/provisioners/modal.py +8 -5
  100. prefect/input/__init__.py +4 -0
  101. prefect/input/actions.py +2 -4
  102. prefect/input/run_input.py +9 -9
  103. prefect/logging/formatters.py +2 -4
  104. prefect/logging/handlers.py +9 -14
  105. prefect/logging/loggers.py +5 -5
  106. prefect/main.py +72 -0
  107. prefect/plugins.py +2 -64
  108. prefect/profiles.toml +16 -2
  109. prefect/records/__init__.py +1 -0
  110. prefect/records/base.py +223 -0
  111. prefect/records/filesystem.py +207 -0
  112. prefect/records/memory.py +178 -0
  113. prefect/records/result_store.py +64 -0
  114. prefect/results.py +577 -504
  115. prefect/runner/runner.py +117 -47
  116. prefect/runner/server.py +32 -34
  117. prefect/runner/storage.py +3 -12
  118. prefect/runner/submit.py +2 -10
  119. prefect/runner/utils.py +2 -2
  120. prefect/runtime/__init__.py +1 -0
  121. prefect/runtime/deployment.py +1 -0
  122. prefect/runtime/flow_run.py +40 -5
  123. prefect/runtime/task_run.py +1 -0
  124. prefect/serializers.py +28 -39
  125. prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
  126. prefect/settings.py +209 -332
  127. prefect/states.py +160 -63
  128. prefect/task_engine.py +1478 -57
  129. prefect/task_runners.py +383 -287
  130. prefect/task_runs.py +240 -0
  131. prefect/task_worker.py +463 -0
  132. prefect/tasks.py +684 -374
  133. prefect/transactions.py +410 -0
  134. prefect/types/__init__.py +72 -86
  135. prefect/types/entrypoint.py +13 -0
  136. prefect/utilities/annotations.py +4 -3
  137. prefect/utilities/asyncutils.py +227 -148
  138. prefect/utilities/callables.py +137 -45
  139. prefect/utilities/collections.py +134 -86
  140. prefect/utilities/dispatch.py +27 -14
  141. prefect/utilities/dockerutils.py +11 -4
  142. prefect/utilities/engine.py +186 -32
  143. prefect/utilities/filesystem.py +4 -5
  144. prefect/utilities/importtools.py +26 -27
  145. prefect/utilities/pydantic.py +128 -38
  146. prefect/utilities/schema_tools/hydration.py +18 -1
  147. prefect/utilities/schema_tools/validation.py +30 -0
  148. prefect/utilities/services.py +35 -9
  149. prefect/utilities/templating.py +12 -2
  150. prefect/utilities/timeout.py +20 -5
  151. prefect/utilities/urls.py +195 -0
  152. prefect/utilities/visualization.py +1 -0
  153. prefect/variables.py +78 -59
  154. prefect/workers/__init__.py +0 -1
  155. prefect/workers/base.py +237 -244
  156. prefect/workers/block.py +5 -226
  157. prefect/workers/cloud.py +6 -0
  158. prefect/workers/process.py +265 -12
  159. prefect/workers/server.py +29 -11
  160. {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/METADATA +28 -24
  161. prefect_client-3.0.0.dist-info/RECORD +201 -0
  162. {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/WHEEL +1 -1
  163. prefect/_internal/pydantic/_base_model.py +0 -51
  164. prefect/_internal/pydantic/_compat.py +0 -82
  165. prefect/_internal/pydantic/_flags.py +0 -20
  166. prefect/_internal/pydantic/_types.py +0 -8
  167. prefect/_internal/pydantic/utilities/config_dict.py +0 -72
  168. prefect/_internal/pydantic/utilities/field_validator.py +0 -150
  169. prefect/_internal/pydantic/utilities/model_construct.py +0 -56
  170. prefect/_internal/pydantic/utilities/model_copy.py +0 -55
  171. prefect/_internal/pydantic/utilities/model_dump.py +0 -136
  172. prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
  173. prefect/_internal/pydantic/utilities/model_fields.py +0 -50
  174. prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
  175. prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
  176. prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
  177. prefect/_internal/pydantic/utilities/model_validate.py +0 -75
  178. prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
  179. prefect/_internal/pydantic/utilities/model_validator.py +0 -87
  180. prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
  181. prefect/_vendor/fastapi/__init__.py +0 -25
  182. prefect/_vendor/fastapi/applications.py +0 -946
  183. prefect/_vendor/fastapi/background.py +0 -3
  184. prefect/_vendor/fastapi/concurrency.py +0 -44
  185. prefect/_vendor/fastapi/datastructures.py +0 -58
  186. prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
  187. prefect/_vendor/fastapi/dependencies/models.py +0 -64
  188. prefect/_vendor/fastapi/dependencies/utils.py +0 -877
  189. prefect/_vendor/fastapi/encoders.py +0 -177
  190. prefect/_vendor/fastapi/exception_handlers.py +0 -40
  191. prefect/_vendor/fastapi/exceptions.py +0 -46
  192. prefect/_vendor/fastapi/logger.py +0 -3
  193. prefect/_vendor/fastapi/middleware/__init__.py +0 -1
  194. prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
  195. prefect/_vendor/fastapi/middleware/cors.py +0 -3
  196. prefect/_vendor/fastapi/middleware/gzip.py +0 -3
  197. prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
  198. prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
  199. prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
  200. prefect/_vendor/fastapi/openapi/__init__.py +0 -0
  201. prefect/_vendor/fastapi/openapi/constants.py +0 -2
  202. prefect/_vendor/fastapi/openapi/docs.py +0 -203
  203. prefect/_vendor/fastapi/openapi/models.py +0 -480
  204. prefect/_vendor/fastapi/openapi/utils.py +0 -485
  205. prefect/_vendor/fastapi/param_functions.py +0 -340
  206. prefect/_vendor/fastapi/params.py +0 -453
  207. prefect/_vendor/fastapi/py.typed +0 -0
  208. prefect/_vendor/fastapi/requests.py +0 -4
  209. prefect/_vendor/fastapi/responses.py +0 -40
  210. prefect/_vendor/fastapi/routing.py +0 -1331
  211. prefect/_vendor/fastapi/security/__init__.py +0 -15
  212. prefect/_vendor/fastapi/security/api_key.py +0 -98
  213. prefect/_vendor/fastapi/security/base.py +0 -6
  214. prefect/_vendor/fastapi/security/http.py +0 -172
  215. prefect/_vendor/fastapi/security/oauth2.py +0 -227
  216. prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
  217. prefect/_vendor/fastapi/security/utils.py +0 -10
  218. prefect/_vendor/fastapi/staticfiles.py +0 -1
  219. prefect/_vendor/fastapi/templating.py +0 -3
  220. prefect/_vendor/fastapi/testclient.py +0 -1
  221. prefect/_vendor/fastapi/types.py +0 -3
  222. prefect/_vendor/fastapi/utils.py +0 -235
  223. prefect/_vendor/fastapi/websockets.py +0 -7
  224. prefect/_vendor/starlette/__init__.py +0 -1
  225. prefect/_vendor/starlette/_compat.py +0 -28
  226. prefect/_vendor/starlette/_exception_handler.py +0 -80
  227. prefect/_vendor/starlette/_utils.py +0 -88
  228. prefect/_vendor/starlette/applications.py +0 -261
  229. prefect/_vendor/starlette/authentication.py +0 -159
  230. prefect/_vendor/starlette/background.py +0 -43
  231. prefect/_vendor/starlette/concurrency.py +0 -59
  232. prefect/_vendor/starlette/config.py +0 -151
  233. prefect/_vendor/starlette/convertors.py +0 -87
  234. prefect/_vendor/starlette/datastructures.py +0 -707
  235. prefect/_vendor/starlette/endpoints.py +0 -130
  236. prefect/_vendor/starlette/exceptions.py +0 -60
  237. prefect/_vendor/starlette/formparsers.py +0 -276
  238. prefect/_vendor/starlette/middleware/__init__.py +0 -17
  239. prefect/_vendor/starlette/middleware/authentication.py +0 -52
  240. prefect/_vendor/starlette/middleware/base.py +0 -220
  241. prefect/_vendor/starlette/middleware/cors.py +0 -176
  242. prefect/_vendor/starlette/middleware/errors.py +0 -265
  243. prefect/_vendor/starlette/middleware/exceptions.py +0 -74
  244. prefect/_vendor/starlette/middleware/gzip.py +0 -113
  245. prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
  246. prefect/_vendor/starlette/middleware/sessions.py +0 -82
  247. prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
  248. prefect/_vendor/starlette/middleware/wsgi.py +0 -147
  249. prefect/_vendor/starlette/py.typed +0 -0
  250. prefect/_vendor/starlette/requests.py +0 -328
  251. prefect/_vendor/starlette/responses.py +0 -347
  252. prefect/_vendor/starlette/routing.py +0 -933
  253. prefect/_vendor/starlette/schemas.py +0 -154
  254. prefect/_vendor/starlette/staticfiles.py +0 -248
  255. prefect/_vendor/starlette/status.py +0 -199
  256. prefect/_vendor/starlette/templating.py +0 -231
  257. prefect/_vendor/starlette/testclient.py +0 -804
  258. prefect/_vendor/starlette/types.py +0 -30
  259. prefect/_vendor/starlette/websockets.py +0 -193
  260. prefect/blocks/kubernetes.py +0 -119
  261. prefect/deprecated/__init__.py +0 -0
  262. prefect/deprecated/data_documents.py +0 -350
  263. prefect/deprecated/packaging/__init__.py +0 -12
  264. prefect/deprecated/packaging/base.py +0 -96
  265. prefect/deprecated/packaging/docker.py +0 -146
  266. prefect/deprecated/packaging/file.py +0 -92
  267. prefect/deprecated/packaging/orion.py +0 -80
  268. prefect/deprecated/packaging/serializers.py +0 -171
  269. prefect/events/instrument.py +0 -135
  270. prefect/infrastructure/container.py +0 -824
  271. prefect/infrastructure/kubernetes.py +0 -920
  272. prefect/infrastructure/process.py +0 -289
  273. prefect/manifests.py +0 -20
  274. prefect/new_flow_engine.py +0 -449
  275. prefect/new_task_engine.py +0 -423
  276. prefect/pydantic/__init__.py +0 -76
  277. prefect/pydantic/main.py +0 -39
  278. prefect/software/__init__.py +0 -2
  279. prefect/software/base.py +0 -50
  280. prefect/software/conda.py +0 -199
  281. prefect/software/pip.py +0 -122
  282. prefect/software/python.py +0 -52
  283. prefect/task_server.py +0 -322
  284. prefect_client-2.20.4.dist-info/RECORD +0 -294
  285. /prefect/{_internal/pydantic/utilities → client/types}/__init__.py +0 -0
  286. /prefect/{_vendor → concurrency/v1}/__init__.py +0 -0
  287. {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
  288. {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/top_level.txt +0 -0
@@ -2,20 +2,13 @@ import datetime
2
2
  from typing import Any, Dict, List, Optional, TypeVar, Union
3
3
  from uuid import UUID
4
4
 
5
- from prefect._internal.compatibility.deprecated import DeprecatedInfraOverridesField
6
- from prefect._internal.pydantic import HAS_PYDANTIC_V2
7
-
8
- if HAS_PYDANTIC_V2:
9
- from pydantic.v1 import Field
10
- else:
11
- from pydantic import Field
12
-
5
+ from pydantic import ConfigDict, Field
6
+ from pydantic_extra_types.pendulum_dt import DateTime
13
7
  from typing_extensions import Literal
14
8
 
15
9
  import prefect.client.schemas.objects as objects
16
10
  from prefect._internal.schemas.bases import ObjectBaseModel, PrefectBaseModel
17
- from prefect._internal.schemas.fields import CreatedBy, DateTimeTZ, UpdatedBy
18
- from prefect.client.schemas.schedules import SCHEDULE_TYPES
11
+ from prefect._internal.schemas.fields import CreatedBy, UpdatedBy
19
12
  from prefect.utilities.collections import AutoEnum
20
13
  from prefect.utilities.names import generate_slug
21
14
 
@@ -120,10 +113,10 @@ class HistoryResponseState(PrefectBaseModel):
120
113
  class HistoryResponse(PrefectBaseModel):
121
114
  """Represents a history of aggregation states over an interval"""
122
115
 
123
- interval_start: DateTimeTZ = Field(
116
+ interval_start: DateTime = Field(
124
117
  default=..., description="The start date of the interval."
125
118
  )
126
- interval_end: DateTimeTZ = Field(
119
+ interval_end: DateTime = Field(
127
120
  default=..., description="The end date of the interval."
128
121
  )
129
122
  states: List[HistoryResponseState] = Field(
@@ -147,8 +140,7 @@ class OrchestrationResult(PrefectBaseModel):
147
140
 
148
141
 
149
142
  class WorkerFlowRunResponse(PrefectBaseModel):
150
- class Config:
151
- arbitrary_types_allowed = True
143
+ model_config = ConfigDict(arbitrary_types_allowed=True)
152
144
 
153
145
  work_pool_id: UUID
154
146
  work_queue_id: UUID
@@ -219,18 +211,18 @@ class FlowRunResponse(ObjectBaseModel):
219
211
  run_count: int = Field(
220
212
  default=0, description="The number of times the flow run was executed."
221
213
  )
222
- expected_start_time: Optional[DateTimeTZ] = Field(
214
+ expected_start_time: Optional[DateTime] = Field(
223
215
  default=None,
224
216
  description="The flow run's expected start time.",
225
217
  )
226
- next_scheduled_start_time: Optional[DateTimeTZ] = Field(
218
+ next_scheduled_start_time: Optional[DateTime] = Field(
227
219
  default=None,
228
220
  description="The next time the flow run is scheduled to start.",
229
221
  )
230
- start_time: Optional[DateTimeTZ] = Field(
222
+ start_time: Optional[DateTime] = Field(
231
223
  default=None, description="The actual start time."
232
224
  )
233
- end_time: Optional[DateTimeTZ] = Field(
225
+ end_time: Optional[DateTime] = Field(
234
226
  default=None, description="The actual end time."
235
227
  )
236
228
  total_run_time: datetime.timedelta = Field(
@@ -279,7 +271,7 @@ class FlowRunResponse(ObjectBaseModel):
279
271
  state: Optional[objects.State] = Field(
280
272
  default=None,
281
273
  description="The state of the flow run.",
282
- examples=[objects.State(type=objects.StateType.COMPLETED)],
274
+ examples=["objects.State(type=objects.StateType.COMPLETED)"],
283
275
  )
284
276
  job_variables: Optional[dict] = Field(
285
277
  default=None, description="Job variables for the flow run."
@@ -304,13 +296,13 @@ class FlowRunResponse(ObjectBaseModel):
304
296
  """
305
297
  if isinstance(other, objects.FlowRun):
306
298
  exclude_fields = {"estimated_run_time", "estimated_start_time_delta"}
307
- return self.dict(exclude=exclude_fields) == other.dict(
299
+ return self.model_dump(exclude=exclude_fields) == other.model_dump(
308
300
  exclude=exclude_fields
309
301
  )
310
302
  return super().__eq__(other)
311
303
 
312
304
 
313
- class DeploymentResponse(DeprecatedInfraOverridesField, ObjectBaseModel):
305
+ class DeploymentResponse(ObjectBaseModel):
314
306
  name: str = Field(default=..., description="The name of the deployment.")
315
307
  version: Optional[str] = Field(
316
308
  default=None, description="An optional version for the deployment."
@@ -321,11 +313,8 @@ class DeploymentResponse(DeprecatedInfraOverridesField, ObjectBaseModel):
321
313
  flow_id: UUID = Field(
322
314
  default=..., description="The flow id associated with the deployment."
323
315
  )
324
- schedule: Optional[SCHEDULE_TYPES] = Field(
325
- default=None, description="A schedule for the deployment."
326
- )
327
- is_schedule_active: bool = Field(
328
- default=True, description="Whether or not the deployment schedule is active."
316
+ concurrency_limit: Optional[int] = Field(
317
+ default=None, description="The concurrency limit for the deployment."
329
318
  )
330
319
  paused: bool = Field(
331
320
  default=False, description="Whether or not the deployment is paused."
@@ -357,7 +346,7 @@ class DeploymentResponse(DeprecatedInfraOverridesField, ObjectBaseModel):
357
346
  " be scheduled."
358
347
  ),
359
348
  )
360
- last_polled: Optional[DateTimeTZ] = Field(
349
+ last_polled: Optional[DateTime] = Field(
361
350
  default=None,
362
351
  description="The last time the deployment was polled for status updates.",
363
352
  )
@@ -378,12 +367,6 @@ class DeploymentResponse(DeprecatedInfraOverridesField, ObjectBaseModel):
378
367
  "The path to the entrypoint for the workflow, relative to the `path`."
379
368
  ),
380
369
  )
381
- manifest_path: Optional[str] = Field(
382
- default=None,
383
- description=(
384
- "The path to the flow's manifest file, relative to the chosen storage."
385
- ),
386
- )
387
370
  storage_document_id: Optional[UUID] = Field(
388
371
  default=None,
389
372
  description="The block document defining storage used for this flow.",
@@ -407,7 +390,7 @@ class DeploymentResponse(DeprecatedInfraOverridesField, ObjectBaseModel):
407
390
  ),
408
391
  )
409
392
  enforce_parameter_schema: bool = Field(
410
- default=False,
393
+ default=True,
411
394
  description=(
412
395
  "Whether or not the deployment should enforce the parameter schema."
413
396
  ),
@@ -423,8 +406,7 @@ class DeploymentResponse(DeprecatedInfraOverridesField, ObjectBaseModel):
423
406
 
424
407
 
425
408
  class MinimalConcurrencyLimitResponse(PrefectBaseModel):
426
- class Config:
427
- extra = "ignore" # 2024/4/1
409
+ model_config = ConfigDict(extra="ignore")
428
410
 
429
411
  id: UUID
430
412
  name: str
@@ -3,28 +3,22 @@ Schedule schemas
3
3
  """
4
4
 
5
5
  import datetime
6
- from typing import Optional, Union
6
+ from typing import Annotated, Any, Optional, Union
7
7
 
8
8
  import dateutil
9
9
  import dateutil.rrule
10
10
  import pendulum
11
+ from pydantic import AfterValidator, ConfigDict, Field, field_validator, model_validator
12
+ from pydantic_extra_types.pendulum_dt import DateTime
13
+ from typing_extensions import TypeAlias, TypeGuard
11
14
 
12
- from prefect._internal.pydantic import HAS_PYDANTIC_V2
13
15
  from prefect._internal.schemas.bases import PrefectBaseModel
14
- from prefect._internal.schemas.fields import DateTimeTZ
15
16
  from prefect._internal.schemas.validators import (
16
17
  default_anchor_date,
17
18
  default_timezone,
18
19
  validate_cron_string,
19
20
  validate_rrule_string,
20
- validate_rrule_timezone,
21
21
  )
22
- from prefect.types import PositiveDuration
23
-
24
- if HAS_PYDANTIC_V2:
25
- from pydantic.v1 import Field, validator
26
- else:
27
- from pydantic import Field, validator
28
22
 
29
23
  MAX_ITERATIONS = 1000
30
24
  # approx. 1 years worth of RDATEs + buffer
@@ -55,26 +49,24 @@ class IntervalSchedule(PrefectBaseModel):
55
49
 
56
50
  Args:
57
51
  interval (datetime.timedelta): an interval to schedule on
58
- anchor_date (DateTimeTZ, optional): an anchor date to schedule increments against;
52
+ anchor_date (DateTime, optional): an anchor date to schedule increments against;
59
53
  if not provided, the current timestamp will be used
60
54
  timezone (str, optional): a valid timezone string
61
55
  """
62
56
 
63
- class Config:
64
- extra = "forbid"
65
- exclude_none = True
57
+ model_config = ConfigDict(extra="forbid", exclude_none=True)
66
58
 
67
- interval: PositiveDuration
68
- anchor_date: Optional[DateTimeTZ] = None
59
+ interval: datetime.timedelta = Field(gt=datetime.timedelta(0))
60
+ anchor_date: Annotated[DateTime, AfterValidator(default_anchor_date)] = Field(
61
+ default_factory=lambda: pendulum.now("UTC"),
62
+ examples=["2020-01-01T00:00:00Z"],
63
+ )
69
64
  timezone: Optional[str] = Field(default=None, examples=["America/New_York"])
70
65
 
71
- @validator("anchor_date", always=True)
72
- def validate_anchor_date(cls, v):
73
- return default_anchor_date(v)
74
-
75
- @validator("timezone", always=True)
76
- def validate_default_timezone(cls, v, values):
77
- return default_timezone(v, values=values)
66
+ @model_validator(mode="after")
67
+ def validate_timezone(self):
68
+ self.timezone = default_timezone(self.timezone, self.model_dump())
69
+ return self
78
70
 
79
71
 
80
72
  class CronSchedule(PrefectBaseModel):
@@ -102,8 +94,7 @@ class CronSchedule(PrefectBaseModel):
102
94
 
103
95
  """
104
96
 
105
- class Config:
106
- extra = "forbid"
97
+ model_config = ConfigDict(extra="forbid")
107
98
 
108
99
  cron: str = Field(default=..., examples=["0 0 * * *"])
109
100
  timezone: Optional[str] = Field(default=None, examples=["America/New_York"])
@@ -114,11 +105,13 @@ class CronSchedule(PrefectBaseModel):
114
105
  ),
115
106
  )
116
107
 
117
- @validator("timezone")
108
+ @field_validator("timezone")
109
+ @classmethod
118
110
  def valid_timezone(cls, v):
119
111
  return default_timezone(v)
120
112
 
121
- @validator("cron")
113
+ @field_validator("cron")
114
+ @classmethod
122
115
  def valid_cron_string(cls, v):
123
116
  return validate_cron_string(v)
124
117
 
@@ -146,13 +139,15 @@ class RRuleSchedule(PrefectBaseModel):
146
139
  timezone (str, optional): a valid timezone string
147
140
  """
148
141
 
149
- class Config:
150
- extra = "forbid"
142
+ model_config = ConfigDict(extra="forbid")
151
143
 
152
144
  rrule: str
153
- timezone: Optional[str] = Field(default=None, examples=["America/New_York"])
145
+ timezone: Optional[str] = Field(
146
+ default="UTC", examples=["America/New_York"], validate_default=True
147
+ )
154
148
 
155
- @validator("rrule")
149
+ @field_validator("rrule")
150
+ @classmethod
156
151
  def validate_rrule_str(cls, v):
157
152
  return validate_rrule_string(v)
158
153
 
@@ -259,17 +254,39 @@ class RRuleSchedule(PrefectBaseModel):
259
254
 
260
255
  return rrule
261
256
 
262
- @validator("timezone", always=True)
257
+ @field_validator("timezone")
263
258
  def valid_timezone(cls, v):
264
- return validate_rrule_timezone(v)
259
+ """
260
+ Validate that the provided timezone is a valid IANA timezone.
261
+
262
+ Unfortunately this list is slightly different from the list of valid
263
+ timezones in pendulum that we use for cron and interval timezone validation.
264
+ """
265
+ from prefect._internal.pytz import HAS_PYTZ
266
+
267
+ if HAS_PYTZ:
268
+ import pytz
269
+ else:
270
+ from prefect._internal import pytz
271
+
272
+ if v and v not in pytz.all_timezones_set:
273
+ raise ValueError(f'Invalid timezone: "{v}"')
274
+ elif v is None:
275
+ return "UTC"
276
+ return v
265
277
 
266
278
 
267
279
  class NoSchedule(PrefectBaseModel):
268
- class Config:
269
- extra = "forbid"
280
+ model_config = ConfigDict(extra="forbid")
281
+
282
+
283
+ SCHEDULE_TYPES: TypeAlias = Union[
284
+ IntervalSchedule, CronSchedule, RRuleSchedule, NoSchedule
285
+ ]
270
286
 
271
287
 
272
- SCHEDULE_TYPES = Union[IntervalSchedule, CronSchedule, RRuleSchedule, NoSchedule]
288
+ def is_schedule_type(obj: Any) -> TypeGuard[SCHEDULE_TYPES]:
289
+ return isinstance(obj, (IntervalSchedule, CronSchedule, RRuleSchedule, NoSchedule))
273
290
 
274
291
 
275
292
  def construct_schedule(
@@ -308,6 +325,8 @@ def construct_schedule(
308
325
  if interval:
309
326
  if isinstance(interval, (int, float)):
310
327
  interval = datetime.timedelta(seconds=interval)
328
+ if not anchor_date:
329
+ anchor_date = DateTime.now()
311
330
  schedule = IntervalSchedule(
312
331
  interval=interval, anchor_date=anchor_date, timezone=timezone
313
332
  )
@@ -59,6 +59,8 @@ class DeploymentSort(AutoEnum):
59
59
  UPDATED_DESC = AutoEnum.auto()
60
60
  NAME_ASC = AutoEnum.auto()
61
61
  NAME_DESC = AutoEnum.auto()
62
+ CONCURRENCY_LIMIT_ASC = AutoEnum.auto()
63
+ CONCURRENCY_LIMIT_DESC = AutoEnum.auto()
62
64
 
63
65
 
64
66
  class ArtifactSort(AutoEnum):
@@ -1,15 +1,15 @@
1
1
  import asyncio
2
- from typing import Any, Dict, Generic, List, Optional, Type, TypeVar
2
+ from typing import Any, Dict, Generic, Iterable, Optional, Type, TypeVar
3
3
 
4
4
  import orjson
5
5
  import websockets
6
6
  import websockets.exceptions
7
- from prefect._vendor.starlette.status import WS_1008_POLICY_VIOLATION
7
+ from starlette.status import WS_1008_POLICY_VIOLATION
8
8
  from typing_extensions import Self
9
9
 
10
10
  from prefect._internal.schemas.bases import IDBaseModel
11
11
  from prefect.logging import get_logger
12
- from prefect.settings import PREFECT_API_KEY, PREFECT_API_URL
12
+ from prefect.settings import PREFECT_API_KEY
13
13
 
14
14
  logger = get_logger(__name__)
15
15
 
@@ -21,15 +21,16 @@ class Subscription(Generic[S]):
21
21
  self,
22
22
  model: Type[S],
23
23
  path: str,
24
- keys: List[str],
24
+ keys: Iterable[str],
25
25
  client_id: Optional[str] = None,
26
+ base_url: Optional[str] = None,
26
27
  ):
27
28
  self.model = model
28
29
  self.client_id = client_id
29
- base_url = PREFECT_API_URL.value().replace("http", "ws", 1)
30
+ base_url = base_url.replace("http", "ws", 1)
30
31
  self.subscription_url = f"{base_url}{path}"
31
32
 
32
- self.keys = keys
33
+ self.keys = list(keys)
33
34
 
34
35
  self._connect = websockets.connect(
35
36
  self.subscription_url,
@@ -48,7 +49,7 @@ class Subscription(Generic[S]):
48
49
 
49
50
  await self._websocket.send(orjson.dumps({"type": "ack"}).decode())
50
51
 
51
- return self.model.parse_raw(message)
52
+ return self.model.model_validate_json(message)
52
53
  except (
53
54
  ConnectionRefusedError,
54
55
  websockets.exceptions.ConnectionClosedError,
@@ -0,0 +1,11 @@
1
+ from typing import TYPE_CHECKING, Any, Sequence, Union
2
+
3
+ from typing_extensions import TypeAlias
4
+
5
+ if TYPE_CHECKING:
6
+ from prefect.client.schemas.actions import DeploymentScheduleCreate
7
+ from prefect.client.schemas.schedules import SCHEDULE_TYPES
8
+
9
+ FlexibleScheduleList: TypeAlias = Sequence[
10
+ Union[DeploymentScheduleCreate, dict[str, Any], "SCHEDULE_TYPES"]
11
+ ]
@@ -42,12 +42,15 @@ def get_or_create_client(
42
42
  if client is not None:
43
43
  return client, True
44
44
  from prefect._internal.concurrency.event_loop import get_running_loop
45
- from prefect.context import FlowRunContext, TaskRunContext
45
+ from prefect.context import AsyncClientContext, FlowRunContext, TaskRunContext
46
46
 
47
+ async_client_context = AsyncClientContext.get()
47
48
  flow_run_context = FlowRunContext.get()
48
49
  task_run_context = TaskRunContext.get()
49
50
 
50
- if (
51
+ if async_client_context and async_client_context.client._loop == get_running_loop():
52
+ return async_client_context.client, True
53
+ elif (
51
54
  flow_run_context
52
55
  and getattr(flow_run_context.client, "_loop", None) == get_running_loop()
53
56
  ):
@@ -75,10 +78,10 @@ def client_injector(
75
78
 
76
79
 
77
80
  def inject_client(
78
- fn: Callable[P, Coroutine[Any, Any, Any]],
79
- ) -> Callable[P, Coroutine[Any, Any, Any]]:
81
+ fn: Callable[P, Coroutine[Any, Any, R]],
82
+ ) -> Callable[P, Coroutine[Any, Any, R]]:
80
83
  """
81
- Simple helper to provide a context managed client to a asynchronous function.
84
+ Simple helper to provide a context managed client to an asynchronous function.
82
85
 
83
86
  The decorated function _must_ take a `client` kwarg and if a client is passed when
84
87
  called it will be used instead of creating a new one, but it will not be context
@@ -86,7 +89,7 @@ def inject_client(
86
89
  """
87
90
 
88
91
  @wraps(fn)
89
- async def with_injected_client(*args: P.args, **kwargs: P.kwargs) -> Any:
92
+ async def with_injected_client(*args: P.args, **kwargs: P.kwargs) -> R:
90
93
  client = cast(Optional["PrefectClient"], kwargs.pop("client", None))
91
94
  client, inferred = get_or_create_client(client)
92
95
  if not inferred:
@@ -1,7 +1,8 @@
1
1
  import asyncio
2
2
  from contextlib import asynccontextmanager
3
- from typing import List, Literal, Optional, Union, cast
3
+ from typing import AsyncGenerator, List, Literal, Optional, Union, cast
4
4
 
5
+ import anyio
5
6
  import httpx
6
7
  import pendulum
7
8
 
@@ -11,9 +12,10 @@ except ImportError:
11
12
  # pendulum < 3
12
13
  from pendulum.period import Period as Interval # type: ignore
13
14
 
14
- from prefect import get_client
15
+ from prefect.client.orchestration import get_client
15
16
  from prefect.client.schemas.responses import MinimalConcurrencyLimitResponse
16
17
 
18
+ from .context import ConcurrencyContext
17
19
  from .events import (
18
20
  _emit_concurrency_acquisition_events,
19
21
  _emit_concurrency_release_events,
@@ -34,7 +36,9 @@ async def concurrency(
34
36
  names: Union[str, List[str]],
35
37
  occupy: int = 1,
36
38
  timeout_seconds: Optional[float] = None,
37
- ):
39
+ create_if_missing: bool = True,
40
+ max_retries: Optional[int] = None,
41
+ ) -> AsyncGenerator[None, None]:
38
42
  """A context manager that acquires and releases concurrency slots from the
39
43
  given concurrency limits.
40
44
 
@@ -43,6 +47,8 @@ async def concurrency(
43
47
  occupy: The number of slots to acquire and hold from each limit.
44
48
  timeout_seconds: The number of seconds to wait for the slots to be acquired before
45
49
  raising a `TimeoutError`. A timeout of `None` will wait indefinitely.
50
+ create_if_missing: Whether to create the concurrency limits if they do not exist.
51
+ max_retries: The maximum number of retries to acquire the concurrency slots.
46
52
 
47
53
  Raises:
48
54
  TimeoutError: If the slots are not acquired within the given timeout.
@@ -60,9 +66,18 @@ async def concurrency(
60
66
  await resource_heavy()
61
67
  ```
62
68
  """
69
+ if not names:
70
+ yield
71
+ return
72
+
63
73
  names = names if isinstance(names, list) else [names]
74
+
64
75
  limits = await _acquire_concurrency_slots(
65
- names, occupy, timeout_seconds=timeout_seconds
76
+ names,
77
+ occupy,
78
+ timeout_seconds=timeout_seconds,
79
+ create_if_missing=create_if_missing,
80
+ max_retries=max_retries,
66
81
  )
67
82
  acquisition_time = pendulum.now("UTC")
68
83
  emitted_events = _emit_concurrency_acquisition_events(limits, occupy)
@@ -71,13 +86,28 @@ async def concurrency(
71
86
  yield
72
87
  finally:
73
88
  occupancy_period = cast(Interval, (pendulum.now("UTC") - acquisition_time))
74
- await _release_concurrency_slots(
75
- names, occupy, occupancy_period.total_seconds()
76
- )
89
+ try:
90
+ await _release_concurrency_slots(
91
+ names, occupy, occupancy_period.total_seconds()
92
+ )
93
+ except anyio.get_cancelled_exc_class():
94
+ # The task was cancelled before it could release the slots. Add the
95
+ # slots to the cleanup list so they can be released when the
96
+ # concurrency context is exited.
97
+ if ctx := ConcurrencyContext.get():
98
+ ctx.cleanup_slots.append(
99
+ (names, occupy, occupancy_period.total_seconds())
100
+ )
101
+
77
102
  _emit_concurrency_release_events(limits, occupy, emitted_events)
78
103
 
79
104
 
80
- async def rate_limit(names: Union[str, List[str]], occupy: int = 1):
105
+ async def rate_limit(
106
+ names: Union[str, List[str]],
107
+ occupy: int = 1,
108
+ timeout_seconds: Optional[float] = None,
109
+ create_if_missing: Optional[bool] = True,
110
+ ) -> None:
81
111
  """Block execution until an `occupy` number of slots of the concurrency
82
112
  limits given in `names` are acquired. Requires that all given concurrency
83
113
  limits have a slot decay.
@@ -85,9 +115,22 @@ async def rate_limit(names: Union[str, List[str]], occupy: int = 1):
85
115
  Args:
86
116
  names: The names of the concurrency limits to acquire slots from.
87
117
  occupy: The number of slots to acquire and hold from each limit.
118
+ timeout_seconds: The number of seconds to wait for the slots to be acquired before
119
+ raising a `TimeoutError`. A timeout of `None` will wait indefinitely.
120
+ create_if_missing: Whether to create the concurrency limits if they do not exist.
88
121
  """
122
+ if not names:
123
+ return
124
+
89
125
  names = names if isinstance(names, list) else [names]
90
- limits = await _acquire_concurrency_slots(names, occupy, mode="rate_limit")
126
+
127
+ limits = await _acquire_concurrency_slots(
128
+ names,
129
+ occupy,
130
+ mode="rate_limit",
131
+ timeout_seconds=timeout_seconds,
132
+ create_if_missing=create_if_missing,
133
+ )
91
134
  _emit_concurrency_acquisition_events(limits, occupy)
92
135
 
93
136
 
@@ -96,9 +139,13 @@ async def _acquire_concurrency_slots(
96
139
  slots: int,
97
140
  mode: Union[Literal["concurrency"], Literal["rate_limit"]] = "concurrency",
98
141
  timeout_seconds: Optional[float] = None,
142
+ create_if_missing: Optional[bool] = True,
143
+ max_retries: Optional[int] = None,
99
144
  ) -> List[MinimalConcurrencyLimitResponse]:
100
145
  service = ConcurrencySlotAcquisitionService.instance(frozenset(names))
101
- future = service.send((slots, mode, timeout_seconds))
146
+ future = service.send(
147
+ (slots, mode, timeout_seconds, create_if_missing, max_retries)
148
+ )
102
149
  response_or_exception = await asyncio.wrap_future(future)
103
150
 
104
151
  if isinstance(response_or_exception, Exception):
@@ -127,4 +174,6 @@ async def _release_concurrency_slots(
127
174
  def _response_to_minimal_concurrency_limit_response(
128
175
  response: httpx.Response,
129
176
  ) -> List[MinimalConcurrencyLimitResponse]:
130
- return [MinimalConcurrencyLimitResponse.parse_obj(obj_) for obj_ in response.json()]
177
+ return [
178
+ MinimalConcurrencyLimitResponse.model_validate(obj_) for obj_ in response.json()
179
+ ]
@@ -0,0 +1,24 @@
1
+ from contextvars import ContextVar
2
+ from typing import List, Tuple
3
+
4
+ from prefect.client.orchestration import get_client
5
+ from prefect.context import ContextModel, Field
6
+
7
+
8
+ class ConcurrencyContext(ContextModel):
9
+ __var__: ContextVar = ContextVar("concurrency")
10
+
11
+ # Track the slots that have been acquired but were not able to be released
12
+ # due to cancellation or some other error. These slots are released when
13
+ # the context manager exits.
14
+ cleanup_slots: List[Tuple[List[str], int, float]] = Field(default_factory=list)
15
+
16
+ def __exit__(self, *exc_info):
17
+ if self.cleanup_slots:
18
+ with get_client(sync_client=True) as client:
19
+ for names, occupy, occupancy_seconds in self.cleanup_slots:
20
+ client.release_concurrency_slots(
21
+ names=names, slots=occupy, occupancy_seconds=occupancy_seconds
22
+ )
23
+
24
+ return super().__exit__(*exc_info)
@@ -20,7 +20,7 @@ def _emit_concurrency_event(
20
20
  }
21
21
 
22
22
  related = [
23
- RelatedResource.parse_obj(
23
+ RelatedResource.model_validate(
24
24
  {
25
25
  "prefect.resource.id": f"prefect.concurrency-limit.{limit.id}",
26
26
  "prefect.resource.role": "concurrency-limit",
@@ -54,6 +54,6 @@ def _emit_concurrency_release_events(
54
54
  limits: List[MinimalConcurrencyLimitResponse],
55
55
  occupy: int,
56
56
  events: Dict[UUID, Optional[Event]],
57
- ):
57
+ ) -> None:
58
58
  for limit in limits:
59
59
  _emit_concurrency_event("released", limit, limits, occupy, events[limit.id])