prefect-client 3.1.5__py3-none-any.whl → 3.1.7__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 +3 -0
- prefect/_experimental/__init__.py +0 -0
- prefect/_experimental/lineage.py +181 -0
- prefect/_internal/compatibility/async_dispatch.py +38 -9
- prefect/_internal/compatibility/migration.py +1 -1
- prefect/_internal/concurrency/api.py +52 -52
- prefect/_internal/concurrency/calls.py +59 -35
- prefect/_internal/concurrency/cancellation.py +34 -18
- prefect/_internal/concurrency/event_loop.py +7 -6
- prefect/_internal/concurrency/threads.py +41 -33
- prefect/_internal/concurrency/waiters.py +28 -21
- prefect/_internal/pydantic/v1_schema.py +2 -2
- prefect/_internal/pydantic/v2_schema.py +10 -9
- prefect/_internal/pydantic/v2_validated_func.py +15 -10
- prefect/_internal/retries.py +15 -6
- prefect/_internal/schemas/bases.py +11 -8
- prefect/_internal/schemas/validators.py +7 -5
- prefect/_version.py +3 -3
- prefect/automations.py +53 -47
- prefect/blocks/abstract.py +12 -10
- prefect/blocks/core.py +148 -19
- prefect/blocks/system.py +2 -1
- prefect/cache_policies.py +11 -11
- prefect/client/__init__.py +3 -1
- prefect/client/base.py +36 -37
- prefect/client/cloud.py +26 -19
- prefect/client/collections.py +2 -2
- prefect/client/orchestration.py +430 -273
- prefect/client/schemas/__init__.py +24 -0
- prefect/client/schemas/actions.py +128 -121
- prefect/client/schemas/filters.py +1 -1
- prefect/client/schemas/objects.py +114 -85
- prefect/client/schemas/responses.py +19 -20
- prefect/client/schemas/schedules.py +136 -93
- prefect/client/subscriptions.py +30 -15
- prefect/client/utilities.py +46 -36
- prefect/concurrency/asyncio.py +6 -9
- prefect/concurrency/sync.py +35 -5
- prefect/context.py +40 -32
- prefect/deployments/flow_runs.py +6 -8
- prefect/deployments/runner.py +14 -14
- prefect/deployments/steps/core.py +3 -1
- prefect/deployments/steps/pull.py +60 -12
- prefect/docker/__init__.py +1 -1
- prefect/events/clients.py +55 -4
- prefect/events/filters.py +1 -1
- prefect/events/related.py +2 -1
- prefect/events/schemas/events.py +26 -21
- prefect/events/utilities.py +3 -2
- prefect/events/worker.py +8 -0
- prefect/filesystems.py +3 -3
- prefect/flow_engine.py +87 -87
- prefect/flow_runs.py +7 -5
- prefect/flows.py +218 -176
- prefect/logging/configuration.py +1 -1
- prefect/logging/highlighters.py +1 -2
- prefect/logging/loggers.py +30 -20
- prefect/main.py +17 -24
- prefect/results.py +43 -22
- prefect/runner/runner.py +43 -21
- prefect/runner/server.py +30 -32
- prefect/runner/storage.py +3 -3
- prefect/runner/submit.py +3 -6
- prefect/runner/utils.py +6 -6
- prefect/runtime/flow_run.py +7 -0
- prefect/serializers.py +28 -24
- prefect/settings/constants.py +2 -2
- prefect/settings/legacy.py +1 -1
- prefect/settings/models/experiments.py +5 -0
- prefect/settings/models/server/events.py +10 -0
- prefect/task_engine.py +87 -26
- prefect/task_runners.py +2 -2
- prefect/task_worker.py +43 -25
- prefect/tasks.py +148 -142
- prefect/telemetry/bootstrap.py +15 -2
- prefect/telemetry/instrumentation.py +1 -1
- prefect/telemetry/processors.py +10 -7
- prefect/telemetry/run_telemetry.py +231 -0
- prefect/transactions.py +14 -14
- prefect/types/__init__.py +5 -5
- prefect/utilities/_engine.py +96 -0
- prefect/utilities/annotations.py +25 -18
- prefect/utilities/asyncutils.py +126 -140
- prefect/utilities/callables.py +87 -78
- prefect/utilities/collections.py +278 -117
- prefect/utilities/compat.py +13 -21
- prefect/utilities/context.py +6 -5
- prefect/utilities/dispatch.py +23 -12
- prefect/utilities/dockerutils.py +33 -32
- prefect/utilities/engine.py +126 -239
- prefect/utilities/filesystem.py +18 -15
- prefect/utilities/hashing.py +10 -11
- prefect/utilities/importtools.py +40 -27
- prefect/utilities/math.py +9 -5
- prefect/utilities/names.py +3 -3
- prefect/utilities/processutils.py +121 -57
- prefect/utilities/pydantic.py +41 -36
- prefect/utilities/render_swagger.py +22 -12
- prefect/utilities/schema_tools/__init__.py +2 -1
- prefect/utilities/schema_tools/hydration.py +50 -43
- prefect/utilities/schema_tools/validation.py +52 -42
- prefect/utilities/services.py +13 -12
- prefect/utilities/templating.py +45 -45
- prefect/utilities/text.py +2 -1
- prefect/utilities/timeout.py +4 -4
- prefect/utilities/urls.py +9 -4
- prefect/utilities/visualization.py +46 -24
- prefect/variables.py +136 -27
- prefect/workers/base.py +15 -8
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/METADATA +5 -2
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/RECORD +114 -110
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/LICENSE +0 -0
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/WHEEL +0 -0
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/top_level.txt +0 -0
@@ -1,19 +1,18 @@
|
|
1
1
|
import datetime
|
2
|
-
from typing import Any,
|
2
|
+
from typing import Any, ClassVar, Generic, Optional, TypeVar, Union
|
3
3
|
from uuid import UUID
|
4
4
|
|
5
5
|
from pydantic import ConfigDict, Field
|
6
|
-
from pydantic_extra_types.pendulum_dt import DateTime
|
7
6
|
from typing_extensions import Literal
|
8
7
|
|
9
8
|
import prefect.client.schemas.objects as objects
|
10
9
|
from prefect._internal.schemas.bases import ObjectBaseModel, PrefectBaseModel
|
11
10
|
from prefect._internal.schemas.fields import CreatedBy, UpdatedBy
|
12
|
-
from prefect.types import KeyValueLabelsField
|
11
|
+
from prefect.types import DateTime, KeyValueLabelsField
|
13
12
|
from prefect.utilities.collections import AutoEnum
|
14
13
|
from prefect.utilities.names import generate_slug
|
15
14
|
|
16
|
-
|
15
|
+
T = TypeVar("T")
|
17
16
|
|
18
17
|
|
19
18
|
class SetStateStatus(AutoEnum):
|
@@ -120,7 +119,7 @@ class HistoryResponse(PrefectBaseModel):
|
|
120
119
|
interval_end: DateTime = Field(
|
121
120
|
default=..., description="The end date of the interval."
|
122
121
|
)
|
123
|
-
states:
|
122
|
+
states: list[HistoryResponseState] = Field(
|
124
123
|
default=..., description="A list of state histories during the interval."
|
125
124
|
)
|
126
125
|
|
@@ -130,18 +129,18 @@ StateResponseDetails = Union[
|
|
130
129
|
]
|
131
130
|
|
132
131
|
|
133
|
-
class OrchestrationResult(PrefectBaseModel):
|
132
|
+
class OrchestrationResult(PrefectBaseModel, Generic[T]):
|
134
133
|
"""
|
135
134
|
A container for the output of state orchestration.
|
136
135
|
"""
|
137
136
|
|
138
|
-
state: Optional[objects.State]
|
137
|
+
state: Optional[objects.State[T]]
|
139
138
|
status: SetStateStatus
|
140
139
|
details: StateResponseDetails
|
141
140
|
|
142
141
|
|
143
142
|
class WorkerFlowRunResponse(PrefectBaseModel):
|
144
|
-
model_config = ConfigDict(arbitrary_types_allowed=True)
|
143
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True)
|
145
144
|
|
146
145
|
work_pool_id: UUID
|
147
146
|
work_queue_id: UUID
|
@@ -179,7 +178,7 @@ class FlowRunResponse(ObjectBaseModel):
|
|
179
178
|
description="The version of the flow executed in this flow run.",
|
180
179
|
examples=["1.0"],
|
181
180
|
)
|
182
|
-
parameters:
|
181
|
+
parameters: dict[str, Any] = Field(
|
183
182
|
default_factory=dict, description="Parameters for the flow run."
|
184
183
|
)
|
185
184
|
idempotency_key: Optional[str] = Field(
|
@@ -189,7 +188,7 @@ class FlowRunResponse(ObjectBaseModel):
|
|
189
188
|
" run is not created multiple times."
|
190
189
|
),
|
191
190
|
)
|
192
|
-
context:
|
191
|
+
context: dict[str, Any] = Field(
|
193
192
|
default_factory=dict,
|
194
193
|
description="Additional context for the flow run.",
|
195
194
|
examples=[{"my_var": "my_val"}],
|
@@ -197,7 +196,7 @@ class FlowRunResponse(ObjectBaseModel):
|
|
197
196
|
empirical_policy: objects.FlowRunPolicy = Field(
|
198
197
|
default_factory=objects.FlowRunPolicy,
|
199
198
|
)
|
200
|
-
tags:
|
199
|
+
tags: list[str] = Field(
|
201
200
|
default_factory=list,
|
202
201
|
description="A list of tags on the flow run",
|
203
202
|
examples=[["tag-1", "tag-2"]],
|
@@ -275,7 +274,7 @@ class FlowRunResponse(ObjectBaseModel):
|
|
275
274
|
description="The state of the flow run.",
|
276
275
|
examples=["objects.State(type=objects.StateType.COMPLETED)"],
|
277
276
|
)
|
278
|
-
job_variables: Optional[dict] = Field(
|
277
|
+
job_variables: Optional[dict[str, Any]] = Field(
|
279
278
|
default=None, description="Job variables for the flow run."
|
280
279
|
)
|
281
280
|
|
@@ -335,22 +334,22 @@ class DeploymentResponse(ObjectBaseModel):
|
|
335
334
|
default=None,
|
336
335
|
description="The concurrency options for the deployment.",
|
337
336
|
)
|
338
|
-
schedules:
|
337
|
+
schedules: list[objects.DeploymentSchedule] = Field(
|
339
338
|
default_factory=list, description="A list of schedules for the deployment."
|
340
339
|
)
|
341
|
-
job_variables:
|
340
|
+
job_variables: dict[str, Any] = Field(
|
342
341
|
default_factory=dict,
|
343
342
|
description="Overrides to apply to flow run infrastructure at runtime.",
|
344
343
|
)
|
345
|
-
parameters:
|
344
|
+
parameters: dict[str, Any] = Field(
|
346
345
|
default_factory=dict,
|
347
346
|
description="Parameters for flow runs scheduled by the deployment.",
|
348
347
|
)
|
349
|
-
pull_steps: Optional[
|
348
|
+
pull_steps: Optional[list[dict[str, Any]]] = Field(
|
350
349
|
default=None,
|
351
350
|
description="Pull steps for cloning and running this deployment.",
|
352
351
|
)
|
353
|
-
tags:
|
352
|
+
tags: list[str] = Field(
|
354
353
|
default_factory=list,
|
355
354
|
description="A list of tags for the deployment",
|
356
355
|
examples=[["tag-1", "tag-2"]],
|
@@ -367,7 +366,7 @@ class DeploymentResponse(ObjectBaseModel):
|
|
367
366
|
default=None,
|
368
367
|
description="The last time the deployment was polled for status updates.",
|
369
368
|
)
|
370
|
-
parameter_openapi_schema: Optional[
|
369
|
+
parameter_openapi_schema: Optional[dict[str, Any]] = Field(
|
371
370
|
default=None,
|
372
371
|
description="The parameter schema of the flow, including defaults.",
|
373
372
|
)
|
@@ -400,7 +399,7 @@ class DeploymentResponse(ObjectBaseModel):
|
|
400
399
|
default=None,
|
401
400
|
description="Optional information about the updater of this deployment.",
|
402
401
|
)
|
403
|
-
work_queue_id: UUID = Field(
|
402
|
+
work_queue_id: Optional[UUID] = Field(
|
404
403
|
default=None,
|
405
404
|
description=(
|
406
405
|
"The id of the work pool queue to which this deployment is assigned."
|
@@ -423,7 +422,7 @@ class DeploymentResponse(ObjectBaseModel):
|
|
423
422
|
|
424
423
|
|
425
424
|
class MinimalConcurrencyLimitResponse(PrefectBaseModel):
|
426
|
-
model_config = ConfigDict(extra="ignore")
|
425
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(extra="ignore")
|
427
426
|
|
428
427
|
id: UUID
|
429
428
|
name: str
|
@@ -3,13 +3,13 @@ Schedule schemas
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import datetime
|
6
|
-
from typing import Annotated, Any, Optional, Union
|
6
|
+
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Optional, Union
|
7
7
|
|
8
8
|
import dateutil
|
9
9
|
import dateutil.rrule
|
10
|
+
import dateutil.tz
|
10
11
|
import pendulum
|
11
12
|
from pydantic import AfterValidator, ConfigDict, Field, field_validator, model_validator
|
12
|
-
from pydantic_extra_types.pendulum_dt import DateTime
|
13
13
|
from typing_extensions import TypeAlias, TypeGuard
|
14
14
|
|
15
15
|
from prefect._internal.schemas.bases import PrefectBaseModel
|
@@ -20,6 +20,14 @@ from prefect._internal.schemas.validators import (
|
|
20
20
|
validate_rrule_string,
|
21
21
|
)
|
22
22
|
|
23
|
+
if TYPE_CHECKING:
|
24
|
+
# type checkers have difficulty accepting that
|
25
|
+
# pydantic_extra_types.pendulum_dt and pendulum.DateTime can be used
|
26
|
+
# together.
|
27
|
+
DateTime = pendulum.DateTime
|
28
|
+
else:
|
29
|
+
from prefect.types import DateTime
|
30
|
+
|
23
31
|
MAX_ITERATIONS = 1000
|
24
32
|
# approx. 1 years worth of RDATEs + buffer
|
25
33
|
MAX_RRULE_LENGTH = 6500
|
@@ -54,7 +62,7 @@ class IntervalSchedule(PrefectBaseModel):
|
|
54
62
|
timezone (str, optional): a valid timezone string
|
55
63
|
"""
|
56
64
|
|
57
|
-
model_config = ConfigDict(extra="forbid"
|
65
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
|
58
66
|
|
59
67
|
interval: datetime.timedelta = Field(gt=datetime.timedelta(0))
|
60
68
|
anchor_date: Annotated[DateTime, AfterValidator(default_anchor_date)] = Field(
|
@@ -68,6 +76,19 @@ class IntervalSchedule(PrefectBaseModel):
|
|
68
76
|
self.timezone = default_timezone(self.timezone, self.model_dump())
|
69
77
|
return self
|
70
78
|
|
79
|
+
if TYPE_CHECKING:
|
80
|
+
# The model accepts str or datetime values for `anchor_date`
|
81
|
+
def __init__(
|
82
|
+
self,
|
83
|
+
/,
|
84
|
+
interval: datetime.timedelta,
|
85
|
+
anchor_date: Optional[
|
86
|
+
Union[pendulum.DateTime, datetime.datetime, str]
|
87
|
+
] = None,
|
88
|
+
timezone: Optional[str] = None,
|
89
|
+
) -> None:
|
90
|
+
...
|
91
|
+
|
71
92
|
|
72
93
|
class CronSchedule(PrefectBaseModel):
|
73
94
|
"""
|
@@ -94,7 +115,7 @@ class CronSchedule(PrefectBaseModel):
|
|
94
115
|
|
95
116
|
"""
|
96
117
|
|
97
|
-
model_config = ConfigDict(extra="forbid")
|
118
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
|
98
119
|
|
99
120
|
cron: str = Field(default=..., examples=["0 0 * * *"])
|
100
121
|
timezone: Optional[str] = Field(default=None, examples=["America/New_York"])
|
@@ -107,18 +128,36 @@ class CronSchedule(PrefectBaseModel):
|
|
107
128
|
|
108
129
|
@field_validator("timezone")
|
109
130
|
@classmethod
|
110
|
-
def valid_timezone(cls, v):
|
131
|
+
def valid_timezone(cls, v: Optional[str]) -> str:
|
111
132
|
return default_timezone(v)
|
112
133
|
|
113
134
|
@field_validator("cron")
|
114
135
|
@classmethod
|
115
|
-
def valid_cron_string(cls, v):
|
136
|
+
def valid_cron_string(cls, v: str) -> str:
|
116
137
|
return validate_cron_string(v)
|
117
138
|
|
118
139
|
|
119
140
|
DEFAULT_ANCHOR_DATE = pendulum.date(2020, 1, 1)
|
120
141
|
|
121
142
|
|
143
|
+
def _rrule_dt(
|
144
|
+
rrule: dateutil.rrule.rrule, name: str = "_dtstart"
|
145
|
+
) -> Optional[datetime.datetime]:
|
146
|
+
return getattr(rrule, name, None)
|
147
|
+
|
148
|
+
|
149
|
+
def _rrule(
|
150
|
+
rruleset: dateutil.rrule.rruleset, name: str = "_rrule"
|
151
|
+
) -> list[dateutil.rrule.rrule]:
|
152
|
+
return getattr(rruleset, name, [])
|
153
|
+
|
154
|
+
|
155
|
+
def _rdates(
|
156
|
+
rrule: dateutil.rrule.rruleset, name: str = "_rdate"
|
157
|
+
) -> list[datetime.datetime]:
|
158
|
+
return getattr(rrule, name, [])
|
159
|
+
|
160
|
+
|
122
161
|
class RRuleSchedule(PrefectBaseModel):
|
123
162
|
"""
|
124
163
|
RRule schedule, based on the iCalendar standard
|
@@ -139,7 +178,7 @@ class RRuleSchedule(PrefectBaseModel):
|
|
139
178
|
timezone (str, optional): a valid timezone string
|
140
179
|
"""
|
141
180
|
|
142
|
-
model_config = ConfigDict(extra="forbid")
|
181
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
|
143
182
|
|
144
183
|
rrule: str
|
145
184
|
timezone: Optional[str] = Field(
|
@@ -148,58 +187,60 @@ class RRuleSchedule(PrefectBaseModel):
|
|
148
187
|
|
149
188
|
@field_validator("rrule")
|
150
189
|
@classmethod
|
151
|
-
def validate_rrule_str(cls, v):
|
190
|
+
def validate_rrule_str(cls, v: str) -> str:
|
152
191
|
return validate_rrule_string(v)
|
153
192
|
|
154
193
|
@classmethod
|
155
|
-
def from_rrule(
|
194
|
+
def from_rrule(
|
195
|
+
cls, rrule: Union[dateutil.rrule.rrule, dateutil.rrule.rruleset]
|
196
|
+
) -> "RRuleSchedule":
|
156
197
|
if isinstance(rrule, dateutil.rrule.rrule):
|
157
|
-
|
158
|
-
|
198
|
+
dtstart = _rrule_dt(rrule)
|
199
|
+
if dtstart and dtstart.tzinfo is not None:
|
200
|
+
timezone = dtstart.tzinfo.tzname(dtstart)
|
159
201
|
else:
|
160
202
|
timezone = "UTC"
|
161
203
|
return RRuleSchedule(rrule=str(rrule), timezone=timezone)
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
timezone = "UTC"
|
179
|
-
|
180
|
-
rruleset_string = ""
|
181
|
-
if rrule._rrule:
|
182
|
-
rruleset_string += "\n".join(str(r) for r in rrule._rrule)
|
183
|
-
if rrule._exrule:
|
184
|
-
rruleset_string += "\n" if rruleset_string else ""
|
185
|
-
rruleset_string += "\n".join(str(r) for r in rrule._exrule).replace(
|
186
|
-
"RRULE", "EXRULE"
|
187
|
-
)
|
188
|
-
if rrule._rdate:
|
189
|
-
rruleset_string += "\n" if rruleset_string else ""
|
190
|
-
rruleset_string += "RDATE:" + ",".join(
|
191
|
-
rd.strftime("%Y%m%dT%H%M%SZ") for rd in rrule._rdate
|
192
|
-
)
|
193
|
-
if rrule._exdate:
|
194
|
-
rruleset_string += "\n" if rruleset_string else ""
|
195
|
-
rruleset_string += "EXDATE:" + ",".join(
|
196
|
-
exd.strftime("%Y%m%dT%H%M%SZ") for exd in rrule._exdate
|
197
|
-
)
|
198
|
-
return RRuleSchedule(rrule=rruleset_string, timezone=timezone)
|
204
|
+
rrules = _rrule(rrule)
|
205
|
+
dtstarts = [dts for rr in rrules if (dts := _rrule_dt(rr)) is not None]
|
206
|
+
unique_dstarts = set(pendulum.instance(d).in_tz("UTC") for d in dtstarts)
|
207
|
+
unique_timezones = set(d.tzinfo for d in dtstarts if d.tzinfo is not None)
|
208
|
+
|
209
|
+
if len(unique_timezones) > 1:
|
210
|
+
raise ValueError(
|
211
|
+
f"rruleset has too many dtstart timezones: {unique_timezones}"
|
212
|
+
)
|
213
|
+
|
214
|
+
if len(unique_dstarts) > 1:
|
215
|
+
raise ValueError(f"rruleset has too many dtstarts: {unique_dstarts}")
|
216
|
+
|
217
|
+
if unique_dstarts and unique_timezones:
|
218
|
+
[unique_tz] = unique_timezones
|
219
|
+
timezone = unique_tz.tzname(dtstarts[0])
|
199
220
|
else:
|
200
|
-
|
201
|
-
|
202
|
-
|
221
|
+
timezone = "UTC"
|
222
|
+
|
223
|
+
rruleset_string = ""
|
224
|
+
if rrules:
|
225
|
+
rruleset_string += "\n".join(str(r) for r in rrules)
|
226
|
+
if exrule := _rrule(rrule, "_exrule"):
|
227
|
+
rruleset_string += "\n" if rruleset_string else ""
|
228
|
+
rruleset_string += "\n".join(str(r) for r in exrule).replace(
|
229
|
+
"RRULE", "EXRULE"
|
230
|
+
)
|
231
|
+
if rdates := _rdates(rrule):
|
232
|
+
rruleset_string += "\n" if rruleset_string else ""
|
233
|
+
rruleset_string += "RDATE:" + ",".join(
|
234
|
+
rd.strftime("%Y%m%dT%H%M%SZ") for rd in rdates
|
235
|
+
)
|
236
|
+
if exdates := _rdates(rrule, "_exdate"):
|
237
|
+
rruleset_string += "\n" if rruleset_string else ""
|
238
|
+
rruleset_string += "EXDATE:" + ",".join(
|
239
|
+
exd.strftime("%Y%m%dT%H%M%SZ") for exd in exdates
|
240
|
+
)
|
241
|
+
return RRuleSchedule(rrule=rruleset_string, timezone=timezone)
|
242
|
+
|
243
|
+
def to_rrule(self) -> Union[dateutil.rrule.rrule, dateutil.rrule.rruleset]:
|
203
244
|
"""
|
204
245
|
Since rrule doesn't properly serialize/deserialize timezones, we localize dates
|
205
246
|
here
|
@@ -211,51 +252,53 @@ class RRuleSchedule(PrefectBaseModel):
|
|
211
252
|
)
|
212
253
|
timezone = dateutil.tz.gettz(self.timezone)
|
213
254
|
if isinstance(rrule, dateutil.rrule.rrule):
|
214
|
-
|
215
|
-
|
255
|
+
dtstart = _rrule_dt(rrule)
|
256
|
+
assert dtstart is not None
|
257
|
+
kwargs: dict[str, Any] = dict(dtstart=dtstart.replace(tzinfo=timezone))
|
258
|
+
if until := _rrule_dt(rrule, "_until"):
|
216
259
|
kwargs.update(
|
217
|
-
until=
|
260
|
+
until=until.replace(tzinfo=timezone),
|
218
261
|
)
|
219
262
|
return rrule.replace(**kwargs)
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
263
|
+
|
264
|
+
# update rrules
|
265
|
+
localized_rrules: list[dateutil.rrule.rrule] = []
|
266
|
+
for rr in _rrule(rrule):
|
267
|
+
dtstart = _rrule_dt(rr)
|
268
|
+
assert dtstart is not None
|
269
|
+
kwargs: dict[str, Any] = dict(dtstart=dtstart.replace(tzinfo=timezone))
|
270
|
+
if until := _rrule_dt(rr, "_until"):
|
271
|
+
kwargs.update(until=until.replace(tzinfo=timezone))
|
272
|
+
localized_rrules.append(rr.replace(**kwargs))
|
273
|
+
setattr(rrule, "_rrule", localized_rrules)
|
274
|
+
|
275
|
+
# update exrules
|
276
|
+
localized_exrules: list[dateutil.rrule.rruleset] = []
|
277
|
+
for exr in _rrule(rrule, "_exrule"):
|
278
|
+
dtstart = _rrule_dt(exr)
|
279
|
+
assert dtstart is not None
|
280
|
+
kwargs = dict(dtstart=dtstart.replace(tzinfo=timezone))
|
281
|
+
if until := _rrule_dt(exr, "_until"):
|
282
|
+
kwargs.update(until=until.replace(tzinfo=timezone))
|
283
|
+
localized_exrules.append(exr.replace(**kwargs))
|
284
|
+
setattr(rrule, "_exrule", localized_exrules)
|
285
|
+
|
286
|
+
# update rdates
|
287
|
+
localized_rdates: list[datetime.datetime] = []
|
288
|
+
for rd in _rdates(rrule):
|
289
|
+
localized_rdates.append(rd.replace(tzinfo=timezone))
|
290
|
+
setattr(rrule, "_rdate", localized_rdates)
|
291
|
+
|
292
|
+
# update exdates
|
293
|
+
localized_exdates: list[datetime.datetime] = []
|
294
|
+
for exd in _rdates(rrule, "_exdate"):
|
295
|
+
localized_exdates.append(exd.replace(tzinfo=timezone))
|
296
|
+
setattr(rrule, "_exdate", localized_exdates)
|
297
|
+
|
298
|
+
return rrule
|
256
299
|
|
257
300
|
@field_validator("timezone")
|
258
|
-
def valid_timezone(cls, v):
|
301
|
+
def valid_timezone(cls, v: Optional[str]) -> str:
|
259
302
|
"""
|
260
303
|
Validate that the provided timezone is a valid IANA timezone.
|
261
304
|
|
@@ -277,7 +320,7 @@ class RRuleSchedule(PrefectBaseModel):
|
|
277
320
|
|
278
321
|
|
279
322
|
class NoSchedule(PrefectBaseModel):
|
280
|
-
model_config = ConfigDict(extra="forbid")
|
323
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
|
281
324
|
|
282
325
|
|
283
326
|
SCHEDULE_TYPES: TypeAlias = Union[
|
@@ -326,7 +369,7 @@ def construct_schedule(
|
|
326
369
|
if isinstance(interval, (int, float)):
|
327
370
|
interval = datetime.timedelta(seconds=interval)
|
328
371
|
if not anchor_date:
|
329
|
-
anchor_date = DateTime.now()
|
372
|
+
anchor_date = pendulum.DateTime.now()
|
330
373
|
schedule = IntervalSchedule(
|
331
374
|
interval=interval, anchor_date=anchor_date, timezone=timezone
|
332
375
|
)
|
prefect/client/subscriptions.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
import asyncio
|
2
|
-
from
|
2
|
+
from collections.abc import Iterable
|
3
|
+
from logging import Logger
|
4
|
+
from typing import Any, Generic, Optional, TypeVar
|
3
5
|
|
4
6
|
import orjson
|
5
7
|
import websockets
|
@@ -8,10 +10,11 @@ from starlette.status import WS_1008_POLICY_VIOLATION
|
|
8
10
|
from typing_extensions import Self
|
9
11
|
|
10
12
|
from prefect._internal.schemas.bases import IDBaseModel
|
13
|
+
from prefect.events.clients import websocket_connect
|
11
14
|
from prefect.logging import get_logger
|
12
15
|
from prefect.settings import PREFECT_API_KEY
|
13
16
|
|
14
|
-
logger = get_logger(__name__)
|
17
|
+
logger: Logger = get_logger(__name__)
|
15
18
|
|
16
19
|
S = TypeVar("S", bound=IDBaseModel)
|
17
20
|
|
@@ -19,7 +22,7 @@ S = TypeVar("S", bound=IDBaseModel)
|
|
19
22
|
class Subscription(Generic[S]):
|
20
23
|
def __init__(
|
21
24
|
self,
|
22
|
-
model:
|
25
|
+
model: type[S],
|
23
26
|
path: str,
|
24
27
|
keys: Iterable[str],
|
25
28
|
client_id: Optional[str] = None,
|
@@ -27,27 +30,33 @@ class Subscription(Generic[S]):
|
|
27
30
|
):
|
28
31
|
self.model = model
|
29
32
|
self.client_id = client_id
|
30
|
-
base_url = base_url.replace("http", "ws", 1)
|
31
|
-
self.subscription_url = f"{base_url}{path}"
|
33
|
+
base_url = base_url.replace("http", "ws", 1) if base_url else None
|
34
|
+
self.subscription_url: str = f"{base_url}{path}"
|
32
35
|
|
33
|
-
self.keys = list(keys)
|
36
|
+
self.keys: list[str] = list(keys)
|
34
37
|
|
35
|
-
self._connect =
|
38
|
+
self._connect = websocket_connect(
|
36
39
|
self.subscription_url,
|
37
|
-
subprotocols=["prefect"],
|
40
|
+
subprotocols=[websockets.Subprotocol("prefect")],
|
38
41
|
)
|
39
42
|
self._websocket = None
|
40
43
|
|
41
44
|
def __aiter__(self) -> Self:
|
42
45
|
return self
|
43
46
|
|
47
|
+
@property
|
48
|
+
def websocket(self) -> websockets.WebSocketClientProtocol:
|
49
|
+
if not self._websocket:
|
50
|
+
raise RuntimeError("Subscription is not connected")
|
51
|
+
return self._websocket
|
52
|
+
|
44
53
|
async def __anext__(self) -> S:
|
45
54
|
while True:
|
46
55
|
try:
|
47
56
|
await self._ensure_connected()
|
48
|
-
message = await self.
|
57
|
+
message = await self.websocket.recv()
|
49
58
|
|
50
|
-
await self.
|
59
|
+
await self.websocket.send(orjson.dumps({"type": "ack"}).decode())
|
51
60
|
|
52
61
|
return self.model.model_validate_json(message)
|
53
62
|
except (
|
@@ -72,10 +81,10 @@ class Subscription(Generic[S]):
|
|
72
81
|
).decode()
|
73
82
|
)
|
74
83
|
|
75
|
-
auth:
|
84
|
+
auth: dict[str, Any] = orjson.loads(await websocket.recv())
|
76
85
|
assert auth["type"] == "auth_success", auth.get("message")
|
77
86
|
|
78
|
-
message = {"type": "subscribe", "keys": self.keys}
|
87
|
+
message: dict[str, Any] = {"type": "subscribe", "keys": self.keys}
|
79
88
|
if self.client_id:
|
80
89
|
message.update({"client_id": self.client_id})
|
81
90
|
|
@@ -84,13 +93,19 @@ class Subscription(Generic[S]):
|
|
84
93
|
AssertionError,
|
85
94
|
websockets.exceptions.ConnectionClosedError,
|
86
95
|
) as e:
|
87
|
-
if isinstance(e, AssertionError) or
|
96
|
+
if isinstance(e, AssertionError) or (
|
97
|
+
e.rcvd and e.rcvd.code == WS_1008_POLICY_VIOLATION
|
98
|
+
):
|
88
99
|
if isinstance(e, AssertionError):
|
89
100
|
reason = e.args[0]
|
90
|
-
elif
|
101
|
+
elif e.rcvd and e.rcvd.reason:
|
91
102
|
reason = e.rcvd.reason
|
103
|
+
else:
|
104
|
+
reason = "unknown"
|
105
|
+
else:
|
106
|
+
reason = None
|
92
107
|
|
93
|
-
if
|
108
|
+
if reason:
|
94
109
|
raise Exception(
|
95
110
|
"Unable to authenticate to the subscription. Please "
|
96
111
|
"ensure the provided `PREFECT_API_KEY` you are using is "
|