prefect-client 2.17.1__py3-none-any.whl → 2.18.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 (45) hide show
  1. prefect/_internal/compatibility/deprecated.py +2 -0
  2. prefect/_internal/pydantic/_compat.py +1 -0
  3. prefect/_internal/pydantic/utilities/field_validator.py +25 -10
  4. prefect/_internal/pydantic/utilities/model_dump.py +1 -1
  5. prefect/_internal/pydantic/utilities/model_validate.py +1 -1
  6. prefect/_internal/pydantic/utilities/model_validator.py +11 -3
  7. prefect/_internal/schemas/validators.py +0 -6
  8. prefect/_version.py +97 -38
  9. prefect/blocks/abstract.py +34 -1
  10. prefect/blocks/notifications.py +14 -5
  11. prefect/client/base.py +10 -5
  12. prefect/client/orchestration.py +125 -66
  13. prefect/client/schemas/actions.py +4 -3
  14. prefect/client/schemas/objects.py +6 -5
  15. prefect/client/schemas/schedules.py +2 -6
  16. prefect/deployments/__init__.py +0 -2
  17. prefect/deployments/base.py +2 -144
  18. prefect/deployments/deployments.py +2 -2
  19. prefect/deployments/runner.py +2 -2
  20. prefect/deployments/steps/core.py +3 -3
  21. prefect/deprecated/packaging/serializers.py +5 -4
  22. prefect/events/__init__.py +45 -0
  23. prefect/events/actions.py +250 -19
  24. prefect/events/cli/__init__.py +0 -0
  25. prefect/events/cli/automations.py +163 -0
  26. prefect/events/clients.py +133 -7
  27. prefect/events/schemas/automations.py +76 -3
  28. prefect/events/schemas/deployment_triggers.py +17 -59
  29. prefect/events/utilities.py +2 -0
  30. prefect/events/worker.py +12 -2
  31. prefect/exceptions.py +1 -1
  32. prefect/logging/__init__.py +2 -2
  33. prefect/logging/loggers.py +64 -1
  34. prefect/results.py +29 -10
  35. prefect/serializers.py +62 -31
  36. prefect/settings.py +6 -10
  37. prefect/types/__init__.py +90 -0
  38. prefect/utilities/pydantic.py +34 -15
  39. prefect/utilities/schema_tools/hydration.py +88 -19
  40. prefect/variables.py +4 -4
  41. {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/METADATA +1 -1
  42. {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/RECORD +45 -42
  43. {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/LICENSE +0 -0
  44. {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/WHEEL +0 -0
  45. {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/top_level.txt +0 -0
prefect/events/actions.py CHANGED
@@ -1,36 +1,70 @@
1
+ import abc
1
2
  from typing import Any, Dict, Optional, Union
2
3
  from uuid import UUID
3
4
 
5
+ from typing_extensions import Literal, TypeAlias
6
+
4
7
  from prefect._internal.pydantic import HAS_PYDANTIC_V2
5
8
 
6
9
  if HAS_PYDANTIC_V2:
7
- from pydantic.v1 import Field
10
+ from pydantic.v1 import Field, root_validator
8
11
  else:
9
- from pydantic import Field
10
-
11
- from typing_extensions import Literal
12
-
12
+ from pydantic import Field, root_validator
13
13
  from prefect._internal.schemas.bases import PrefectBaseModel
14
+ from prefect.client.schemas.objects import StateType
14
15
 
15
16
 
16
- class Action(PrefectBaseModel):
17
+ class Action(PrefectBaseModel, abc.ABC):
17
18
  """An Action that may be performed when an Automation is triggered"""
18
19
 
19
20
  type: str
20
21
 
22
+ def describe_for_cli(self) -> str:
23
+ """A human-readable description of the action"""
24
+ return self.type.replace("-", " ").capitalize()
25
+
21
26
 
22
27
  class DoNothing(Action):
23
- """Do nothing, which may be helpful for testing automations"""
28
+ """Do nothing when an Automation is triggered"""
24
29
 
25
30
  type: Literal["do-nothing"] = "do-nothing"
26
31
 
27
32
 
28
- class RunDeployment(Action):
29
- """Run the given deployment with the given parameters"""
33
+ class DeploymentAction(Action):
34
+ """Base class for Actions that operate on Deployments and need to infer them from
35
+ events"""
36
+
37
+ source: Literal["selected", "inferred"] = Field(
38
+ "selected",
39
+ description=(
40
+ "Whether this Action applies to a specific selected "
41
+ "deployment (given by `deployment_id`), or to a deployment that is "
42
+ "inferred from the triggering event. If the source is 'inferred', "
43
+ "the `deployment_id` may not be set. If the source is 'selected', the "
44
+ "`deployment_id` must be set."
45
+ ),
46
+ )
47
+ deployment_id: Optional[UUID] = Field(
48
+ None, description="The identifier of the deployment"
49
+ )
50
+
51
+ @root_validator
52
+ def selected_deployment_requires_id(cls, values):
53
+ wants_selected_deployment = values.get("source") == "selected"
54
+ has_deployment_id = bool(values.get("deployment_id"))
55
+ if wants_selected_deployment != has_deployment_id:
56
+ raise ValueError(
57
+ "deployment_id is "
58
+ + ("not allowed" if has_deployment_id else "required")
59
+ )
60
+ return values
61
+
62
+
63
+ class RunDeployment(DeploymentAction):
64
+ """Runs the given deployment with the given parameters"""
30
65
 
31
66
  type: Literal["run-deployment"] = "run-deployment"
32
67
 
33
- source: Literal["selected"] = "selected"
34
68
  parameters: Optional[Dict[str, Any]] = Field(
35
69
  None,
36
70
  description=(
@@ -38,26 +72,223 @@ class RunDeployment(Action):
38
72
  "deployment's default parameters"
39
73
  ),
40
74
  )
41
- deployment_id: UUID = Field(..., description="The identifier of the deployment")
42
75
  job_variables: Optional[Dict[str, Any]] = Field(
43
76
  None,
44
77
  description=(
45
- "Job variables to pass to the run, or None to use the "
46
- "deployment's default job variables"
78
+ "The job variables to pass to the created flow run, or None "
79
+ "to use the deployment's default job variables"
47
80
  ),
48
81
  )
49
82
 
50
83
 
84
+ class PauseDeployment(DeploymentAction):
85
+ """Pauses the given Deployment"""
86
+
87
+ type: Literal["pause-deployment"] = "pause-deployment"
88
+
89
+
90
+ class ResumeDeployment(DeploymentAction):
91
+ """Resumes the given Deployment"""
92
+
93
+ type: Literal["resume-deployment"] = "resume-deployment"
94
+
95
+
96
+ class ChangeFlowRunState(Action):
97
+ """Changes the state of a flow run associated with the trigger"""
98
+
99
+ type: Literal["change-flow-run-state"] = "change-flow-run-state"
100
+
101
+ name: Optional[str] = Field(
102
+ None,
103
+ description="The name of the state to change the flow run to",
104
+ )
105
+ state: StateType = Field(
106
+ ...,
107
+ description="The type of the state to change the flow run to",
108
+ )
109
+ message: Optional[str] = Field(
110
+ None,
111
+ description="An optional message to associate with the state change",
112
+ )
113
+
114
+
115
+ class CancelFlowRun(Action):
116
+ """Cancels a flow run associated with the trigger"""
117
+
118
+ type: Literal["cancel-flow-run"] = "cancel-flow-run"
119
+
120
+
121
+ class SuspendFlowRun(Action):
122
+ """Suspends a flow run associated with the trigger"""
123
+
124
+ type: Literal["suspend-flow-run"] = "suspend-flow-run"
125
+
126
+
127
+ class CallWebhook(Action):
128
+ """Call a webhook when an Automation is triggered."""
129
+
130
+ type: Literal["call-webhook"] = "call-webhook"
131
+ block_document_id: UUID = Field(
132
+ description="The identifier of the webhook block to use"
133
+ )
134
+ payload: str = Field(
135
+ default="",
136
+ description="An optional templatable payload to send when calling the webhook.",
137
+ )
138
+
139
+
51
140
  class SendNotification(Action):
52
- """Send a notification with the given parameters"""
141
+ """Send a notification when an Automation is triggered"""
53
142
 
54
143
  type: Literal["send-notification"] = "send-notification"
55
-
56
144
  block_document_id: UUID = Field(
57
- ..., description="The identifier of the notification block"
145
+ description="The identifier of the notification block to use"
58
146
  )
59
- body: str = Field(..., description="Notification body")
60
- subject: Optional[str] = Field(None, description="Notification subject")
147
+ subject: str = Field("Prefect automated notification")
148
+ body: str = Field(description="The text of the notification to send")
149
+
150
+
151
+ class WorkPoolAction(Action):
152
+ """Base class for Actions that operate on Work Pools and need to infer them from
153
+ events"""
154
+
155
+ source: Literal["selected", "inferred"] = Field(
156
+ "selected",
157
+ description=(
158
+ "Whether this Action applies to a specific selected "
159
+ "work pool (given by `work_pool_id`), or to a work pool that is "
160
+ "inferred from the triggering event. If the source is 'inferred', "
161
+ "the `work_pool_id` may not be set. If the source is 'selected', the "
162
+ "`work_pool_id` must be set."
163
+ ),
164
+ )
165
+ work_pool_id: Optional[UUID] = Field(
166
+ None,
167
+ description="The identifier of the work pool to pause",
168
+ )
169
+
170
+
171
+ class PauseWorkPool(WorkPoolAction):
172
+ """Pauses a Work Pool"""
173
+
174
+ type: Literal["pause-work-pool"] = "pause-work-pool"
175
+
176
+
177
+ class ResumeWorkPool(WorkPoolAction):
178
+ """Resumes a Work Pool"""
179
+
180
+ type: Literal["resume-work-pool"] = "resume-work-pool"
181
+
182
+
183
+ class WorkQueueAction(Action):
184
+ """Base class for Actions that operate on Work Queues and need to infer them from
185
+ events"""
186
+
187
+ source: Literal["selected", "inferred"] = Field(
188
+ "selected",
189
+ description=(
190
+ "Whether this Action applies to a specific selected "
191
+ "work queue (given by `work_queue_id`), or to a work queue that is "
192
+ "inferred from the triggering event. If the source is 'inferred', "
193
+ "the `work_queue_id` may not be set. If the source is 'selected', the "
194
+ "`work_queue_id` must be set."
195
+ ),
196
+ )
197
+ work_queue_id: Optional[UUID] = Field(
198
+ None, description="The identifier of the work queue to pause"
199
+ )
200
+
201
+ @root_validator
202
+ def selected_work_queue_requires_id(cls, values):
203
+ wants_selected_work_queue = values.get("source") == "selected"
204
+ has_work_queue_id = bool(values.get("work_queue_id"))
205
+ if wants_selected_work_queue != has_work_queue_id:
206
+ raise ValueError(
207
+ "work_queue_id is "
208
+ + ("not allowed" if has_work_queue_id else "required")
209
+ )
210
+ return values
211
+
212
+
213
+ class PauseWorkQueue(WorkQueueAction):
214
+ """Pauses a Work Queue"""
215
+
216
+ type: Literal["pause-work-queue"] = "pause-work-queue"
217
+
218
+
219
+ class ResumeWorkQueue(WorkQueueAction):
220
+ """Resumes a Work Queue"""
221
+
222
+ type: Literal["resume-work-queue"] = "resume-work-queue"
223
+
224
+
225
+ class AutomationAction(Action):
226
+ """Base class for Actions that operate on Automations and need to infer them from
227
+ events"""
228
+
229
+ source: Literal["selected", "inferred"] = Field(
230
+ "selected",
231
+ description=(
232
+ "Whether this Action applies to a specific selected "
233
+ "automation (given by `automation_id`), or to an automation that is "
234
+ "inferred from the triggering event. If the source is 'inferred', "
235
+ "the `automation_id` may not be set. If the source is 'selected', the "
236
+ "`automation_id` must be set."
237
+ ),
238
+ )
239
+ automation_id: Optional[UUID] = Field(
240
+ None, description="The identifier of the automation to act on"
241
+ )
242
+
243
+ @root_validator
244
+ def selected_automation_requires_id(cls, values):
245
+ wants_selected_automation = values.get("source") == "selected"
246
+ has_automation_id = bool(values.get("automation_id"))
247
+ if wants_selected_automation != has_automation_id:
248
+ raise ValueError(
249
+ "automation_id is "
250
+ + ("not allowed" if has_automation_id else "required")
251
+ )
252
+ return values
253
+
254
+
255
+ class PauseAutomation(AutomationAction):
256
+ """Pauses a Work Queue"""
257
+
258
+ type: Literal["pause-automation"] = "pause-automation"
259
+
260
+
261
+ class ResumeAutomation(AutomationAction):
262
+ """Resumes a Work Queue"""
263
+
264
+ type: Literal["resume-automation"] = "resume-automation"
265
+
266
+
267
+ class DeclareIncident(Action):
268
+ """Declares an incident for the triggering event. Only available on Prefect Cloud"""
269
+
270
+ type: Literal["declare-incident"] = "declare-incident"
61
271
 
62
272
 
63
- ActionTypes = Union[DoNothing, RunDeployment, SendNotification]
273
+ # The actual action types that we support. It's important to update this
274
+ # Union when adding new subclasses of Action so that they are available for clients
275
+ # and in the OpenAPI docs
276
+ ActionTypes: TypeAlias = Union[
277
+ DoNothing,
278
+ RunDeployment,
279
+ PauseDeployment,
280
+ ResumeDeployment,
281
+ CancelFlowRun,
282
+ ChangeFlowRunState,
283
+ PauseWorkQueue,
284
+ ResumeWorkQueue,
285
+ SendNotification,
286
+ CallWebhook,
287
+ PauseAutomation,
288
+ ResumeAutomation,
289
+ SuspendFlowRun,
290
+ PauseWorkPool,
291
+ ResumeWorkPool,
292
+ # Prefect Cloud only
293
+ DeclareIncident,
294
+ ]
File without changes
@@ -0,0 +1,163 @@
1
+ """
2
+ Command line interface for working with automations.
3
+ """
4
+
5
+ import functools
6
+
7
+ import orjson
8
+ import yaml as pyyaml
9
+ from rich.pretty import Pretty
10
+ from rich.table import Table
11
+ from rich.text import Text
12
+
13
+ from prefect.cli._types import PrefectTyper
14
+ from prefect.cli._utilities import exit_with_error, exit_with_success
15
+ from prefect.cli.root import app
16
+ from prefect.client.orchestration import get_client
17
+
18
+ automations_app = PrefectTyper(
19
+ name="automation",
20
+ help="Commands for managing automations.",
21
+ )
22
+ app.add_typer(automations_app, aliases=["automations"])
23
+
24
+
25
+ def requires_automations(func):
26
+ @functools.wraps(func)
27
+ async def wrapper(*args, **kwargs):
28
+ try:
29
+ return await func(*args, **kwargs)
30
+ except RuntimeError as exc:
31
+ if "Enable experimental" in str(exc):
32
+ exit_with_error(str(exc))
33
+ raise
34
+
35
+ return wrapper
36
+
37
+
38
+ @automations_app.command()
39
+ @requires_automations
40
+ async def ls():
41
+ """List all automations."""
42
+ async with get_client() as client:
43
+ automations = await client.read_automations()
44
+
45
+ table = Table(title="Automations", show_lines=True)
46
+
47
+ table.add_column("Automation")
48
+ table.add_column("Enabled")
49
+ table.add_column("Trigger")
50
+ table.add_column("Actions")
51
+
52
+ for automation in automations:
53
+ identifier_column = Text()
54
+
55
+ identifier_column.append(automation.name, style="white bold")
56
+ identifier_column.append("\n")
57
+
58
+ identifier_column.append(str(automation.id), style="cyan")
59
+ identifier_column.append("\n")
60
+
61
+ if automation.description:
62
+ identifier_column.append(automation.description, style="white")
63
+ identifier_column.append("\n")
64
+
65
+ if automation.actions_on_trigger or automation.actions_on_resolve:
66
+ actions = (
67
+ [
68
+ f"(trigger) {action.describe_for_cli()}"
69
+ for action in automation.actions
70
+ ]
71
+ + [
72
+ f"(trigger) {action.describe_for_cli()}"
73
+ for action in automation.actions_on_trigger
74
+ ]
75
+ + [
76
+ f"(resolve) {action.describe_for_cli()}"
77
+ for action in automation.actions
78
+ ]
79
+ + [
80
+ f"(resolve) {action.describe_for_cli()}"
81
+ for action in automation.actions_on_resolve
82
+ ]
83
+ )
84
+ else:
85
+ actions = [action.describe_for_cli() for action in automation.actions]
86
+
87
+ table.add_row(
88
+ identifier_column,
89
+ str(automation.enabled),
90
+ automation.trigger.describe_for_cli(),
91
+ "\n".join(actions),
92
+ )
93
+
94
+ app.console.print(table)
95
+
96
+
97
+ @automations_app.command()
98
+ @requires_automations
99
+ async def inspect(id_or_name: str, yaml: bool = False, json: bool = False):
100
+ """Inspect an automation."""
101
+ async with get_client() as client:
102
+ automation = await client.find_automation(id_or_name)
103
+ if not automation:
104
+ exit_with_error(f"Automation {id_or_name!r} not found.")
105
+
106
+ if yaml:
107
+ app.console.print(
108
+ pyyaml.dump(automation.dict(json_compatible=True), sort_keys=False)
109
+ )
110
+ elif json:
111
+ app.console.print(
112
+ orjson.dumps(
113
+ automation.dict(json_compatible=True), option=orjson.OPT_INDENT_2
114
+ ).decode()
115
+ )
116
+ else:
117
+ app.console.print(Pretty(automation))
118
+
119
+
120
+ @automations_app.command(aliases=["enable"])
121
+ @requires_automations
122
+ async def resume(id_or_name: str):
123
+ """Resume an automation."""
124
+ async with get_client() as client:
125
+ automation = await client.find_automation(id_or_name)
126
+ if not automation:
127
+ exit_with_error(f"Automation {id_or_name!r} not found.")
128
+
129
+ async with get_client() as client:
130
+ await client.resume_automation(automation.id)
131
+
132
+ exit_with_success(f"Resumed automation {automation.name!r} ({automation.id})")
133
+
134
+
135
+ @automations_app.command(aliases=["disable"])
136
+ @requires_automations
137
+ async def pause(id_or_name: str):
138
+ """Pause an automation."""
139
+ async with get_client() as client:
140
+ automation = await client.find_automation(id_or_name)
141
+ if not automation:
142
+ exit_with_error(f"Automation {id_or_name!r} not found.")
143
+
144
+ async with get_client() as client:
145
+ await client.pause_automation(automation.id)
146
+
147
+ exit_with_success(f"Paused automation {automation.name!r} ({automation.id})")
148
+
149
+
150
+ @automations_app.command()
151
+ @requires_automations
152
+ async def delete(id_or_name: str):
153
+ """Delete an automation."""
154
+ async with get_client() as client:
155
+ automation = await client.find_automation(id_or_name)
156
+
157
+ if not automation:
158
+ exit_with_success(f"Automation {id_or_name!r} not found.")
159
+
160
+ async with get_client() as client:
161
+ await client.delete_automation(automation.id)
162
+
163
+ exit_with_success(f"Deleted automation {automation.name!r} ({automation.id})")
prefect/events/clients.py CHANGED
@@ -14,6 +14,7 @@ from typing import (
14
14
  )
15
15
  from uuid import UUID
16
16
 
17
+ import httpx
17
18
  import orjson
18
19
  import pendulum
19
20
  from cachetools import TTLCache
@@ -25,9 +26,15 @@ from websockets.exceptions import (
25
26
  ConnectionClosedOK,
26
27
  )
27
28
 
29
+ from prefect.client.base import PrefectHttpxClient
28
30
  from prefect.events import Event
29
31
  from prefect.logging import get_logger
30
- from prefect.settings import PREFECT_API_KEY, PREFECT_API_URL
32
+ from prefect.settings import (
33
+ PREFECT_API_KEY,
34
+ PREFECT_API_URL,
35
+ PREFECT_CLOUD_API_URL,
36
+ PREFECT_EXPERIMENTAL_EVENTS,
37
+ )
31
38
 
32
39
  if TYPE_CHECKING:
33
40
  from prefect.events.filters import EventFilter
@@ -35,6 +42,53 @@ if TYPE_CHECKING:
35
42
  logger = get_logger(__name__)
36
43
 
37
44
 
45
+ def get_events_client(
46
+ reconnection_attempts: int = 10,
47
+ checkpoint_every: int = 20,
48
+ ) -> "EventsClient":
49
+ api_url = PREFECT_API_URL.value()
50
+ if isinstance(api_url, str) and api_url.startswith(PREFECT_CLOUD_API_URL.value()):
51
+ return PrefectCloudEventsClient(
52
+ reconnection_attempts=reconnection_attempts,
53
+ checkpoint_every=checkpoint_every,
54
+ )
55
+ elif PREFECT_EXPERIMENTAL_EVENTS:
56
+ if PREFECT_API_URL:
57
+ return PrefectEventsClient(
58
+ reconnection_attempts=reconnection_attempts,
59
+ checkpoint_every=checkpoint_every,
60
+ )
61
+ else:
62
+ return PrefectEphemeralEventsClient()
63
+
64
+ raise RuntimeError(
65
+ "The current server and client configuration does not support "
66
+ "events. Enable experimental events support with the "
67
+ "PREFECT_EXPERIMENTAL_EVENTS setting."
68
+ )
69
+
70
+
71
+ def get_events_subscriber(
72
+ filter: "EventFilter" = None,
73
+ reconnection_attempts: int = 10,
74
+ ) -> "PrefectEventSubscriber":
75
+ api_url = PREFECT_API_URL.value()
76
+ if isinstance(api_url, str) and api_url.startswith(PREFECT_CLOUD_API_URL.value()):
77
+ return PrefectCloudEventSubscriber(
78
+ filter=filter, reconnection_attempts=reconnection_attempts
79
+ )
80
+ elif PREFECT_EXPERIMENTAL_EVENTS:
81
+ return PrefectEventSubscriber(
82
+ filter=filter, reconnection_attempts=reconnection_attempts
83
+ )
84
+
85
+ raise RuntimeError(
86
+ "The current server and client configuration does not support "
87
+ "events. Enable experimental events support with the "
88
+ "PREFECT_EXPERIMENTAL_EVENTS setting."
89
+ )
90
+
91
+
38
92
  class EventsClient(abc.ABC):
39
93
  """The abstract interface for all Prefect Events clients"""
40
94
 
@@ -119,6 +173,52 @@ def _get_api_url_and_key(
119
173
  return api_url, api_key
120
174
 
121
175
 
176
+ class PrefectEphemeralEventsClient(EventsClient):
177
+ """A Prefect Events client that sends events to an ephemeral Prefect server"""
178
+
179
+ def __init__(self):
180
+ if not PREFECT_EXPERIMENTAL_EVENTS:
181
+ raise ValueError(
182
+ "PrefectEphemeralEventsClient can only be used when "
183
+ "PREFECT_EXPERIMENTAL_EVENTS is set to True"
184
+ )
185
+ if PREFECT_API_KEY.value():
186
+ raise ValueError(
187
+ "PrefectEphemeralEventsClient cannot be used when PREFECT_API_KEY is set."
188
+ " Please use PrefectEventsClient or PrefectCloudEventsClient instead."
189
+ )
190
+ from prefect.server.api.server import create_app
191
+
192
+ app = create_app()
193
+
194
+ self._http_client = PrefectHttpxClient(
195
+ transport=httpx.ASGITransport(app=app, raise_app_exceptions=False),
196
+ base_url="http://ephemeral-prefect/api",
197
+ enable_csrf_support=False,
198
+ )
199
+
200
+ async def __aenter__(self) -> Self:
201
+ await super().__aenter__()
202
+ await self._http_client.__aenter__()
203
+ return self
204
+
205
+ async def __aexit__(
206
+ self,
207
+ exc_type: Optional[Type[Exception]],
208
+ exc_val: Optional[Exception],
209
+ exc_tb: Optional[TracebackType],
210
+ ) -> None:
211
+ self._websocket = None
212
+ await self._http_client.__aexit__(exc_type, exc_val, exc_tb)
213
+ return await super().__aexit__(exc_type, exc_val, exc_tb)
214
+
215
+ async def _emit(self, event: Event) -> None:
216
+ await self._http_client.post(
217
+ "/events",
218
+ json=[event.dict(json_compatible=True)],
219
+ )
220
+
221
+
122
222
  class PrefectEventsClient(EventsClient):
123
223
  """A Prefect Events client that streams events to a Prefect server"""
124
224
 
@@ -274,18 +374,18 @@ SEEN_EVENTS_SIZE = 500_000
274
374
  SEEN_EVENTS_TTL = 120
275
375
 
276
376
 
277
- class PrefectCloudEventSubscriber:
377
+ class PrefectEventSubscriber:
278
378
  """
279
379
  Subscribes to a Prefect Cloud event stream, yielding events as they occur.
280
380
 
281
381
  Example:
282
382
 
283
- from prefect.events.clients import PrefectCloudEventSubscriber
383
+ from prefect.events.clients import PrefectEventSubscriber
284
384
  from prefect.events.filters import EventFilter, EventNameFilter
285
385
 
286
386
  filter = EventFilter(event=EventNameFilter(prefix=["prefect.flow-run."]))
287
387
 
288
- async with PrefectCloudEventSubscriber(api_url, api_key, filter) as subscriber:
388
+ async with PrefectEventSubscriber(filter=filter) as subscriber:
289
389
  async for event in subscriber:
290
390
  print(event.occurred, event.resource.id, event.event)
291
391
 
@@ -298,7 +398,6 @@ class PrefectCloudEventSubscriber:
298
398
  def __init__(
299
399
  self,
300
400
  api_url: str = None,
301
- api_key: str = None,
302
401
  filter: "EventFilter" = None,
303
402
  reconnection_attempts: int = 10,
304
403
  ):
@@ -309,7 +408,9 @@ class PrefectCloudEventSubscriber:
309
408
  reconnection_attempts: When the client is disconnected, how many times
310
409
  the client should attempt to reconnect
311
410
  """
312
- api_url, api_key = _get_api_url_and_key(api_url, api_key)
411
+ if not api_url:
412
+ api_url = PREFECT_API_URL.value()
413
+ self._api_key = None
313
414
 
314
415
  from prefect.events.filters import EventFilter
315
416
 
@@ -324,7 +425,6 @@ class PrefectCloudEventSubscriber:
324
425
 
325
426
  logger.debug("Connecting to %s", socket_url)
326
427
 
327
- self._api_key = api_key
328
428
  self._connect = connect(
329
429
  socket_url,
330
430
  subprotocols=["prefect"],
@@ -441,3 +541,29 @@ class PrefectCloudEventSubscriber:
441
541
  # a standard load balancer timeout, but after that, just take a
442
542
  # beat to let things come back around.
443
543
  await asyncio.sleep(1)
544
+
545
+
546
+ class PrefectCloudEventSubscriber(PrefectEventSubscriber):
547
+ def __init__(
548
+ self,
549
+ api_url: str = None,
550
+ api_key: str = None,
551
+ filter: "EventFilter" = None,
552
+ reconnection_attempts: int = 10,
553
+ ):
554
+ """
555
+ Args:
556
+ api_url: The base URL for a Prefect Cloud workspace
557
+ api_key: The API of an actor with the manage_events scope
558
+ reconnection_attempts: When the client is disconnected, how many times
559
+ the client should attempt to reconnect
560
+ """
561
+ api_url, api_key = _get_api_url_and_key(api_url, api_key)
562
+
563
+ super().__init__(
564
+ api_url=api_url,
565
+ filter=filter,
566
+ reconnection_attempts=reconnection_attempts,
567
+ )
568
+
569
+ self._api_key = api_key