prefect-client 2.20.2__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 +423 -164
  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 +667 -440
  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 -2466
  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 +124 -51
  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 +138 -48
  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.2.dist-info → prefect_client-3.0.0.dist-info}/METADATA +30 -26
  161. prefect_client-3.0.0.dist-info/RECORD +201 -0
  162. {prefect_client-2.20.2.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.2.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.2.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
  288. {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/top_level.txt +0 -0
prefect/events/actions.py CHANGED
@@ -2,14 +2,8 @@ import abc
2
2
  from typing import Any, Dict, Optional, Union
3
3
  from uuid import UUID
4
4
 
5
- from typing_extensions import Literal, TypeAlias
6
-
7
- from prefect._internal.pydantic import HAS_PYDANTIC_V2
8
-
9
- if HAS_PYDANTIC_V2:
10
- from pydantic.v1 import Field, root_validator
11
- else:
12
- from pydantic import Field, root_validator # type: ignore
5
+ from pydantic import Field, model_validator
6
+ from typing_extensions import Literal, Self, TypeAlias
13
7
 
14
8
  from prefect._internal.schemas.bases import PrefectBaseModel
15
9
  from prefect.client.schemas.objects import StateType
@@ -49,16 +43,16 @@ class DeploymentAction(Action):
49
43
  None, description="The identifier of the deployment"
50
44
  )
51
45
 
52
- @root_validator
53
- def selected_deployment_requires_id(cls, values):
54
- wants_selected_deployment = values.get("source") == "selected"
55
- has_deployment_id = bool(values.get("deployment_id"))
46
+ @model_validator(mode="after")
47
+ def selected_deployment_requires_id(self):
48
+ wants_selected_deployment = self.source == "selected"
49
+ has_deployment_id = bool(self.deployment_id)
56
50
  if wants_selected_deployment != has_deployment_id:
57
51
  raise ValueError(
58
52
  "deployment_id is "
59
53
  + ("not allowed" if has_deployment_id else "required")
60
54
  )
61
- return values
55
+ return self
62
56
 
63
57
 
64
58
  class RunDeployment(DeploymentAction):
@@ -199,16 +193,16 @@ class WorkQueueAction(Action):
199
193
  None, description="The identifier of the work queue to pause"
200
194
  )
201
195
 
202
- @root_validator
203
- def selected_work_queue_requires_id(cls, values):
204
- wants_selected_work_queue = values.get("source") == "selected"
205
- has_work_queue_id = bool(values.get("work_queue_id"))
196
+ @model_validator(mode="after")
197
+ def selected_work_queue_requires_id(self) -> Self:
198
+ wants_selected_work_queue = self.source == "selected"
199
+ has_work_queue_id = bool(self.work_queue_id)
206
200
  if wants_selected_work_queue != has_work_queue_id:
207
201
  raise ValueError(
208
202
  "work_queue_id is "
209
203
  + ("not allowed" if has_work_queue_id else "required")
210
204
  )
211
- return values
205
+ return self
212
206
 
213
207
 
214
208
  class PauseWorkQueue(WorkQueueAction):
@@ -241,16 +235,16 @@ class AutomationAction(Action):
241
235
  None, description="The identifier of the automation to act on"
242
236
  )
243
237
 
244
- @root_validator
245
- def selected_automation_requires_id(cls, values):
246
- wants_selected_automation = values.get("source") == "selected"
247
- has_automation_id = bool(values.get("automation_id"))
238
+ @model_validator(mode="after")
239
+ def selected_automation_requires_id(self) -> Self:
240
+ wants_selected_automation = self.source == "selected"
241
+ has_automation_id = bool(self.automation_id)
248
242
  if wants_selected_automation != has_automation_id:
249
243
  raise ValueError(
250
244
  "automation_id is "
251
245
  + ("not allowed" if has_automation_id else "required")
252
246
  )
253
- return values
247
+ return self
254
248
 
255
249
 
256
250
  class PauseAutomation(AutomationAction):
