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
@@ -2,20 +2,13 @@ import datetime
|
|
2
2
|
from typing import Any, Dict, List, Optional, TypeVar, Union
|
3
3
|
from uuid import UUID
|
4
4
|
|
5
|
-
from
|
6
|
-
from
|
7
|
-
|
8
|
-
if HAS_PYDANTIC_V2:
|
9
|
-
from pydantic.v1 import Field
|
10
|
-
else:
|
11
|
-
from pydantic import Field
|
12
|
-
|
5
|
+
from pydantic import ConfigDict, Field
|
6
|
+
from pydantic_extra_types.pendulum_dt import DateTime
|
13
7
|
from typing_extensions import Literal
|
14
8
|
|
15
9
|
import prefect.client.schemas.objects as objects
|
16
10
|
from prefect._internal.schemas.bases import ObjectBaseModel, PrefectBaseModel
|
17
|
-
from prefect._internal.schemas.fields import CreatedBy,
|
18
|
-
from prefect.client.schemas.schedules import SCHEDULE_TYPES
|
11
|
+
from prefect._internal.schemas.fields import CreatedBy, UpdatedBy
|
19
12
|
from prefect.utilities.collections import AutoEnum
|
20
13
|
from prefect.utilities.names import generate_slug
|
21
14
|
|
@@ -120,10 +113,10 @@ class HistoryResponseState(PrefectBaseModel):
|
|
120
113
|
class HistoryResponse(PrefectBaseModel):
|
121
114
|
"""Represents a history of aggregation states over an interval"""
|
122
115
|
|
123
|
-
interval_start:
|
116
|
+
interval_start: DateTime = Field(
|
124
117
|
default=..., description="The start date of the interval."
|
125
118
|
)
|
126
|
-
interval_end:
|
119
|
+
interval_end: DateTime = Field(
|
127
120
|
default=..., description="The end date of the interval."
|
128
121
|
)
|
129
122
|
states: List[HistoryResponseState] = Field(
|
@@ -147,8 +140,7 @@ class OrchestrationResult(PrefectBaseModel):
|
|
147
140
|
|
148
141
|
|
149
142
|
class WorkerFlowRunResponse(PrefectBaseModel):
|
150
|
-
|
151
|
-
arbitrary_types_allowed = True
|
143
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
152
144
|
|
153
145
|
work_pool_id: UUID
|
154
146
|
work_queue_id: UUID
|
@@ -219,18 +211,18 @@ class FlowRunResponse(ObjectBaseModel):
|
|
219
211
|
run_count: int = Field(
|
220
212
|
default=0, description="The number of times the flow run was executed."
|
221
213
|
)
|
222
|
-
expected_start_time: Optional[
|
214
|
+
expected_start_time: Optional[DateTime] = Field(
|
223
215
|
default=None,
|
224
216
|
description="The flow run's expected start time.",
|
225
217
|
)
|
226
|
-
next_scheduled_start_time: Optional[
|
218
|
+
next_scheduled_start_time: Optional[DateTime] = Field(
|
227
219
|
default=None,
|
228
220
|
description="The next time the flow run is scheduled to start.",
|
229
221
|
)
|
230
|
-
start_time: Optional[
|
222
|
+
start_time: Optional[DateTime] = Field(
|
231
223
|
default=None, description="The actual start time."
|
232
224
|
)
|
233
|
-
end_time: Optional[
|
225
|
+
end_time: Optional[DateTime] = Field(
|
234
226
|
default=None, description="The actual end time."
|
235
227
|
)
|
236
228
|
total_run_time: datetime.timedelta = Field(
|
@@ -279,7 +271,7 @@ class FlowRunResponse(ObjectBaseModel):
|
|
279
271
|
state: Optional[objects.State] = Field(
|
280
272
|
default=None,
|
281
273
|
description="The state of the flow run.",
|
282
|
-
examples=[objects.State(type=objects.StateType.COMPLETED)],
|
274
|
+
examples=["objects.State(type=objects.StateType.COMPLETED)"],
|
283
275
|
)
|
284
276
|
job_variables: Optional[dict] = Field(
|
285
277
|
default=None, description="Job variables for the flow run."
|
@@ -304,13 +296,13 @@ class FlowRunResponse(ObjectBaseModel):
|
|
304
296
|
"""
|
305
297
|
if isinstance(other, objects.FlowRun):
|
306
298
|
exclude_fields = {"estimated_run_time", "estimated_start_time_delta"}
|
307
|
-
return self.
|
299
|
+
return self.model_dump(exclude=exclude_fields) == other.model_dump(
|
308
300
|
exclude=exclude_fields
|
309
301
|
)
|
310
302
|
return super().__eq__(other)
|
311
303
|
|
312
304
|
|
313
|
-
class DeploymentResponse(
|
305
|
+
class DeploymentResponse(ObjectBaseModel):
|
314
306
|
name: str = Field(default=..., description="The name of the deployment.")
|
315
307
|
version: Optional[str] = Field(
|
316
308
|
default=None, description="An optional version for the deployment."
|
@@ -321,11 +313,8 @@ class DeploymentResponse(DeprecatedInfraOverridesField, ObjectBaseModel):
|
|
321
313
|
flow_id: UUID = Field(
|
322
314
|
default=..., description="The flow id associated with the deployment."
|
323
315
|
)
|
324
|
-
|
325
|
-
default=None, description="
|
326
|
-
)
|
327
|
-
is_schedule_active: bool = Field(
|
328
|
-
default=True, description="Whether or not the deployment schedule is active."
|
316
|
+
concurrency_limit: Optional[int] = Field(
|
317
|
+
default=None, description="The concurrency limit for the deployment."
|
329
318
|
)
|
330
319
|
paused: bool = Field(
|
331
320
|
default=False, description="Whether or not the deployment is paused."
|
@@ -357,7 +346,7 @@ class DeploymentResponse(DeprecatedInfraOverridesField, ObjectBaseModel):
|
|
357
346
|
" be scheduled."
|
358
347
|
),
|
359
348
|
)
|
360
|
-
last_polled: Optional[
|
349
|
+
last_polled: Optional[DateTime] = Field(
|
361
350
|
default=None,
|
362
351
|
description="The last time the deployment was polled for status updates.",
|
363
352
|
)
|
@@ -378,12 +367,6 @@ class DeploymentResponse(DeprecatedInfraOverridesField, ObjectBaseModel):
|
|
378
367
|
"The path to the entrypoint for the workflow, relative to the `path`."
|
379
368
|
),
|
380
369
|
)
|
381
|
-
manifest_path: Optional[str] = Field(
|
382
|
-
default=None,
|
383
|
-
description=(
|
384
|
-
"The path to the flow's manifest file, relative to the chosen storage."
|
385
|
-
),
|
386
|
-
)
|
387
370
|
storage_document_id: Optional[UUID] = Field(
|
388
371
|
default=None,
|
389
372
|
description="The block document defining storage used for this flow.",
|
@@ -407,7 +390,7 @@ class DeploymentResponse(DeprecatedInfraOverridesField, ObjectBaseModel):
|
|
407
390
|
),
|
408
391
|
)
|
409
392
|
enforce_parameter_schema: bool = Field(
|
410
|
-
default=
|
393
|
+
default=True,
|
411
394
|
description=(
|
412
395
|
"Whether or not the deployment should enforce the parameter schema."
|
413
396
|
),
|
@@ -423,8 +406,7 @@ class DeploymentResponse(DeprecatedInfraOverridesField, ObjectBaseModel):
|
|
423
406
|
|
424
407
|
|
425
408
|
class MinimalConcurrencyLimitResponse(PrefectBaseModel):
|
426
|
-
|
427
|
-
extra = "ignore" # 2024/4/1
|
409
|
+
model_config = ConfigDict(extra="ignore")
|
428
410
|
|
429
411
|
id: UUID
|
430
412
|
name: str
|
@@ -3,28 +3,22 @@ Schedule schemas
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import datetime
|
6
|
-
from typing import Optional, Union
|
6
|
+
from typing import Annotated, Any, Optional, Union
|
7
7
|
|
8
8
|
import dateutil
|
9
9
|
import dateutil.rrule
|
10
10
|
import pendulum
|
11
|
+
from pydantic import AfterValidator, ConfigDict, Field, field_validator, model_validator
|
12
|
+
from pydantic_extra_types.pendulum_dt import DateTime
|
13
|
+
from typing_extensions import TypeAlias, TypeGuard
|
11
14
|
|
12
|
-
from prefect._internal.pydantic import HAS_PYDANTIC_V2
|
13
15
|
from prefect._internal.schemas.bases import PrefectBaseModel
|
14
|
-
from prefect._internal.schemas.fields import DateTimeTZ
|
15
16
|
from prefect._internal.schemas.validators import (
|
16
17
|
default_anchor_date,
|
17
18
|
default_timezone,
|
18
19
|
validate_cron_string,
|
19
20
|
validate_rrule_string,
|
20
|
-
validate_rrule_timezone,
|
21
21
|
)
|
22
|
-
from prefect.types import PositiveDuration
|
23
|
-
|
24
|
-
if HAS_PYDANTIC_V2:
|
25
|
-
from pydantic.v1 import Field, validator
|
26
|
-
else:
|
27
|
-
from pydantic import Field, validator
|
28
22
|
|
29
23
|
MAX_ITERATIONS = 1000
|
30
24
|
# approx. 1 years worth of RDATEs + buffer
|
@@ -55,26 +49,24 @@ class IntervalSchedule(PrefectBaseModel):
|
|
55
49
|
|
56
50
|
Args:
|
57
51
|
interval (datetime.timedelta): an interval to schedule on
|
58
|
-
anchor_date (
|
52
|
+
anchor_date (DateTime, optional): an anchor date to schedule increments against;
|
59
53
|
if not provided, the current timestamp will be used
|
60
54
|
timezone (str, optional): a valid timezone string
|
61
55
|
"""
|
62
56
|
|
63
|
-
|
64
|
-
extra = "forbid"
|
65
|
-
exclude_none = True
|
57
|
+
model_config = ConfigDict(extra="forbid", exclude_none=True)
|
66
58
|
|
67
|
-
interval:
|
68
|
-
anchor_date:
|
59
|
+
interval: datetime.timedelta = Field(gt=datetime.timedelta(0))
|
60
|
+
anchor_date: Annotated[DateTime, AfterValidator(default_anchor_date)] = Field(
|
61
|
+
default_factory=lambda: pendulum.now("UTC"),
|
62
|
+
examples=["2020-01-01T00:00:00Z"],
|
63
|
+
)
|
69
64
|
timezone: Optional[str] = Field(default=None, examples=["America/New_York"])
|
70
65
|
|
71
|
-
@
|
72
|
-
def
|
73
|
-
|
74
|
-
|
75
|
-
@validator("timezone", always=True)
|
76
|
-
def validate_default_timezone(cls, v, values):
|
77
|
-
return default_timezone(v, values=values)
|
66
|
+
@model_validator(mode="after")
|
67
|
+
def validate_timezone(self):
|
68
|
+
self.timezone = default_timezone(self.timezone, self.model_dump())
|
69
|
+
return self
|
78
70
|
|
79
71
|
|
80
72
|
class CronSchedule(PrefectBaseModel):
|
@@ -102,8 +94,7 @@ class CronSchedule(PrefectBaseModel):
|
|
102
94
|
|
103
95
|
"""
|
104
96
|
|
105
|
-
|
106
|
-
extra = "forbid"
|
97
|
+
model_config = ConfigDict(extra="forbid")
|
107
98
|
|
108
99
|
cron: str = Field(default=..., examples=["0 0 * * *"])
|
109
100
|
timezone: Optional[str] = Field(default=None, examples=["America/New_York"])
|
@@ -114,11 +105,13 @@ class CronSchedule(PrefectBaseModel):
|
|
114
105
|
),
|
115
106
|
)
|
116
107
|
|
117
|
-
@
|
108
|
+
@field_validator("timezone")
|
109
|
+
@classmethod
|
118
110
|
def valid_timezone(cls, v):
|
119
111
|
return default_timezone(v)
|
120
112
|
|
121
|
-
@
|
113
|
+
@field_validator("cron")
|
114
|
+
@classmethod
|
122
115
|
def valid_cron_string(cls, v):
|
123
116
|
return validate_cron_string(v)
|
124
117
|
|
@@ -146,13 +139,15 @@ class RRuleSchedule(PrefectBaseModel):
|
|
146
139
|
timezone (str, optional): a valid timezone string
|
147
140
|
"""
|
148
141
|
|
149
|
-
|
150
|
-
extra = "forbid"
|
142
|
+
model_config = ConfigDict(extra="forbid")
|
151
143
|
|
152
144
|
rrule: str
|
153
|
-
timezone: Optional[str] = Field(
|
145
|
+
timezone: Optional[str] = Field(
|
146
|
+
default="UTC", examples=["America/New_York"], validate_default=True
|
147
|
+
)
|
154
148
|
|
155
|
-
@
|
149
|
+
@field_validator("rrule")
|
150
|
+
@classmethod
|
156
151
|
def validate_rrule_str(cls, v):
|
157
152
|
return validate_rrule_string(v)
|
158
153
|
|
@@ -259,17 +254,39 @@ class RRuleSchedule(PrefectBaseModel):
|
|
259
254
|
|
260
255
|
return rrule
|
261
256
|
|
262
|
-
@
|
257
|
+
@field_validator("timezone")
|
263
258
|
def valid_timezone(cls, v):
|
264
|
-
|
259
|
+
"""
|
260
|
+
Validate that the provided timezone is a valid IANA timezone.
|
261
|
+
|
262
|
+
Unfortunately this list is slightly different from the list of valid
|
263
|
+
timezones in pendulum that we use for cron and interval timezone validation.
|
264
|
+
"""
|
265
|
+
from prefect._internal.pytz import HAS_PYTZ
|
266
|
+
|
267
|
+
if HAS_PYTZ:
|
268
|
+
import pytz
|
269
|
+
else:
|
270
|
+
from prefect._internal import pytz
|
271
|
+
|
272
|
+
if v and v not in pytz.all_timezones_set:
|
273
|
+
raise ValueError(f'Invalid timezone: "{v}"')
|
274
|
+
elif v is None:
|
275
|
+
return "UTC"
|
276
|
+
return v
|
265
277
|
|
266
278
|
|
267
279
|
class NoSchedule(PrefectBaseModel):
|
268
|
-
|
269
|
-
|
280
|
+
model_config = ConfigDict(extra="forbid")
|
281
|
+
|
282
|
+
|
283
|
+
SCHEDULE_TYPES: TypeAlias = Union[
|
284
|
+
IntervalSchedule, CronSchedule, RRuleSchedule, NoSchedule
|
285
|
+
]
|
270
286
|
|
271
287
|
|
272
|
-
|
288
|
+
def is_schedule_type(obj: Any) -> TypeGuard[SCHEDULE_TYPES]:
|
289
|
+
return isinstance(obj, (IntervalSchedule, CronSchedule, RRuleSchedule, NoSchedule))
|
273
290
|
|
274
291
|
|
275
292
|
def construct_schedule(
|
@@ -308,6 +325,8 @@ def construct_schedule(
|
|
308
325
|
if interval:
|
309
326
|
if isinstance(interval, (int, float)):
|
310
327
|
interval = datetime.timedelta(seconds=interval)
|
328
|
+
if not anchor_date:
|
329
|
+
anchor_date = DateTime.now()
|
311
330
|
schedule = IntervalSchedule(
|
312
331
|
interval=interval, anchor_date=anchor_date, timezone=timezone
|
313
332
|
)
|
prefect/client/subscriptions.py
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
import asyncio
|
2
|
-
from typing import Any, Dict, Generic,
|
2
|
+
from typing import Any, Dict, Generic, Iterable, Optional, Type, TypeVar
|
3
3
|
|
4
4
|
import orjson
|
5
5
|
import websockets
|
6
6
|
import websockets.exceptions
|
7
|
-
from
|
7
|
+
from starlette.status import WS_1008_POLICY_VIOLATION
|
8
8
|
from typing_extensions import Self
|
9
9
|
|
10
10
|
from prefect._internal.schemas.bases import IDBaseModel
|
11
11
|
from prefect.logging import get_logger
|
12
|
-
from prefect.settings import PREFECT_API_KEY
|
12
|
+
from prefect.settings import PREFECT_API_KEY
|
13
13
|
|
14
14
|
logger = get_logger(__name__)
|
15
15
|
|
@@ -21,15 +21,16 @@ class Subscription(Generic[S]):
|
|
21
21
|
self,
|
22
22
|
model: Type[S],
|
23
23
|
path: str,
|
24
|
-
keys:
|
24
|
+
keys: Iterable[str],
|
25
25
|
client_id: Optional[str] = None,
|
26
|
+
base_url: Optional[str] = None,
|
26
27
|
):
|
27
28
|
self.model = model
|
28
29
|
self.client_id = client_id
|
29
|
-
base_url =
|
30
|
+
base_url = base_url.replace("http", "ws", 1)
|
30
31
|
self.subscription_url = f"{base_url}{path}"
|
31
32
|
|
32
|
-
self.keys = keys
|
33
|
+
self.keys = list(keys)
|
33
34
|
|
34
35
|
self._connect = websockets.connect(
|
35
36
|
self.subscription_url,
|
@@ -48,7 +49,7 @@ class Subscription(Generic[S]):
|
|
48
49
|
|
49
50
|
await self._websocket.send(orjson.dumps({"type": "ack"}).decode())
|
50
51
|
|
51
|
-
return self.model.
|
52
|
+
return self.model.model_validate_json(message)
|
52
53
|
except (
|
53
54
|
ConnectionRefusedError,
|
54
55
|
websockets.exceptions.ConnectionClosedError,
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Sequence, Union
|
2
|
+
|
3
|
+
from typing_extensions import TypeAlias
|
4
|
+
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
from prefect.client.schemas.actions import DeploymentScheduleCreate
|
7
|
+
from prefect.client.schemas.schedules import SCHEDULE_TYPES
|
8
|
+
|
9
|
+
FlexibleScheduleList: TypeAlias = Sequence[
|
10
|
+
Union[DeploymentScheduleCreate, dict[str, Any], "SCHEDULE_TYPES"]
|
11
|
+
]
|
prefect/client/utilities.py
CHANGED
@@ -42,12 +42,15 @@ def get_or_create_client(
|
|
42
42
|
if client is not None:
|
43
43
|
return client, True
|
44
44
|
from prefect._internal.concurrency.event_loop import get_running_loop
|
45
|
-
from prefect.context import FlowRunContext, TaskRunContext
|
45
|
+
from prefect.context import AsyncClientContext, FlowRunContext, TaskRunContext
|
46
46
|
|
47
|
+
async_client_context = AsyncClientContext.get()
|
47
48
|
flow_run_context = FlowRunContext.get()
|
48
49
|
task_run_context = TaskRunContext.get()
|
49
50
|
|
50
|
-
if (
|
51
|
+
if async_client_context and async_client_context.client._loop == get_running_loop():
|
52
|
+
return async_client_context.client, True
|
53
|
+
elif (
|
51
54
|
flow_run_context
|
52
55
|
and getattr(flow_run_context.client, "_loop", None) == get_running_loop()
|
53
56
|
):
|
@@ -75,10 +78,10 @@ def client_injector(
|
|
75
78
|
|
76
79
|
|
77
80
|
def inject_client(
|
78
|
-
fn: Callable[P, Coroutine[Any, Any,
|
79
|
-
) -> Callable[P, Coroutine[Any, Any,
|
81
|
+
fn: Callable[P, Coroutine[Any, Any, R]],
|
82
|
+
) -> Callable[P, Coroutine[Any, Any, R]]:
|
80
83
|
"""
|
81
|
-
Simple helper to provide a context managed client to
|
84
|
+
Simple helper to provide a context managed client to an asynchronous function.
|
82
85
|
|
83
86
|
The decorated function _must_ take a `client` kwarg and if a client is passed when
|
84
87
|
called it will be used instead of creating a new one, but it will not be context
|
@@ -86,7 +89,7 @@ def inject_client(
|
|
86
89
|
"""
|
87
90
|
|
88
91
|
@wraps(fn)
|
89
|
-
async def with_injected_client(*args: P.args, **kwargs: P.kwargs) ->
|
92
|
+
async def with_injected_client(*args: P.args, **kwargs: P.kwargs) -> R:
|
90
93
|
client = cast(Optional["PrefectClient"], kwargs.pop("client", None))
|
91
94
|
client, inferred = get_or_create_client(client)
|
92
95
|
if not inferred:
|
prefect/concurrency/asyncio.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
import asyncio
|
2
2
|
from contextlib import asynccontextmanager
|
3
|
-
from typing import List, Literal, Optional, Union, cast
|
3
|
+
from typing import AsyncGenerator, List, Literal, Optional, Union, cast
|
4
4
|
|
5
|
+
import anyio
|
5
6
|
import httpx
|
6
7
|
import pendulum
|
7
8
|
|
@@ -11,9 +12,10 @@ except ImportError:
|
|
11
12
|
# pendulum < 3
|
12
13
|
from pendulum.period import Period as Interval # type: ignore
|
13
14
|
|
14
|
-
from prefect import get_client
|
15
|
+
from prefect.client.orchestration import get_client
|
15
16
|
from prefect.client.schemas.responses import MinimalConcurrencyLimitResponse
|
16
17
|
|
18
|
+
from .context import ConcurrencyContext
|
17
19
|
from .events import (
|
18
20
|
_emit_concurrency_acquisition_events,
|
19
21
|
_emit_concurrency_release_events,
|
@@ -34,7 +36,9 @@ async def concurrency(
|
|
34
36
|
names: Union[str, List[str]],
|
35
37
|
occupy: int = 1,
|
36
38
|
timeout_seconds: Optional[float] = None,
|
37
|
-
|
39
|
+
create_if_missing: bool = True,
|
40
|
+
max_retries: Optional[int] = None,
|
41
|
+
) -> AsyncGenerator[None, None]:
|
38
42
|
"""A context manager that acquires and releases concurrency slots from the
|
39
43
|
given concurrency limits.
|
40
44
|
|
@@ -43,6 +47,8 @@ async def concurrency(
|
|
43
47
|
occupy: The number of slots to acquire and hold from each limit.
|
44
48
|
timeout_seconds: The number of seconds to wait for the slots to be acquired before
|
45
49
|
raising a `TimeoutError`. A timeout of `None` will wait indefinitely.
|
50
|
+
create_if_missing: Whether to create the concurrency limits if they do not exist.
|
51
|
+
max_retries: The maximum number of retries to acquire the concurrency slots.
|
46
52
|
|
47
53
|
Raises:
|
48
54
|
TimeoutError: If the slots are not acquired within the given timeout.
|
@@ -60,9 +66,18 @@ async def concurrency(
|
|
60
66
|
await resource_heavy()
|
61
67
|
```
|
62
68
|
"""
|
69
|
+
if not names:
|
70
|
+
yield
|
71
|
+
return
|
72
|
+
|
63
73
|
names = names if isinstance(names, list) else [names]
|
74
|
+
|
64
75
|
limits = await _acquire_concurrency_slots(
|
65
|
-
names,
|
76
|
+
names,
|
77
|
+
occupy,
|
78
|
+
timeout_seconds=timeout_seconds,
|
79
|
+
create_if_missing=create_if_missing,
|
80
|
+
max_retries=max_retries,
|
66
81
|
)
|
67
82
|
acquisition_time = pendulum.now("UTC")
|
68
83
|
emitted_events = _emit_concurrency_acquisition_events(limits, occupy)
|
@@ -71,13 +86,28 @@ async def concurrency(
|
|
71
86
|
yield
|
72
87
|
finally:
|
73
88
|
occupancy_period = cast(Interval, (pendulum.now("UTC") - acquisition_time))
|
74
|
-
|
75
|
-
|
76
|
-
|
89
|
+
try:
|
90
|
+
await _release_concurrency_slots(
|
91
|
+
names, occupy, occupancy_period.total_seconds()
|
92
|
+
)
|
93
|
+
except anyio.get_cancelled_exc_class():
|
94
|
+
# The task was cancelled before it could release the slots. Add the
|
95
|
+
# slots to the cleanup list so they can be released when the
|
96
|
+
# concurrency context is exited.
|
97
|
+
if ctx := ConcurrencyContext.get():
|
98
|
+
ctx.cleanup_slots.append(
|
99
|
+
(names, occupy, occupancy_period.total_seconds())
|
100
|
+
)
|
101
|
+
|
77
102
|
_emit_concurrency_release_events(limits, occupy, emitted_events)
|
78
103
|
|
79
104
|
|
80
|
-
async def rate_limit(
|
105
|
+
async def rate_limit(
|
106
|
+
names: Union[str, List[str]],
|
107
|
+
occupy: int = 1,
|
108
|
+
timeout_seconds: Optional[float] = None,
|
109
|
+
create_if_missing: Optional[bool] = True,
|
110
|
+
) -> None:
|
81
111
|
"""Block execution until an `occupy` number of slots of the concurrency
|
82
112
|
limits given in `names` are acquired. Requires that all given concurrency
|
83
113
|
limits have a slot decay.
|
@@ -85,9 +115,22 @@ async def rate_limit(names: Union[str, List[str]], occupy: int = 1):
|
|
85
115
|
Args:
|
86
116
|
names: The names of the concurrency limits to acquire slots from.
|
87
117
|
occupy: The number of slots to acquire and hold from each limit.
|
118
|
+
timeout_seconds: The number of seconds to wait for the slots to be acquired before
|
119
|
+
raising a `TimeoutError`. A timeout of `None` will wait indefinitely.
|
120
|
+
create_if_missing: Whether to create the concurrency limits if they do not exist.
|
88
121
|
"""
|
122
|
+
if not names:
|
123
|
+
return
|
124
|
+
|
89
125
|
names = names if isinstance(names, list) else [names]
|
90
|
-
|
126
|
+
|
127
|
+
limits = await _acquire_concurrency_slots(
|
128
|
+
names,
|
129
|
+
occupy,
|
130
|
+
mode="rate_limit",
|
131
|
+
timeout_seconds=timeout_seconds,
|
132
|
+
create_if_missing=create_if_missing,
|
133
|
+
)
|
91
134
|
_emit_concurrency_acquisition_events(limits, occupy)
|
92
135
|
|
93
136
|
|
@@ -96,9 +139,13 @@ async def _acquire_concurrency_slots(
|
|
96
139
|
slots: int,
|
97
140
|
mode: Union[Literal["concurrency"], Literal["rate_limit"]] = "concurrency",
|
98
141
|
timeout_seconds: Optional[float] = None,
|
142
|
+
create_if_missing: Optional[bool] = True,
|
143
|
+
max_retries: Optional[int] = None,
|
99
144
|
) -> List[MinimalConcurrencyLimitResponse]:
|
100
145
|
service = ConcurrencySlotAcquisitionService.instance(frozenset(names))
|
101
|
-
future = service.send(
|
146
|
+
future = service.send(
|
147
|
+
(slots, mode, timeout_seconds, create_if_missing, max_retries)
|
148
|
+
)
|
102
149
|
response_or_exception = await asyncio.wrap_future(future)
|
103
150
|
|
104
151
|
if isinstance(response_or_exception, Exception):
|
@@ -127,4 +174,6 @@ async def _release_concurrency_slots(
|
|
127
174
|
def _response_to_minimal_concurrency_limit_response(
|
128
175
|
response: httpx.Response,
|
129
176
|
) -> List[MinimalConcurrencyLimitResponse]:
|
130
|
-
return [
|
177
|
+
return [
|
178
|
+
MinimalConcurrencyLimitResponse.model_validate(obj_) for obj_ in response.json()
|
179
|
+
]
|
@@ -0,0 +1,24 @@
|
|
1
|
+
from contextvars import ContextVar
|
2
|
+
from typing import List, Tuple
|
3
|
+
|
4
|
+
from prefect.client.orchestration import get_client
|
5
|
+
from prefect.context import ContextModel, Field
|
6
|
+
|
7
|
+
|
8
|
+
class ConcurrencyContext(ContextModel):
|
9
|
+
__var__: ContextVar = ContextVar("concurrency")
|
10
|
+
|
11
|
+
# Track the slots that have been acquired but were not able to be released
|
12
|
+
# due to cancellation or some other error. These slots are released when
|
13
|
+
# the context manager exits.
|
14
|
+
cleanup_slots: List[Tuple[List[str], int, float]] = Field(default_factory=list)
|
15
|
+
|
16
|
+
def __exit__(self, *exc_info):
|
17
|
+
if self.cleanup_slots:
|
18
|
+
with get_client(sync_client=True) as client:
|
19
|
+
for names, occupy, occupancy_seconds in self.cleanup_slots:
|
20
|
+
client.release_concurrency_slots(
|
21
|
+
names=names, slots=occupy, occupancy_seconds=occupancy_seconds
|
22
|
+
)
|
23
|
+
|
24
|
+
return super().__exit__(*exc_info)
|
prefect/concurrency/events.py
CHANGED
@@ -20,7 +20,7 @@ def _emit_concurrency_event(
|
|
20
20
|
}
|
21
21
|
|
22
22
|
related = [
|
23
|
-
RelatedResource.
|
23
|
+
RelatedResource.model_validate(
|
24
24
|
{
|
25
25
|
"prefect.resource.id": f"prefect.concurrency-limit.{limit.id}",
|
26
26
|
"prefect.resource.role": "concurrency-limit",
|
@@ -54,6 +54,6 @@ def _emit_concurrency_release_events(
|
|
54
54
|
limits: List[MinimalConcurrencyLimitResponse],
|
55
55
|
occupy: int,
|
56
56
|
events: Dict[UUID, Optional[Event]],
|
57
|
-
):
|
57
|
+
) -> None:
|
58
58
|
for limit in limits:
|
59
59
|
_emit_concurrency_event("released", limit, limits, occupy, events[limit.id])
|