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.
Files changed (114) hide show
  1. prefect/__init__.py +3 -0
  2. prefect/_experimental/__init__.py +0 -0
  3. prefect/_experimental/lineage.py +181 -0
  4. prefect/_internal/compatibility/async_dispatch.py +38 -9
  5. prefect/_internal/compatibility/migration.py +1 -1
  6. prefect/_internal/concurrency/api.py +52 -52
  7. prefect/_internal/concurrency/calls.py +59 -35
  8. prefect/_internal/concurrency/cancellation.py +34 -18
  9. prefect/_internal/concurrency/event_loop.py +7 -6
  10. prefect/_internal/concurrency/threads.py +41 -33
  11. prefect/_internal/concurrency/waiters.py +28 -21
  12. prefect/_internal/pydantic/v1_schema.py +2 -2
  13. prefect/_internal/pydantic/v2_schema.py +10 -9
  14. prefect/_internal/pydantic/v2_validated_func.py +15 -10
  15. prefect/_internal/retries.py +15 -6
  16. prefect/_internal/schemas/bases.py +11 -8
  17. prefect/_internal/schemas/validators.py +7 -5
  18. prefect/_version.py +3 -3
  19. prefect/automations.py +53 -47
  20. prefect/blocks/abstract.py +12 -10
  21. prefect/blocks/core.py +148 -19
  22. prefect/blocks/system.py +2 -1
  23. prefect/cache_policies.py +11 -11
  24. prefect/client/__init__.py +3 -1
  25. prefect/client/base.py +36 -37
  26. prefect/client/cloud.py +26 -19
  27. prefect/client/collections.py +2 -2
  28. prefect/client/orchestration.py +430 -273
  29. prefect/client/schemas/__init__.py +24 -0
  30. prefect/client/schemas/actions.py +128 -121
  31. prefect/client/schemas/filters.py +1 -1
  32. prefect/client/schemas/objects.py +114 -85
  33. prefect/client/schemas/responses.py +19 -20
  34. prefect/client/schemas/schedules.py +136 -93
  35. prefect/client/subscriptions.py +30 -15
  36. prefect/client/utilities.py +46 -36
  37. prefect/concurrency/asyncio.py +6 -9
  38. prefect/concurrency/sync.py +35 -5
  39. prefect/context.py +40 -32
  40. prefect/deployments/flow_runs.py +6 -8
  41. prefect/deployments/runner.py +14 -14
  42. prefect/deployments/steps/core.py +3 -1
  43. prefect/deployments/steps/pull.py +60 -12
  44. prefect/docker/__init__.py +1 -1
  45. prefect/events/clients.py +55 -4
  46. prefect/events/filters.py +1 -1
  47. prefect/events/related.py +2 -1
  48. prefect/events/schemas/events.py +26 -21
  49. prefect/events/utilities.py +3 -2
  50. prefect/events/worker.py +8 -0
  51. prefect/filesystems.py +3 -3
  52. prefect/flow_engine.py +87 -87
  53. prefect/flow_runs.py +7 -5
  54. prefect/flows.py +218 -176
  55. prefect/logging/configuration.py +1 -1
  56. prefect/logging/highlighters.py +1 -2
  57. prefect/logging/loggers.py +30 -20
  58. prefect/main.py +17 -24
  59. prefect/results.py +43 -22
  60. prefect/runner/runner.py +43 -21
  61. prefect/runner/server.py +30 -32
  62. prefect/runner/storage.py +3 -3
  63. prefect/runner/submit.py +3 -6
  64. prefect/runner/utils.py +6 -6
  65. prefect/runtime/flow_run.py +7 -0
  66. prefect/serializers.py +28 -24
  67. prefect/settings/constants.py +2 -2
  68. prefect/settings/legacy.py +1 -1
  69. prefect/settings/models/experiments.py +5 -0
  70. prefect/settings/models/server/events.py +10 -0
  71. prefect/task_engine.py +87 -26
  72. prefect/task_runners.py +2 -2
  73. prefect/task_worker.py +43 -25
  74. prefect/tasks.py +148 -142
  75. prefect/telemetry/bootstrap.py +15 -2
  76. prefect/telemetry/instrumentation.py +1 -1
  77. prefect/telemetry/processors.py +10 -7
  78. prefect/telemetry/run_telemetry.py +231 -0
  79. prefect/transactions.py +14 -14
  80. prefect/types/__init__.py +5 -5
  81. prefect/utilities/_engine.py +96 -0
  82. prefect/utilities/annotations.py +25 -18
  83. prefect/utilities/asyncutils.py +126 -140
  84. prefect/utilities/callables.py +87 -78
  85. prefect/utilities/collections.py +278 -117
  86. prefect/utilities/compat.py +13 -21
  87. prefect/utilities/context.py +6 -5
  88. prefect/utilities/dispatch.py +23 -12
  89. prefect/utilities/dockerutils.py +33 -32
  90. prefect/utilities/engine.py +126 -239
  91. prefect/utilities/filesystem.py +18 -15
  92. prefect/utilities/hashing.py +10 -11
  93. prefect/utilities/importtools.py +40 -27
  94. prefect/utilities/math.py +9 -5
  95. prefect/utilities/names.py +3 -3
  96. prefect/utilities/processutils.py +121 -57
  97. prefect/utilities/pydantic.py +41 -36
  98. prefect/utilities/render_swagger.py +22 -12
  99. prefect/utilities/schema_tools/__init__.py +2 -1
  100. prefect/utilities/schema_tools/hydration.py +50 -43
  101. prefect/utilities/schema_tools/validation.py +52 -42
  102. prefect/utilities/services.py +13 -12
  103. prefect/utilities/templating.py +45 -45
  104. prefect/utilities/text.py +2 -1
  105. prefect/utilities/timeout.py +4 -4
  106. prefect/utilities/urls.py +9 -4
  107. prefect/utilities/visualization.py +46 -24
  108. prefect/variables.py +136 -27
  109. prefect/workers/base.py +15 -8
  110. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/METADATA +5 -2
  111. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/RECORD +114 -110
  112. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/LICENSE +0 -0
  113. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/WHEEL +0 -0
  114. {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, Dict, List, Optional, TypeVar, Union
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
- R = TypeVar("R")
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: List[HistoryResponseState] = Field(
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: Dict[str, Any] = Field(
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: Dict[str, Any] = Field(
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: List[str] = Field(
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: List[objects.DeploymentSchedule] = Field(
337
+ schedules: list[objects.DeploymentSchedule] = Field(
339
338
  default_factory=list, description="A list of schedules for the deployment."
340
339
  )
341
- job_variables: Dict[str, Any] = Field(
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: Dict[str, Any] = Field(
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[List[dict]] = Field(
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: List[str] = Field(
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[Dict[str, Any]] = Field(
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", exclude_none=True)
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(cls, rrule: dateutil.rrule.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
- if rrule._dtstart.tzinfo is not None:
158
- timezone = rrule._dtstart.tzinfo.name
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
- elif isinstance(rrule, dateutil.rrule.rruleset):
163
- dtstarts = [rr._dtstart for rr in rrule._rrule if rr._dtstart is not None]
164
- unique_dstarts = set(pendulum.instance(d).in_tz("UTC") for d in dtstarts)
165
- unique_timezones = set(d.tzinfo for d in dtstarts if d.tzinfo is not None)
166
-
167
- if len(unique_timezones) > 1:
168
- raise ValueError(
169
- f"rruleset has too many dtstart timezones: {unique_timezones}"
170
- )
171
-
172
- if len(unique_dstarts) > 1:
173
- raise ValueError(f"rruleset has too many dtstarts: {unique_dstarts}")
174
-
175
- if unique_dstarts and unique_timezones:
176
- timezone = dtstarts[0].tzinfo.name
177
- else:
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
- raise ValueError(f"Invalid RRule object: {rrule}")
201
-
202
- def to_rrule(self) -> dateutil.rrule.rrule:
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
- kwargs = dict(dtstart=rrule._dtstart.replace(tzinfo=timezone))
215
- if rrule._until:
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=rrule._until.replace(tzinfo=timezone),
260
+ until=until.replace(tzinfo=timezone),
218
261
  )
219
262
  return rrule.replace(**kwargs)
220
- elif isinstance(rrule, dateutil.rrule.rruleset):
221
- # update rrules
222
- localized_rrules = []
223
- for rr in rrule._rrule:
224
- kwargs = dict(dtstart=rr._dtstart.replace(tzinfo=timezone))
225
- if rr._until:
226
- kwargs.update(
227
- until=rr._until.replace(tzinfo=timezone),
228
- )
229
- localized_rrules.append(rr.replace(**kwargs))
230
- rrule._rrule = localized_rrules
231
-
232
- # update exrules
233
- localized_exrules = []
234
- for exr in rrule._exrule:
235
- kwargs = dict(dtstart=exr._dtstart.replace(tzinfo=timezone))
236
- if exr._until:
237
- kwargs.update(
238
- until=exr._until.replace(tzinfo=timezone),
239
- )
240
- localized_exrules.append(exr.replace(**kwargs))
241
- rrule._exrule = localized_exrules
242
-
243
- # update rdates
244
- localized_rdates = []
245
- for rd in rrule._rdate:
246
- localized_rdates.append(rd.replace(tzinfo=timezone))
247
- rrule._rdate = localized_rdates
248
-
249
- # update exdates
250
- localized_exdates = []
251
- for exd in rrule._exdate:
252
- localized_exdates.append(exd.replace(tzinfo=timezone))
253
- rrule._exdate = localized_exdates
254
-
255
- return rrule
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
  )
@@ -1,5 +1,7 @@
1
1
  import asyncio
2
- from typing import Any, Dict, Generic, Iterable, Optional, Type, TypeVar
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: Type[S],
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 = websockets.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._websocket.recv()
57
+ message = await self.websocket.recv()
49
58
 
50
- await self._websocket.send(orjson.dumps({"type": "ack"}).decode())
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: Dict[str, Any] = orjson.loads(await websocket.recv())
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 e.rcvd.code == WS_1008_POLICY_VIOLATION:
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 isinstance(e, websockets.exceptions.ConnectionClosedError):
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 isinstance(e, AssertionError) or e.rcvd.code == WS_1008_POLICY_VIOLATION:
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 "