@@ -3,26 +3,27 @@ Command line interface for working with automations.
3
3
  """
4
4
 
5
5
  import functools
6
- from typing import Optional
6
+ from typing import Optional, Type
7
7
  from uuid import UUID
8
8
 
9
9
  import orjson
10
10
  import typer
11
11
  import yaml as pyyaml
12
+ from pydantic import BaseModel
12
13
  from rich.pretty import Pretty
13
14
  from rich.table import Table
14
15
  from rich.text import Text
15
16
 
16
17
  from prefect.cli._types import PrefectTyper
17
18
  from prefect.cli._utilities import exit_with_error, exit_with_success
18
- from prefect.cli.root import app
19
+ from prefect.cli.root import app, is_interactive
19
20
  from prefect.client.orchestration import get_client
20
21
  from prefect.events.schemas.automations import Automation
21
22
  from prefect.exceptions import PrefectHTTPStatusError
22
23
 
23
24
  automations_app = PrefectTyper(
24
25
  name="automation",
25
- help="Commands for managing automations.",
26
+ help="Manage automations.",
26
27
  )
27
28
  app.add_typer(automations_app, aliases=["automations"])
28
29
 
@@ -148,10 +149,22 @@ async def inspect(
148
149
  exit_with_error(f"Automation with id {id!r} not found.")
149
150
 
150
151
  if yaml or json:
152
+
153
+ def no_really_json(obj: Type[BaseModel]):
154
+ # Working around a weird bug where pydantic isn't rendering enums as strings
155
+ #
156
+ # automation.trigger.model_dump(mode="json")
157
+ # {..., 'posture': 'Reactive', ...}
158
+ #
159
+ # automation.model_dump(mode="json")
160
+ # {..., 'posture': Posture.Reactive, ...}
161
+ return orjson.loads(obj.model_dump_json())
162
+
151
163
  if isinstance(automation, list):
152
- automation = [a.dict(json_compatible=True) for a in automation]
164
+ automation = [no_really_json(a) for a in automation]
153
165
  elif isinstance(automation, Automation):
154
- automation = automation.dict(json_compatible=True)
166
+ automation = no_really_json(automation)
167
+
155
168
  if yaml:
156
169
  app.console.print(pyyaml.dump(automation, sort_keys=False))
157
170
  elif json:
@@ -297,7 +310,7 @@ async def delete(
297
310
  automation = await client.read_automation(id)
298
311
  if not automation:
299
312
  exit_with_error(f"Automation with id {id!r} not found.")
300
- if not typer.confirm(
313
+ if is_interactive() and not typer.confirm(
301
314
  (f"Are you sure you want to delete automation with id {id!r}?"),
302
315
  default=False,
303
316
  ):
@@ -315,7 +328,7 @@ async def delete(
315
328
  exit_with_error(
316
329
  f"Multiple automations found with name {name!r}. Please specify an id with the `--id` flag instead."
317
330
  )
318
- if not typer.confirm(
331
+ if is_interactive() and not typer.confirm(
319
332
  (f"Are you sure you want to delete automation with name {name!r}?"),
320
333
  default=False,
321
334
  ):
prefect/events/clients.py CHANGED
@@ -15,10 +15,10 @@ from typing import (
15
15
  )
16
16
  from uuid import UUID
17
17
 
18
- import httpx
19
18
  import orjson
20
19
  import pendulum
21
20
  from cachetools import TTLCache
21
+ from prometheus_client import Counter
22
22
  from typing_extensions import Self
23
23
  from websockets import Subprotocol
24
24
  from websockets.client import WebSocketClientProtocol, connect
@@ -28,25 +28,48 @@ from websockets.exceptions import (
28
28
  ConnectionClosedOK,
29
29
  )
30
30
 
31
- from prefect.client.base import PrefectHttpxAsyncClient
32
31
  from prefect.events import Event
33
32
  from prefect.logging import get_logger
34
33
  from prefect.settings import (
35
34
  PREFECT_API_KEY,
36
35
  PREFECT_API_URL,
37
36
  PREFECT_CLOUD_API_URL,
38
- PREFECT_EXPERIMENTAL_EVENTS,
37
+ PREFECT_SERVER_ALLOW_EPHEMERAL_MODE,
39
38
  )
40
39
 
41
40
  if TYPE_CHECKING:
42
41
  from prefect.events.filters import EventFilter
43
42
 
43
+ EVENTS_EMITTED = Counter(
44
+ "prefect_events_emitted",
45
+ "The number of events emitted by Prefect event clients",
46
+ labelnames=["client"],
47
+ )
48
+ EVENTS_OBSERVED = Counter(
49
+ "prefect_events_observed",
50
+ "The number of events observed by Prefect event subscribers",
51
+ labelnames=["client"],
52
+ )
53
+ EVENT_WEBSOCKET_CONNECTIONS = Counter(
54
+ "prefect_event_websocket_connections",
55
+ (
56
+ "The number of times Prefect event clients have connected to an event stream, "
57
+ "broken down by direction (in/out) and connection (initial/reconnect)"
58
+ ),
59
+ labelnames=["client", "direction", "connection"],
60
+ )
61
+ EVENT_WEBSOCKET_CHECKPOINTS = Counter(
62
+ "prefect_event_websocket_checkpoints",
63
+ "The number of checkpoints performed by Prefect event clients",
64
+ labelnames=["client"],
65
+ )
66
+
44
67
  logger = get_logger(__name__)
45
68
 
46
69
 
47
70
  def get_events_client(
48
71
  reconnection_attempts: int = 10,
49
- checkpoint_every: int = 20,
72
+ checkpoint_every: int = 700,
50
73
  ) -> "EventsClient":
51
74
  api_url = PREFECT_API_URL.value()
52
75
  if isinstance(api_url, str) and api_url.startswith(PREFECT_CLOUD_API_URL.value()):
@@ -54,20 +77,25 @@ def get_events_client(
54
77
  reconnection_attempts=reconnection_attempts,
55
78
  checkpoint_every=checkpoint_every,
56
79
  )
57
- elif PREFECT_EXPERIMENTAL_EVENTS:
58
- if PREFECT_API_URL:
59
- return PrefectEventsClient(
60
- reconnection_attempts=reconnection_attempts,
61
- checkpoint_every=checkpoint_every,
62
- )
63
- else:
64
- return PrefectEphemeralEventsClient()
80
+ elif api_url:
81
+ return PrefectEventsClient(
82
+ reconnection_attempts=reconnection_attempts,
83
+ checkpoint_every=checkpoint_every,
84
+ )
85
+ elif PREFECT_SERVER_ALLOW_EPHEMERAL_MODE:
86
+ from prefect.server.api.server import SubprocessASGIServer
65
87
 
66
- raise RuntimeError(
67
- "The current server and client configuration does not support "
68
- "events. Enable experimental events support with the "
69
- "PREFECT_EXPERIMENTAL_EVENTS setting."
70
- )
88
+ server = SubprocessASGIServer()
89
+ server.start()
90
+ return PrefectEventsClient(
91
+ api_url=server.api_url,
92
+ reconnection_attempts=reconnection_attempts,
93
+ checkpoint_every=checkpoint_every,
94
+ )
95
+ else:
96
+ raise ValueError(
97
+ "No Prefect API URL provided. Please set PREFECT_API_URL to the address of a running Prefect server."
98
+ )
71
99
 
72
100
 
73
101
  def get_events_subscriber(
@@ -75,25 +103,38 @@ def get_events_subscriber(
75
103
  reconnection_attempts: int = 10,
76
104
  ) -> "PrefectEventSubscriber":
77
105
  api_url = PREFECT_API_URL.value()
106
+
78
107
  if isinstance(api_url, str) and api_url.startswith(PREFECT_CLOUD_API_URL.value()):
79
108
  return PrefectCloudEventSubscriber(
80
109
  filter=filter, reconnection_attempts=reconnection_attempts
81
110
  )
82
- elif PREFECT_EXPERIMENTAL_EVENTS:
111
+ elif api_url:
83
112
  return PrefectEventSubscriber(
84
113
  filter=filter, reconnection_attempts=reconnection_attempts
85
114
  )
115
+ elif PREFECT_SERVER_ALLOW_EPHEMERAL_MODE:
116
+ from prefect.server.api.server import SubprocessASGIServer
86
117
 
87
- raise RuntimeError(
88
- "The current server and client configuration does not support "
89
- "events. Enable experimental events support with the "
90
- "PREFECT_EXPERIMENTAL_EVENTS setting."
91
- )
118
+ server = SubprocessASGIServer()
119
+ server.start()
120
+ return PrefectEventSubscriber(
121
+ api_url=server.api_url,
122
+ filter=filter,
123
+ reconnection_attempts=reconnection_attempts,
124
+ )
125
+ else:
126
+ raise ValueError(
127
+ "No Prefect API URL provided. Please set PREFECT_API_URL to the address of a running Prefect server."
128
+ )
92
129
 
93
130
 
94
131
  class EventsClient(abc.ABC):
95
132
  """The abstract interface for all Prefect Events clients"""
96
133
 
134
+ @property
135
+ def client_name(self) -> str:
136
+ return self.__class__.__name__
137
+
97
138
  async def emit(self, event: Event) -> None:
98
139
  """Emit a single event"""
99
140
  if not hasattr(self, "_in_context"):
@@ -101,7 +142,11 @@ class EventsClient(abc.ABC):
101
142
  "Events may only be emitted while this client is being used as a "
102
143
  "context manager"
103
144
  )
104
- return await self._emit(event)
145
+
146
+ try:
147
+ return await self._emit(event)
148
+ finally:
149
+ EVENTS_EMITTED.labels(self.client_name).inc()
105
150
 
106
151
  @abc.abstractmethod
107
152
  async def _emit(self, event: Event) -> None: # pragma: no cover
@@ -132,7 +177,7 @@ class AssertingEventsClient(EventsClient):
132
177
  """A Prefect Events client that records all events sent to it for inspection during
