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.
- prefect/__init__.py +74 -110
- prefect/_internal/compatibility/deprecated.py +6 -115
- prefect/_internal/compatibility/experimental.py +4 -79
- prefect/_internal/compatibility/migration.py +166 -0
- prefect/_internal/concurrency/__init__.py +2 -2
- prefect/_internal/concurrency/api.py +1 -35
- prefect/_internal/concurrency/calls.py +0 -6
- prefect/_internal/concurrency/cancellation.py +0 -3
- prefect/_internal/concurrency/event_loop.py +0 -20
- prefect/_internal/concurrency/inspection.py +3 -3
- prefect/_internal/concurrency/primitives.py +1 -0
- prefect/_internal/concurrency/services.py +23 -0
- prefect/_internal/concurrency/threads.py +35 -0
- prefect/_internal/concurrency/waiters.py +0 -28
- prefect/_internal/integrations.py +7 -0
- prefect/_internal/pydantic/__init__.py +0 -45
- prefect/_internal/pydantic/annotations/pendulum.py +2 -2
- prefect/_internal/pydantic/v1_schema.py +21 -22
- prefect/_internal/pydantic/v2_schema.py +0 -2
- prefect/_internal/pydantic/v2_validated_func.py +18 -23
- prefect/_internal/pytz.py +1 -1
- prefect/_internal/retries.py +61 -0
- prefect/_internal/schemas/bases.py +45 -177
- prefect/_internal/schemas/fields.py +1 -43
- prefect/_internal/schemas/validators.py +47 -233
- prefect/agent.py +3 -695
- prefect/artifacts.py +173 -14
- prefect/automations.py +39 -4
- prefect/blocks/abstract.py +1 -1
- prefect/blocks/core.py +405 -153
- prefect/blocks/fields.py +2 -57
- prefect/blocks/notifications.py +43 -28
- prefect/blocks/redis.py +168 -0
- prefect/blocks/system.py +67 -20
- prefect/blocks/webhook.py +2 -9
- prefect/cache_policies.py +239 -0
- prefect/client/__init__.py +4 -0
- prefect/client/base.py +33 -27
- prefect/client/cloud.py +65 -20
- prefect/client/collections.py +1 -1
- prefect/client/orchestration.py +650 -442
- prefect/client/schemas/actions.py +115 -100
- prefect/client/schemas/filters.py +46 -52
- prefect/client/schemas/objects.py +228 -178
- prefect/client/schemas/responses.py +18 -36
- prefect/client/schemas/schedules.py +55 -36
- prefect/client/schemas/sorting.py +2 -0
- prefect/client/subscriptions.py +8 -7
- prefect/client/types/flexible_schedule_list.py +11 -0
- prefect/client/utilities.py +9 -6
- prefect/concurrency/asyncio.py +60 -11
- prefect/concurrency/context.py +24 -0
- prefect/concurrency/events.py +2 -2
- prefect/concurrency/services.py +46 -16
- prefect/concurrency/sync.py +51 -7
- prefect/concurrency/v1/asyncio.py +143 -0
- prefect/concurrency/v1/context.py +27 -0
- prefect/concurrency/v1/events.py +61 -0
- prefect/concurrency/v1/services.py +116 -0
- prefect/concurrency/v1/sync.py +92 -0
- prefect/context.py +246 -149
- prefect/deployments/__init__.py +33 -18
- prefect/deployments/base.py +10 -15
- prefect/deployments/deployments.py +2 -1048
- prefect/deployments/flow_runs.py +178 -0
- prefect/deployments/runner.py +72 -173
- prefect/deployments/schedules.py +31 -25
- prefect/deployments/steps/__init__.py +0 -1
- prefect/deployments/steps/core.py +7 -0
- prefect/deployments/steps/pull.py +15 -21
- prefect/deployments/steps/utility.py +2 -1
- prefect/docker/__init__.py +20 -0
- prefect/docker/docker_image.py +82 -0
- prefect/engine.py +15 -2475
- prefect/events/actions.py +17 -23
- prefect/events/cli/automations.py +20 -7
- prefect/events/clients.py +142 -80
- prefect/events/filters.py +14 -18
- prefect/events/related.py +74 -75
- prefect/events/schemas/__init__.py +0 -5
- prefect/events/schemas/automations.py +55 -46
- prefect/events/schemas/deployment_triggers.py +7 -197
- prefect/events/schemas/events.py +46 -65
- prefect/events/schemas/labelling.py +10 -14
- prefect/events/utilities.py +4 -5
- prefect/events/worker.py +23 -8
- prefect/exceptions.py +15 -0
- prefect/filesystems.py +30 -529
- prefect/flow_engine.py +827 -0
- prefect/flow_runs.py +379 -7
- prefect/flows.py +470 -360
- prefect/futures.py +382 -331
- prefect/infrastructure/__init__.py +5 -26
- prefect/infrastructure/base.py +3 -320
- prefect/infrastructure/provisioners/__init__.py +5 -3
- prefect/infrastructure/provisioners/cloud_run.py +13 -8
- prefect/infrastructure/provisioners/container_instance.py +14 -9
- prefect/infrastructure/provisioners/ecs.py +10 -8
- prefect/infrastructure/provisioners/modal.py +8 -5
- prefect/input/__init__.py +4 -0
- prefect/input/actions.py +2 -4
- prefect/input/run_input.py +9 -9
- prefect/logging/formatters.py +2 -4
- prefect/logging/handlers.py +9 -14
- prefect/logging/loggers.py +5 -5
- prefect/main.py +72 -0
- prefect/plugins.py +2 -64
- prefect/profiles.toml +16 -2
- prefect/records/__init__.py +1 -0
- prefect/records/base.py +223 -0
- prefect/records/filesystem.py +207 -0
- prefect/records/memory.py +178 -0
- prefect/records/result_store.py +64 -0
- prefect/results.py +577 -504
- prefect/runner/runner.py +117 -47
- prefect/runner/server.py +32 -34
- prefect/runner/storage.py +3 -12
- prefect/runner/submit.py +2 -10
- prefect/runner/utils.py +2 -2
- prefect/runtime/__init__.py +1 -0
- prefect/runtime/deployment.py +1 -0
- prefect/runtime/flow_run.py +40 -5
- prefect/runtime/task_run.py +1 -0
- prefect/serializers.py +28 -39
- prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
- prefect/settings.py +209 -332
- prefect/states.py +160 -63
- prefect/task_engine.py +1478 -57
- prefect/task_runners.py +383 -287
- prefect/task_runs.py +240 -0
- prefect/task_worker.py +463 -0
- prefect/tasks.py +684 -374
- prefect/transactions.py +410 -0
- prefect/types/__init__.py +72 -86
- prefect/types/entrypoint.py +13 -0
- prefect/utilities/annotations.py +4 -3
- prefect/utilities/asyncutils.py +227 -148
- prefect/utilities/callables.py +137 -45
- prefect/utilities/collections.py +134 -86
- prefect/utilities/dispatch.py +27 -14
- prefect/utilities/dockerutils.py +11 -4
- prefect/utilities/engine.py +186 -32
- prefect/utilities/filesystem.py +4 -5
- prefect/utilities/importtools.py +26 -27
- prefect/utilities/pydantic.py +128 -38
- prefect/utilities/schema_tools/hydration.py +18 -1
- prefect/utilities/schema_tools/validation.py +30 -0
- prefect/utilities/services.py +35 -9
- prefect/utilities/templating.py +12 -2
- prefect/utilities/timeout.py +20 -5
- prefect/utilities/urls.py +195 -0
- prefect/utilities/visualization.py +1 -0
- prefect/variables.py +78 -59
- prefect/workers/__init__.py +0 -1
- prefect/workers/base.py +237 -244
- prefect/workers/block.py +5 -226
- prefect/workers/cloud.py +6 -0
- prefect/workers/process.py +265 -12
- prefect/workers/server.py +29 -11
- {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/METADATA +28 -24
- prefect_client-3.0.0.dist-info/RECORD +201 -0
- {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/WHEEL +1 -1
- prefect/_internal/pydantic/_base_model.py +0 -51
- prefect/_internal/pydantic/_compat.py +0 -82
- prefect/_internal/pydantic/_flags.py +0 -20
- prefect/_internal/pydantic/_types.py +0 -8
- prefect/_internal/pydantic/utilities/config_dict.py +0 -72
- prefect/_internal/pydantic/utilities/field_validator.py +0 -150
- prefect/_internal/pydantic/utilities/model_construct.py +0 -56
- prefect/_internal/pydantic/utilities/model_copy.py +0 -55
- prefect/_internal/pydantic/utilities/model_dump.py +0 -136
- prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
- prefect/_internal/pydantic/utilities/model_fields.py +0 -50
- prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
- prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
- prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
- prefect/_internal/pydantic/utilities/model_validate.py +0 -75
- prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
- prefect/_internal/pydantic/utilities/model_validator.py +0 -87
- prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
- prefect/_vendor/fastapi/__init__.py +0 -25
- prefect/_vendor/fastapi/applications.py +0 -946
- prefect/_vendor/fastapi/background.py +0 -3
- prefect/_vendor/fastapi/concurrency.py +0 -44
- prefect/_vendor/fastapi/datastructures.py +0 -58
- prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
- prefect/_vendor/fastapi/dependencies/models.py +0 -64
- prefect/_vendor/fastapi/dependencies/utils.py +0 -877
- prefect/_vendor/fastapi/encoders.py +0 -177
- prefect/_vendor/fastapi/exception_handlers.py +0 -40
- prefect/_vendor/fastapi/exceptions.py +0 -46
- prefect/_vendor/fastapi/logger.py +0 -3
- prefect/_vendor/fastapi/middleware/__init__.py +0 -1
- prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
- prefect/_vendor/fastapi/middleware/cors.py +0 -3
- prefect/_vendor/fastapi/middleware/gzip.py +0 -3
- prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
- prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
- prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
- prefect/_vendor/fastapi/openapi/__init__.py +0 -0
- prefect/_vendor/fastapi/openapi/constants.py +0 -2
- prefect/_vendor/fastapi/openapi/docs.py +0 -203
- prefect/_vendor/fastapi/openapi/models.py +0 -480
- prefect/_vendor/fastapi/openapi/utils.py +0 -485
- prefect/_vendor/fastapi/param_functions.py +0 -340
- prefect/_vendor/fastapi/params.py +0 -453
- prefect/_vendor/fastapi/py.typed +0 -0
- prefect/_vendor/fastapi/requests.py +0 -4
- prefect/_vendor/fastapi/responses.py +0 -40
- prefect/_vendor/fastapi/routing.py +0 -1331
- prefect/_vendor/fastapi/security/__init__.py +0 -15
- prefect/_vendor/fastapi/security/api_key.py +0 -98
- prefect/_vendor/fastapi/security/base.py +0 -6
- prefect/_vendor/fastapi/security/http.py +0 -172
- prefect/_vendor/fastapi/security/oauth2.py +0 -227
- prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
- prefect/_vendor/fastapi/security/utils.py +0 -10
- prefect/_vendor/fastapi/staticfiles.py +0 -1
- prefect/_vendor/fastapi/templating.py +0 -3
- prefect/_vendor/fastapi/testclient.py +0 -1
- prefect/_vendor/fastapi/types.py +0 -3
- prefect/_vendor/fastapi/utils.py +0 -235
- prefect/_vendor/fastapi/websockets.py +0 -7
- prefect/_vendor/starlette/__init__.py +0 -1
- prefect/_vendor/starlette/_compat.py +0 -28
- prefect/_vendor/starlette/_exception_handler.py +0 -80
- prefect/_vendor/starlette/_utils.py +0 -88
- prefect/_vendor/starlette/applications.py +0 -261
- prefect/_vendor/starlette/authentication.py +0 -159
- prefect/_vendor/starlette/background.py +0 -43
- prefect/_vendor/starlette/concurrency.py +0 -59
- prefect/_vendor/starlette/config.py +0 -151
- prefect/_vendor/starlette/convertors.py +0 -87
- prefect/_vendor/starlette/datastructures.py +0 -707
- prefect/_vendor/starlette/endpoints.py +0 -130
- prefect/_vendor/starlette/exceptions.py +0 -60
- prefect/_vendor/starlette/formparsers.py +0 -276
- prefect/_vendor/starlette/middleware/__init__.py +0 -17
- prefect/_vendor/starlette/middleware/authentication.py +0 -52
- prefect/_vendor/starlette/middleware/base.py +0 -220
- prefect/_vendor/starlette/middleware/cors.py +0 -176
- prefect/_vendor/starlette/middleware/errors.py +0 -265
- prefect/_vendor/starlette/middleware/exceptions.py +0 -74
- prefect/_vendor/starlette/middleware/gzip.py +0 -113
- prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
- prefect/_vendor/starlette/middleware/sessions.py +0 -82
- prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
- prefect/_vendor/starlette/middleware/wsgi.py +0 -147
- prefect/_vendor/starlette/py.typed +0 -0
- prefect/_vendor/starlette/requests.py +0 -328
- prefect/_vendor/starlette/responses.py +0 -347
- prefect/_vendor/starlette/routing.py +0 -933
- prefect/_vendor/starlette/schemas.py +0 -154
- prefect/_vendor/starlette/staticfiles.py +0 -248
- prefect/_vendor/starlette/status.py +0 -199
- prefect/_vendor/starlette/templating.py +0 -231
- prefect/_vendor/starlette/testclient.py +0 -804
- prefect/_vendor/starlette/types.py +0 -30
- prefect/_vendor/starlette/websockets.py +0 -193
- prefect/blocks/kubernetes.py +0 -119
- prefect/deprecated/__init__.py +0 -0
- prefect/deprecated/data_documents.py +0 -350
- prefect/deprecated/packaging/__init__.py +0 -12
- prefect/deprecated/packaging/base.py +0 -96
- prefect/deprecated/packaging/docker.py +0 -146
- prefect/deprecated/packaging/file.py +0 -92
- prefect/deprecated/packaging/orion.py +0 -80
- prefect/deprecated/packaging/serializers.py +0 -171
- prefect/events/instrument.py +0 -135
- prefect/infrastructure/container.py +0 -824
- prefect/infrastructure/kubernetes.py +0 -920
- prefect/infrastructure/process.py +0 -289
- prefect/manifests.py +0 -20
- prefect/new_flow_engine.py +0 -449
- prefect/new_task_engine.py +0 -423
- prefect/pydantic/__init__.py +0 -76
- prefect/pydantic/main.py +0 -39
- prefect/software/__init__.py +0 -2
- prefect/software/base.py +0 -50
- prefect/software/conda.py +0 -199
- prefect/software/pip.py +0 -122
- prefect/software/python.py +0 -52
- prefect/task_server.py +0 -322
- prefect_client-2.20.4.dist-info/RECORD +0 -294
- /prefect/{_internal/pydantic/utilities → client/types}/__init__.py +0 -0
- /prefect/{_vendor → concurrency/v1}/__init__.py +0 -0
- {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
- {prefect_client-2.20.4.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
|
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
|
-
@
|
53
|
-
def selected_deployment_requires_id(
|
54
|
-
wants_selected_deployment =
|
55
|
-
has_deployment_id = bool(
|
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
|
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
|
-
@
|
203
|
-
def selected_work_queue_requires_id(
|
204
|
-
wants_selected_work_queue =
|
205
|
-
has_work_queue_id = bool(
|
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
|
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
|
-
@
|
245
|
-
def selected_automation_requires_id(
|
246
|
-
wants_selected_automation =
|
247
|
-
has_automation_id = bool(
|
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
|
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="
|
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
|
164
|
+
automation = [no_really_json(a) for a in automation]
|
153
165
|
elif isinstance(automation, Automation):
|
154
|
-
automation = automation
|
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
|
-
|
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 =
|
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
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
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
|
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 =
|
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.
|
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 =
|
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
|
-
|
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.
|
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
|
-
|
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.
|
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
|
-
|
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[
|
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
|
44
|
+
_top_level_filter: Optional["EventFilter"] = PrivateAttr(None)
|
50
45
|
|
51
46
|
def get_filters(self) -> List["EventDataFilter"]:
|
52
|
-
|
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:
|
68
|
+
since: DateTime = Field(
|
73
69
|
default_factory=lambda: cast(
|
74
|
-
|
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:
|
80
|
-
default_factory=lambda: cast(
|
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
|
|