133
178
  tests."""
134
179
 
135
- last: ClassVar["AssertingEventsClient | None"] = None
180
+ last: ClassVar["Optional[AssertingEventsClient]"] = None
136
181
  all: ClassVar[List["AssertingEventsClient"]] = []
137
182
 
138
183
  args: Tuple
@@ -152,6 +197,11 @@ class AssertingEventsClient(EventsClient):
152
197
  cls.last = None
153
198
  cls.all = []
154
199
 
200
+ def pop_events(self) -> List[Event]:
201
+ events = self.events
202
+ self.events = []
203
+ return events
204
+
155
205
  async def _emit(self, event: Event) -> None:
156
206
  self.events.append(event)
157
207
 
@@ -175,52 +225,6 @@ def _get_api_url_and_key(
175
225
  return api_url, api_key
176
226
 
177
227
 
178
- class PrefectEphemeralEventsClient(EventsClient):
179
- """A Prefect Events client that sends events to an ephemeral Prefect server"""
180
-
181
- def __init__(self):
182
- if not PREFECT_EXPERIMENTAL_EVENTS:
183
- raise ValueError(
184
- "PrefectEphemeralEventsClient can only be used when "
185
- "PREFECT_EXPERIMENTAL_EVENTS is set to True"
186
- )
187
- if PREFECT_API_KEY.value():
188
- raise ValueError(
189
- "PrefectEphemeralEventsClient cannot be used when PREFECT_API_KEY is set."
190
- " Please use PrefectEventsClient or PrefectCloudEventsClient instead."
191
- )
192
- from prefect.server.api.server import create_app
193
-
194
- app = create_app()
195
-
196
- self._http_client = PrefectHttpxAsyncClient(
197
- transport=httpx.ASGITransport(app=app, raise_app_exceptions=False),
198
- base_url="http://ephemeral-prefect/api",
199
- enable_csrf_support=False,
200
- )
201
-
202
- async def __aenter__(self) -> Self:
203
- await super().__aenter__()
204
- await self._http_client.__aenter__()
205
- return self
206
-
207
- async def __aexit__(
208
- self,
209
- exc_type: Optional[Type[Exception]],
210
- exc_val: Optional[Exception],
211
- exc_tb: Optional[TracebackType],
212
- ) -> None:
213
- self._websocket = None
214
- await self._http_client.__aexit__(exc_type, exc_val, exc_tb)
215
- return await super().__aexit__(exc_type, exc_val, exc_tb)
216
-
217
- async def _emit(self, event: Event) -> None:
218
- await self._http_client.post(
219
- "/events",
220
- json=[event.dict(json_compatible=True)],
221
- )
222
-
223
-
224
228
  class PrefectEventsClient(EventsClient):
225
229
  """A Prefect Events client that streams events to a Prefect server"""
226
230
 
@@ -231,7 +235,7 @@ class PrefectEventsClient(EventsClient):
231
235
  self,
232
236
  api_url: Optional[str] = None,
233
237
  reconnection_attempts: int = 10,
234
- checkpoint_every: int = 20,
238
+ checkpoint_every: int = 700,
235
239
  ):
236
240
  """
237
241
  Args:
@@ -311,6 +315,8 @@ class PrefectEventsClient(EventsClient):
311
315
  # don't clear the list, just the ones that we are sure of.
312
316
  self._unconfirmed_events = self._unconfirmed_events[unconfirmed_count:]
313
317
 
318
+ EVENT_WEBSOCKET_CHECKPOINTS.labels(self.client_name).inc()
319
+
314
320
  async def _emit(self, event: Event) -> None:
315
321
  for i in range(self._reconnection_attempts + 1):
316
322
  try:
@@ -324,7 +330,7 @@ class PrefectEventsClient(EventsClient):
324
330
  await self._reconnect()
325
331
  assert self._websocket
326
332
 
327
- await self._websocket.send(event.json())
333
+ await self._websocket.send(event.model_dump_json())
328
334
  await self._checkpoint(event)
329
335
 
330
336
  return
@@ -340,6 +346,47 @@ class PrefectEventsClient(EventsClient):
340
346
  await asyncio.sleep(1)
341
347
 
342
348
 
349
+ class AssertingPassthroughEventsClient(PrefectEventsClient):
350
+ """A Prefect Events client that BOTH records all events sent to it for inspection
351
+ during tests AND sends them to a Prefect server."""
352
+
353
+ last: ClassVar["Optional[AssertingPassthroughEventsClient]"] = None
354
+ all: ClassVar[List["AssertingPassthroughEventsClient"]] = []
355
+
356
+ args: Tuple
357
+ kwargs: Dict[str, Any]
358
+ events: List[Event]
359
+
360
+ def __init__(self, *args, **kwargs):
361
+ super().__init__(*args, **kwargs)
362
+ AssertingPassthroughEventsClient.last = self
363
+ AssertingPassthroughEventsClient.all.append(self)
364
+ self.args = args
365
+ self.kwargs = kwargs
366
+
367
+ @classmethod
368
+ def reset(cls) -> None:
369
+ cls.last = None
370
+ cls.all = []
371
+
372
+ def pop_events(self) -> List[Event]:
373
+ events = self.events
374
+ self.events = []
375
+ return events
376
+
377
+ async def _emit(self, event: Event) -> None:
378
+ # actually send the event to the server
379
+ await super()._emit(event)
380
+
381
+ # record the event for inspection
382
+ self.events.append(event)
383
+
384
+ async def __aenter__(self) -> Self:
385
+ await super().__aenter__()
386
+ self.events = []
387
+ return self
388
+
389
+
343
390
  class PrefectCloudEventsClient(PrefectEventsClient):
344
391
  """A Prefect Events client that streams events to a Prefect Cloud Workspace"""
345
392
 
@@ -348,7 +395,7 @@ class PrefectCloudEventsClient(PrefectEventsClient):
348
395
  api_url: Optional[str] = None,
349
396
  api_key: Optional[str] = None,
350
397
  reconnection_attempts: int = 10,
351
- checkpoint_every: int = 20,
398
+ checkpoint_every: int = 700,
352
399
  ):
353
400
  """
354
401
  Args:
@@ -412,9 +459,9 @@ class PrefectEventSubscriber:
412
459
  reconnection_attempts: When the client is disconnected, how many times
413
460
  the client should attempt to reconnect
414
461
  """
462
+ self._api_key = None
415
463
  if not api_url:
416
464
  api_url = cast(str, PREFECT_API_URL.value())
417
- self._api_key = None
418
465
 
419
466
  from prefect.events.filters import EventFilter
420
467
 
@@ -438,10 +485,17 @@ class PrefectEventSubscriber:
438
485
  if self._reconnection_attempts < 0:
439
486
  raise ValueError("reconnection_attempts must be a non-negative integer")
440
487
 
488
+ @property
489
+ def client_name(self) -> str:
490
+ return self.__class__.__name__
491
+
441
492
  async def __aenter__(self) -> Self:
442
493
  # Don't handle any errors in the initial connection, because these are most
443
494
  # likely a permission or configuration issue that should propagate
444
- await self._reconnect()
495
+ try:
496
+ await self._reconnect()
497
+ finally:
498
+ EVENT_WEBSOCKET_CONNECTIONS.labels(self.client_name, "out", "initial")
445
499
  return self
446
500
 
447
501
  async def _reconnect(self) -> None:
@@ -489,7 +543,7 @@ class PrefectEventSubscriber:
489
543
  logger.debug(" filtering events since %s...", self._filter.occurred.since)
490
544
  filter_message = {
491
545
  "type": "filter",
492
- "filter": self._filter.dict(json_compatible=True),
546
+ "filter": self._filter.model_dump(mode="json"),
493
547
  }
494
548
  await self._websocket.send(orjson.dumps(filter_message).decode())
495
549
 
@@ -515,18 +569,26 @@ class PrefectEventSubscriber:
515
569
  # Otherwise, after the first time through this loop, we're recovering
516
570
  # from a ConnectionClosed, so reconnect now.
517
571
  if not self._websocket or i > 0:
518
- await self._reconnect()
572
+ try:
573
+ await self._reconnect()
574
+ finally:
575
+ EVENT_WEBSOCKET_CONNECTIONS.labels(
576
+ self.client_name, "out", "reconnect"
577
+ )
519
578
  assert self._websocket
520
579
 
521
580
  while True:
522
581
  message = orjson.loads(await self._websocket.recv())
523
- event: Event = Event.parse_obj(message["event"])
582
+ event: Event = Event.model_validate(message["event"])
524
583
 
525
584
  if event.id in self._seen_events:
526
585
  continue
527
586
  self._seen_events[event.id] = True
528
587
 
529
- return event
588
+ try:
589
+ return event
590
+ finally:
591
+ EVENTS_OBSERVED.labels(self.client_name).inc()
530
592
  except ConnectionClosedOK:
531
593
  logger.debug('Connection closed with "OK" status')
532
594
  raise StopAsyncIteration
prefect/events/filters.py CHANGED
@@ -2,24 +2,19 @@ from typing import List, Optional, Tuple, cast
2
2
  from uuid import UUID
3
3
 
4
4
  import pendulum
5
+ from pydantic import Field, PrivateAttr
6
+ from pydantic_extra_types.pendulum_dt import DateTime
5
7
 
6
- from prefect._internal.pydantic import HAS_PYDANTIC_V2
7
8
  from prefect._internal.schemas.bases import PrefectBaseModel
8
- from prefect._internal.schemas.fields import DateTimeTZ
9
9
  from prefect.utilities.collections import AutoEnum
10
10
 
11
11
  from .schemas.events import Event, Resource, ResourceSpecification
12
12
 
13
- if HAS_PYDANTIC_V2:
14
- from pydantic.v1 import Field, PrivateAttr
15
- else:
16
- from pydantic import Field, PrivateAttr # type: ignore
17
-
18
13
 
19
14
  class AutomationFilterCreated(PrefectBaseModel):
20
15
  """Filter by `Automation.created`."""
21
16
 
22
- before_: Optional[DateTimeTZ] = Field(
17
+ before_: Optional[DateTime] = Field(
23
18
  default=None,
24
19
  description="Only include automations created before this datetime",
25
20
  )
@@ -46,18 +41,19 @@ class AutomationFilter(PrefectBaseModel):
46
41
  class EventDataFilter(PrefectBaseModel, extra="forbid"): # type: ignore[call-arg]
47
42
  """A base class for filtering event data."""
48
43
 
49
- _top_level_filter: "EventFilter | None" = PrivateAttr(None)
44
+ _top_level_filter: Optional["EventFilter"] = PrivateAttr(None)
50
45
 
51
46
  def get_filters(self) -> List["EventDataFilter"]:
52
- return [
47
+ filters: List["EventDataFilter"] = [
53
48
  filter
54
49
  for filter in [
55
- getattr(self, name)
56
- for name, field in self.__fields__.items()
57
- if issubclass(field.type_, EventDataFilter)
50
+ getattr(self, name) for name, field in self.model_fields.items()
58
51
  ]
59
- if filter
52
+ if isinstance(filter, EventDataFilter)
60
53
  ]
54
+ for filter in filters:
55
+ filter._top_level_filter = self._top_level_filter
56
+ return filters
61
57
 
62
58
  def includes(self, event: Event) -> bool:
63
59
  """Does the given event match the criteria of this filter?"""
@@ -69,15 +65,15 @@ class EventDataFilter(PrefectBaseModel, extra="forbid"): # type: ignore[call-ar
69
65
 
70
66
 
71
67
  class EventOccurredFilter(EventDataFilter):
72
- since: DateTimeTZ = Field(
68
+ since: DateTime = Field(
73
69
  default_factory=lambda: cast(
74
- DateTimeTZ,
70
+ DateTime,
75
71
  pendulum.now("UTC").start_of("day").subtract(days=180),
76
72
  ),
77
73
  description="Only include events after this time (inclusive)",
78
74
  )
79
- until: DateTimeTZ = Field(
80
- default_factory=lambda: cast(DateTimeTZ, pendulum.now("UTC")),
75
+ until: DateTime = Field(
76
+ default_factory=lambda: cast(DateTime, pendulum.now("UTC")),
81
77
  description="Only include events prior to this time (inclusive)",
82
78
  )
83
79