prefect-client 3.0.0rc20__py3-none-any.whl → 3.0.2__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 (57) hide show
  1. prefect/_internal/compatibility/deprecated.py +1 -1
  2. prefect/_internal/compatibility/migration.py +1 -1
  3. prefect/artifacts.py +1 -1
  4. prefect/blocks/core.py +3 -4
  5. prefect/blocks/notifications.py +31 -10
  6. prefect/blocks/system.py +4 -4
  7. prefect/blocks/webhook.py +11 -1
  8. prefect/client/cloud.py +2 -1
  9. prefect/client/orchestration.py +93 -21
  10. prefect/client/schemas/actions.py +2 -2
  11. prefect/client/schemas/objects.py +24 -6
  12. prefect/client/types/flexible_schedule_list.py +1 -1
  13. prefect/concurrency/asyncio.py +45 -6
  14. prefect/concurrency/services.py +1 -1
  15. prefect/concurrency/sync.py +21 -27
  16. prefect/concurrency/v1/asyncio.py +3 -0
  17. prefect/concurrency/v1/sync.py +4 -5
  18. prefect/context.py +11 -9
  19. prefect/deployments/runner.py +4 -3
  20. prefect/events/actions.py +6 -0
  21. prefect/exceptions.py +6 -0
  22. prefect/filesystems.py +5 -3
  23. prefect/flow_engine.py +22 -11
  24. prefect/flows.py +0 -2
  25. prefect/futures.py +2 -1
  26. prefect/locking/__init__.py +0 -0
  27. prefect/locking/filesystem.py +243 -0
  28. prefect/locking/memory.py +213 -0
  29. prefect/locking/protocol.py +122 -0
  30. prefect/logging/handlers.py +0 -2
  31. prefect/logging/loggers.py +0 -18
  32. prefect/logging/logging.yml +1 -0
  33. prefect/main.py +19 -5
  34. prefect/records/base.py +12 -0
  35. prefect/records/filesystem.py +10 -4
  36. prefect/records/memory.py +6 -0
  37. prefect/records/result_store.py +18 -6
  38. prefect/results.py +702 -205
  39. prefect/runner/runner.py +74 -5
  40. prefect/settings.py +11 -4
  41. prefect/states.py +40 -23
  42. prefect/task_engine.py +39 -37
  43. prefect/task_worker.py +6 -4
  44. prefect/tasks.py +24 -6
  45. prefect/transactions.py +116 -54
  46. prefect/utilities/callables.py +1 -3
  47. prefect/utilities/engine.py +16 -8
  48. prefect/utilities/importtools.py +1 -0
  49. prefect/utilities/urls.py +70 -12
  50. prefect/variables.py +34 -24
  51. prefect/workers/base.py +14 -6
  52. prefect/workers/process.py +1 -3
  53. {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.2.dist-info}/METADATA +2 -2
  54. {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.2.dist-info}/RECORD +57 -53
  55. {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.2.dist-info}/LICENSE +0 -0
  56. {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.2.dist-info}/WHEEL +0 -0
  57. {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.2.dist-info}/top_level.txt +0 -0
@@ -30,7 +30,7 @@ M = TypeVar("M", bound=BaseModel)
30
30
 
31
31
 
32
32
  DEPRECATED_WARNING = (
33
- "{name} has been deprecated{when}. It will not be available after {end_date}."
33
+ "{name} has been deprecated{when}. It will not be available in new releases after {end_date}."
34
34
  " {help}"
35
35
  )
36
36
  DEPRECATED_MOVED_WARNING = (
@@ -60,7 +60,7 @@ MOVED_IN_V3 = {
60
60
  "prefect.client:get_client": "prefect.client.orchestration:get_client",
61
61
  }
62
62
 
63
- upgrade_guide_msg = "Refer to the upgrade guide for more information: https://docs.prefect.io/latest/guides/upgrade-guide-agents-to-workers/."
63
+ upgrade_guide_msg = "Refer to the upgrade guide for more information: https://docs.prefect.io/latest/resources/upgrade-agents-to-workers."
64
64
 
65
65
  REMOVED_IN_V3 = {
66
66
  "prefect.client.schemas.objects:MinimalDeploymentSchedule": "Use `prefect.client.schemas.actions.DeploymentScheduleCreate` instead.",
prefect/artifacts.py CHANGED
@@ -31,7 +31,7 @@ if TYPE_CHECKING:
31
31
  class Artifact(ArtifactRequest):
32
32
  """
33
33
  An artifact is a piece of data that is created by a flow or task run.
34
- https://docs.prefect.io/latest/concepts/artifacts/
34
+ https://docs.prefect.io/latest/develop/artifacts
35
35
 
36
36
  Arguments:
37
37
  type: A string identifying the type of artifact.
prefect/blocks/core.py CHANGED
@@ -555,10 +555,9 @@ class Block(BaseModel, ABC):
555
555
  `<module>:11: No type or annotation for parameter 'write_json'`
556
556
  because griffe is unable to parse the types from pydantic.BaseModel.
557
557
  """
558
- with disable_logger("griffe.docstrings.google"):
559
- with disable_logger("griffe.agents.nodes"):
560
- docstring = Docstring(cls.__doc__)
561
- parsed = parse(docstring, Parser.google)
558
+ with disable_logger("griffe"):
559
+ docstring = Docstring(cls.__doc__)
560
+ parsed = parse(docstring, Parser.google)
562
561
  return parsed
563
562
 
564
563
  @classmethod
@@ -10,6 +10,7 @@ from prefect.logging import LogEavesdropper
10
10
  from prefect.types import SecretDict
11
11
  from prefect.utilities.asyncutils import sync_compatible
12
12
  from prefect.utilities.templating import apply_values, find_placeholders
13
+ from prefect.utilities.urls import validate_restricted_url
13
14
 
14
15
  PREFECT_NOTIFY_TYPE_DEFAULT = "prefect_default"
15
16
 
@@ -73,13 +74,33 @@ class AppriseNotificationBlock(AbstractAppriseNotificationBlock, ABC):
73
74
  A base class for sending notifications using Apprise, through webhook URLs.
74
75
  """
75
76
 
76
- _documentation_url = "https://docs.prefect.io/ui/notifications/"
77
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
77
78
  url: SecretStr = Field(
78
79
  default=...,
79
80
  title="Webhook URL",
80
81
  description="Incoming webhook URL used to send notifications.",
81
82
  examples=["https://hooks.example.com/XXX"],
82
83
  )
84
+ allow_private_urls: bool = Field(
85
+ default=True,
86
+ description="Whether to allow notifications to private URLs. Defaults to True.",
87
+ )
88
+
89
+ @sync_compatible
90
+ async def notify(
91
+ self,
92
+ body: str,
93
+ subject: Optional[str] = None,
94
+ ):
95
+ if not self.allow_private_urls:
96
+ try:
97
+ validate_restricted_url(self.url.get_secret_value())
98
+ except ValueError as exc:
99
+ if self._raise_on_failure:
100
+ raise NotificationError(str(exc))
101
+ raise
102
+
103
+ await super().notify(body, subject)
83
104
 
84
105
 
85
106
  # TODO: Move to prefect-slack once collection block auto-registration is
@@ -100,7 +121,7 @@ class SlackWebhook(AppriseNotificationBlock):
100
121
 
101
122
  _block_type_name = "Slack Webhook"
102
123
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/c1965ecbf8704ee1ea20d77786de9a41ce1087d1-500x500.png"
103
- _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/notifications/#prefect.blocks.notifications.SlackWebhook"
124
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
104
125
 
105
126
  url: SecretStr = Field(
106
127
  default=...,
@@ -126,7 +147,7 @@ class MicrosoftTeamsWebhook(AppriseNotificationBlock):
126
147
  _block_type_name = "Microsoft Teams Webhook"
127
148
  _block_type_slug = "ms-teams-webhook"
128
149
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/817efe008a57f0a24f3587414714b563e5e23658-250x250.png"
129
- _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/notifications/#prefect.blocks.notifications.MicrosoftTeamsWebhook"
150
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
130
151
 
131
152
  url: SecretStr = Field(
132
153
  default=...,
@@ -181,7 +202,7 @@ class PagerDutyWebHook(AbstractAppriseNotificationBlock):
181
202
  _block_type_name = "Pager Duty Webhook"
182
203
  _block_type_slug = "pager-duty-webhook"
183
204
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/8dbf37d17089c1ce531708eac2e510801f7b3aee-250x250.png"
184
- _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/notifications/#prefect.blocks.notifications.PagerDutyWebHook"
205
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
185
206
 
186
207
  # The default cannot be prefect_default because NotifyPagerDuty's
187
208
  # PAGERDUTY_SEVERITY_MAP only has these notify types defined as keys
@@ -291,7 +312,7 @@ class TwilioSMS(AbstractAppriseNotificationBlock):
291
312
  _block_type_name = "Twilio SMS"
292
313
  _block_type_slug = "twilio-sms"
293
314
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/8bd8777999f82112c09b9c8d57083ac75a4a0d65-250x250.png" # noqa
294
- _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/notifications/#prefect.blocks.notifications.TwilioSMS"
315
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
295
316
 
296
317
  account_sid: str = Field(
297
318
  default=...,
@@ -360,7 +381,7 @@ class OpsgenieWebhook(AbstractAppriseNotificationBlock):
360
381
  _block_type_name = "Opsgenie Webhook"
361
382
  _block_type_slug = "opsgenie-webhook"
362
383
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/d8b5bc6244ae6cd83b62ec42f10d96e14d6e9113-280x280.png"
363
- _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/notifications/#prefect.blocks.notifications.OpsgenieWebhook"
384
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
364
385
 
365
386
  apikey: SecretStr = Field(
366
387
  default=...,
@@ -478,7 +499,7 @@ class MattermostWebhook(AbstractAppriseNotificationBlock):
478
499
  _block_type_name = "Mattermost Webhook"
479
500
  _block_type_slug = "mattermost-webhook"
480
501
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/1350a147130bf82cbc799a5f868d2c0116207736-250x250.png"
481
- _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/notifications/#prefect.blocks.notifications.MattermostWebhook"
502
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
482
503
 
483
504
  hostname: str = Field(
484
505
  default=...,
@@ -559,7 +580,7 @@ class DiscordWebhook(AbstractAppriseNotificationBlock):
559
580
  _block_type_name = "Discord Webhook"
560
581
  _block_type_slug = "discord-webhook"
561
582
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/9e94976c80ef925b66d24e5d14f0d47baa6b8f88-250x250.png"
562
- _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/notifications/#prefect.blocks.notifications.DiscordWebhook"
583
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
563
584
 
564
585
  webhook_id: SecretStr = Field(
565
586
  default=...,
@@ -658,7 +679,7 @@ class CustomWebhookNotificationBlock(NotificationBlock):
658
679
 
659
680
  _block_type_name = "Custom Webhook"
660
681
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/c7247cb359eb6cf276734d4b1fbf00fb8930e89e-250x250.png"
661
- _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/notifications/#prefect.blocks.notifications.CustomWebhookNotificationBlock"
682
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
662
683
 
663
684
  name: str = Field(title="Name", description="Name of the webhook.")
664
685
 
@@ -789,7 +810,7 @@ class SendgridEmail(AbstractAppriseNotificationBlock):
789
810
  _block_type_name = "Sendgrid Email"
790
811
  _block_type_slug = "sendgrid-email"
791
812
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/82bc6ed16ca42a2252a5512c72233a253b8a58eb-250x250.png"
792
- _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/notifications/#prefect.blocks.notifications.SendgridEmail"
813
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
793
814
 
794
815
  api_key: SecretStr = Field(
795
816
  default=...,
prefect/blocks/system.py CHANGED
@@ -44,7 +44,7 @@ class JSON(Block):
44
44
  """
45
45
 
46
46
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/4fcef2294b6eeb423b1332d1ece5156bf296ff96-48x48.png"
47
- _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/system/#prefect.blocks.system.JSON"
47
+ _documentation_url = "https://docs.prefect.io/latest/develop/blocks"
48
48
 
49
49
  value: Any = Field(default=..., description="A JSON-compatible value.")
50
50
 
@@ -71,7 +71,7 @@ class String(Block):
71
71
  """
72
72
 
73
73
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/c262ea2c80a2c043564e8763f3370c3db5a6b3e6-48x48.png"
74
- _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/system/#prefect.blocks.system.String"
74
+ _documentation_url = "https://docs.prefect.io/latest/develop/blocks"
75
75
 
76
76
  value: str = Field(default=..., description="A string value.")
77
77
 
@@ -99,7 +99,7 @@ class DateTime(Block):
99
99
 
100
100
  _block_type_name = "Date Time"
101
101
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/8b3da9a6621e92108b8e6a75b82e15374e170ff7-48x48.png"
102
- _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/system/#prefect.blocks.system.DateTime"
102
+ _documentation_url = "https://docs.prefect.io/latest/develop/blocks"
103
103
 
104
104
  value: PydanticDateTime = Field(
105
105
  default=...,
@@ -129,7 +129,7 @@ class Secret(Block, Generic[T]):
129
129
  """
130
130
 
131
131
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/c6f20e556dd16effda9df16551feecfb5822092b-48x48.png"
132
- _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/system/#prefect.blocks.system.Secret"
132
+ _documentation_url = "https://docs.prefect.io/latest/develop/blocks"
133
133
 
134
134
  value: Union[SecretStr, PydanticSecret[T]] = Field(
135
135
  default=...,
prefect/blocks/webhook.py CHANGED
@@ -6,6 +6,7 @@ from typing_extensions import Literal
6
6
 
7
7
  from prefect.blocks.core import Block
8
8
  from prefect.types import SecretDict
9
+ from prefect.utilities.urls import validate_restricted_url
9
10
 
10
11
  # Use a global HTTP transport to maintain a process-wide connection pool for
11
12
  # interservice requests
@@ -19,7 +20,9 @@ class Webhook(Block):
19
20
 
20
21
  _block_type_name = "Webhook"
21
22
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/c7247cb359eb6cf276734d4b1fbf00fb8930e89e-250x250.png" # type: ignore
22
- _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/webhook/#prefect.blocks.webhook.Webhook"
23
+ _documentation_url = (
24
+ "https://docs.prefect.io/latest/automate/events/webhook-triggers"
25
+ )
23
26
 
24
27
  method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"] = Field(
25
28
  default="POST", description="The webhook request method. Defaults to `POST`."
@@ -37,6 +40,10 @@ class Webhook(Block):
37
40
  title="Webhook Headers",
38
41
  description="A dictionary of headers to send with the webhook request.",
39
42
  )
43
+ allow_private_urls: bool = Field(
44
+ default=True,
45
+ description="Whether to allow notifications to private URLs. Defaults to True.",
46
+ )
40
47
 
41
48
  def block_initialization(self):
42
49
  self._client = AsyncClient(transport=_http_transport)
@@ -48,6 +55,9 @@ class Webhook(Block):
48
55
  Args:
49
56
  payload: an optional payload to send when calling the webhook.
50
57
  """
58
+ if not self.allow_private_urls:
59
+ validate_restricted_url(self.url.get_secret_value())
60
+
51
61
  async with self._client:
52
62
  return await self._client.request(
53
63
  method=self.method,
prefect/client/cloud.py CHANGED
@@ -73,9 +73,10 @@ class CloudClient:
73
73
  **httpx_settings, enable_csrf_support=False
74
74
  )
75
75
 
76
+ api_url = prefect.settings.PREFECT_API_URL.value() or ""
76
77
  if match := (
77
78
  re.search(PARSE_API_URL_REGEX, host)
78
- or re.search(PARSE_API_URL_REGEX, prefect.settings.PREFECT_API_URL.value())
79
+ or re.search(PARSE_API_URL_REGEX, api_url)
79
80
  ):
80
81
  self.account_id, self.workspace_id = match.groups()
81
82
 
@@ -24,6 +24,7 @@ import httpx
24
24
  import pendulum
25
25
  import pydantic
26
26
  from asgi_lifespan import LifespanManager
27
+ from packaging import version
27
28
  from starlette import status
28
29
  from typing_extensions import ParamSpec
29
30
 
@@ -93,7 +94,6 @@ from prefect.client.schemas.objects import (
93
94
  FlowRunPolicy,
94
95
  Log,
95
96
  Parameter,
96
- QueueFilter,
97
97
  TaskRunPolicy,
98
98
  TaskRunResult,
99
99
  Variable,
@@ -993,7 +993,6 @@ class PrefectClient:
993
993
  async def create_work_queue(
994
994
  self,
995
995
  name: str,
996
- tags: Optional[List[str]] = None,
997
996
  description: Optional[str] = None,
998
997
  is_paused: Optional[bool] = None,
999
998
  concurrency_limit: Optional[int] = None,
@@ -1005,8 +1004,6 @@ class PrefectClient:
1005
1004
 
1006
1005
  Args:
1007
1006
  name: a unique name for the work queue
1008
- tags: DEPRECATED: an optional list of tags to filter on; only work scheduled with these tags
1009
- will be included in the queue. This option will be removed on 2023-02-23.
1010
1007
  description: An optional description for the work queue.
1011
1008
  is_paused: Whether or not the work queue is paused.
1012
1009
  concurrency_limit: An optional concurrency limit for the work queue.
@@ -1020,18 +1017,7 @@ class PrefectClient:
1020
1017
  Returns:
1021
1018
  The created work queue
1022
1019
  """
1023
- if tags:
1024
- warnings.warn(
1025
- (
1026
- "The use of tags for creating work queue filters is deprecated."
1027
- " This option will be removed on 2023-02-23."
1028
- ),
1029
- DeprecationWarning,
1030
- )
1031
- filter = QueueFilter(tags=tags)
1032
- else:
1033
- filter = None
1034
- create_model = WorkQueueCreate(name=name, filter=filter)
1020
+ create_model = WorkQueueCreate(name=name, filter=None)
1035
1021
  if description is not None:
1036
1022
  create_model.description = description
1037
1023
  if is_paused is not None:
@@ -2157,7 +2143,10 @@ class PrefectClient:
2157
2143
  try:
2158
2144
  response = await self._client.post(
2159
2145
  f"/flow_runs/{flow_run_id}/set_state",
2160
- json=dict(state=state_create.model_dump(mode="json"), force=force),
2146
+ json=dict(
2147
+ state=state_create.model_dump(mode="json", serialize_as_any=True),
2148
+ force=force,
2149
+ ),
2161
2150
  )
2162
2151
  except httpx.HTTPStatusError as e:
2163
2152
  if e.response.status_code == status.HTTP_404_NOT_FOUND:
@@ -3054,7 +3043,11 @@ class PrefectClient:
3054
3043
  return response.json()
3055
3044
 
3056
3045
  async def increment_concurrency_slots(
3057
- self, names: List[str], slots: int, mode: str, create_if_missing: Optional[bool]
3046
+ self,
3047
+ names: List[str],
3048
+ slots: int,
3049
+ mode: str,
3050
+ create_if_missing: Optional[bool] = None,
3058
3051
  ) -> httpx.Response:
3059
3052
  return await self._client.post(
3060
3053
  "/v2/concurrency_limits/increment",
@@ -3062,7 +3055,7 @@ class PrefectClient:
3062
3055
  "names": names,
3063
3056
  "slots": slots,
3064
3057
  "mode": mode,
3065
- "create_if_missing": create_if_missing,
3058
+ "create_if_missing": create_if_missing if create_if_missing else False,
3066
3059
  },
3067
3060
  )
3068
3061
 
@@ -3139,6 +3132,30 @@ class PrefectClient:
3139
3132
  else:
3140
3133
  raise
3141
3134
 
3135
+ async def upsert_global_concurrency_limit_by_name(self, name: str, limit: int):
3136
+ """Creates a global concurrency limit with the given name and limit if one does not already exist.
3137
+
3138
+ If one does already exist matching the name then update it's limit if it is different.
3139
+
3140
+ Note: This is not done atomically.
3141
+ """
3142
+ try:
3143
+ existing_limit = await self.read_global_concurrency_limit_by_name(name)
3144
+ except prefect.exceptions.ObjectNotFound:
3145
+ existing_limit = None
3146
+
3147
+ if not existing_limit:
3148
+ await self.create_global_concurrency_limit(
3149
+ GlobalConcurrencyLimitCreate(
3150
+ name=name,
3151
+ limit=limit,
3152
+ )
3153
+ )
3154
+ elif existing_limit.limit != limit:
3155
+ await self.update_global_concurrency_limit(
3156
+ name, GlobalConcurrencyLimitUpdate(limit=limit)
3157
+ )
3158
+
3142
3159
  async def read_global_concurrency_limits(
3143
3160
  self, limit: int = 10, offset: int = 0
3144
3161
  ) -> List[GlobalConcurrencyLimitResponse]:
@@ -3329,6 +3346,32 @@ class PrefectClient:
3329
3346
  async def delete_resource_owned_automations(self, resource_id: str):
3330
3347
  await self._client.delete(f"/automations/owned-by/{resource_id}")
3331
3348
 
3349
+ async def api_version(self) -> str:
3350
+ res = await self._client.get("/admin/version")
3351
+ return res.json()
3352
+
3353
+ def client_version(self) -> str:
3354
+ return prefect.__version__
3355
+
3356
+ async def raise_for_api_version_mismatch(self):
3357
+ # Cloud is always compatible as a server
3358
+ if self.server_type == ServerType.CLOUD:
3359
+ return
3360
+
3361
+ try:
3362
+ api_version = await self.api_version()
3363
+ except Exception as e:
3364
+ raise RuntimeError(f"Failed to reach API at {self.api_url}") from e
3365
+
3366
+ api_version = version.parse(api_version)
3367
+ client_version = version.parse(self.client_version())
3368
+
3369
+ if api_version.major != client_version.major:
3370
+ raise RuntimeError(
3371
+ f"Found incompatible versions: client: {client_version}, server: {api_version}. "
3372
+ f"Major versions must match."
3373
+ )
3374
+
3332
3375
  async def __aenter__(self):
3333
3376
  """
3334
3377
  Start the client.
@@ -3622,6 +3665,32 @@ class SyncPrefectClient:
3622
3665
  """
3623
3666
  return self._client.get("/hello")
3624
3667
 
3668
+ def api_version(self) -> str:
3669
+ res = self._client.get("/admin/version")
3670
+ return res.json()
3671
+
3672
+ def client_version(self) -> str:
3673
+ return prefect.__version__
3674
+
3675
+ def raise_for_api_version_mismatch(self):
3676
+ # Cloud is always compatible as a server
3677
+ if self.server_type == ServerType.CLOUD:
3678
+ return
3679
+
3680
+ try:
3681
+ api_version = self.api_version()
3682
+ except Exception as e:
3683
+ raise RuntimeError(f"Failed to reach API at {self.api_url}") from e
3684
+
3685
+ api_version = version.parse(api_version)
3686
+ client_version = version.parse(self.client_version())
3687
+
3688
+ if api_version.major != client_version.major:
3689
+ raise RuntimeError(
3690
+ f"Found incompatible versions: client: {client_version}, server: {api_version}. "
3691
+ f"Major versions must match."
3692
+ )
3693
+
3625
3694
  def create_flow(self, flow: "FlowObject") -> UUID:
3626
3695
  """
3627
3696
  Create a flow in the Prefect API.
@@ -3881,7 +3950,10 @@ class SyncPrefectClient:
3881
3950
  try:
3882
3951
  response = self._client.post(
3883
3952
  f"/flow_runs/{flow_run_id}/set_state",
3884
- json=dict(state=state_create.model_dump(mode="json"), force=force),
3953
+ json=dict(
3954
+ state=state_create.model_dump(mode="json", serialize_as_any=True),
3955
+ force=force,
3956
+ ),
3885
3957
  )
3886
3958
  except httpx.HTTPStatusError as e:
3887
3959
  if e.response.status_code == status.HTTP_404_NOT_FOUND:
@@ -4147,7 +4219,7 @@ class SyncPrefectClient:
4147
4219
 
4148
4220
  response = self._client.post(
4149
4221
  "/artifacts/",
4150
- json=artifact.model_dump_json(exclude_unset=True),
4222
+ json=artifact.model_dump(mode="json", exclude_unset=True),
4151
4223
  )
4152
4224
 
4153
4225
  return Artifact.model_validate(response.json())
@@ -38,7 +38,7 @@ from prefect.utilities.collections import listrepr
38
38
  from prefect.utilities.pydantic import get_class_fields_only
39
39
 
40
40
  if TYPE_CHECKING:
41
- from prefect.results import BaseResult
41
+ from prefect.results import BaseResult, ResultRecordMetadata
42
42
 
43
43
  R = TypeVar("R")
44
44
 
@@ -50,7 +50,7 @@ class StateCreate(ActionBaseModel):
50
50
  name: Optional[str] = Field(default=None)
51
51
  message: Optional[str] = Field(default=None, examples=["Run started"])
52
52
  state_details: StateDetails = Field(default_factory=StateDetails)
53
- data: Union["BaseResult[R]", Any] = Field(
53
+ data: Union["BaseResult[R]", "ResultRecordMetadata", Any] = Field(
54
54
  default=None,
55
55
  )
56
56
 
@@ -3,6 +3,7 @@ import warnings
3
3
  from functools import partial
4
4
  from typing import (
5
5
  TYPE_CHECKING,
6
+ Annotated,
6
7
  Any,
7
8
  Dict,
8
9
  Generic,
@@ -17,10 +18,12 @@ import orjson
17
18
  import pendulum
18
19
  from pydantic import (
19
20
  ConfigDict,
21
+ Discriminator,
20
22
  Field,
21
23
  HttpUrl,
22
24
  IPvAnyNetwork,
23
25
  SerializationInfo,
26
+ Tag,
24
27
  field_validator,
25
28
  model_serializer,
26
29
  model_validator,
@@ -59,7 +62,7 @@ from prefect.utilities.names import generate_slug
59
62
  from prefect.utilities.pydantic import handle_secret_render
60
63
 
61
64
  if TYPE_CHECKING:
62
- from prefect.results import BaseResult
65
+ from prefect.results import BaseResult, ResultRecordMetadata
63
66
 
64
67
 
65
68
  R = TypeVar("R", default=Any)
@@ -158,6 +161,14 @@ class StateDetails(PrefectBaseModel):
158
161
  task_parameters_id: Optional[UUID] = None
159
162
 
160
163
 
164
+ def data_discriminator(x: Any) -> str:
165
+ if isinstance(x, dict) and "type" in x:
166
+ return "BaseResult"
167
+ elif isinstance(x, dict) and "storage_key" in x:
168
+ return "ResultRecordMetadata"
169
+ return "Any"
170
+
171
+
161
172
  class State(ObjectBaseModel, Generic[R]):
162
173
  """
163
174
  The state of a run.
@@ -168,9 +179,14 @@ class State(ObjectBaseModel, Generic[R]):
168
179
  timestamp: DateTime = Field(default_factory=lambda: pendulum.now("UTC"))
169
180
  message: Optional[str] = Field(default=None, examples=["Run started"])
170
181
  state_details: StateDetails = Field(default_factory=StateDetails)
171
- data: Union["BaseResult[R]", Any] = Field(
172
- default=None,
173
- )
182
+ data: Annotated[
183
+ Union[
184
+ Annotated["BaseResult[R]", Tag("BaseResult")],
185
+ Annotated["ResultRecordMetadata", Tag("ResultRecordMetadata")],
186
+ Annotated[Any, Tag("Any")],
187
+ ],
188
+ Discriminator(data_discriminator),
189
+ ] = Field(default=None)
174
190
 
175
191
  @overload
176
192
  def result(self: "State[R]", raise_on_failure: bool = True) -> R:
@@ -276,10 +292,12 @@ class State(ObjectBaseModel, Generic[R]):
276
292
  results should be sent to the API. Other data is only available locally.
277
293
  """
278
294
  from prefect.client.schemas.actions import StateCreate
279
- from prefect.results import BaseResult
295
+ from prefect.results import BaseResult, ResultRecord, should_persist_result
280
296
 
281
- if isinstance(self.data, BaseResult) and self.data.serialize_to_none is False:
297
+ if isinstance(self.data, BaseResult):
282
298
  data = self.data
299
+ elif isinstance(self.data, ResultRecord) and should_persist_result():
300
+ data = self.data.metadata
283
301
  else:
284
302
  data = None
285
303
 
@@ -7,5 +7,5 @@ if TYPE_CHECKING:
7
7
  from prefect.client.schemas.schedules import SCHEDULE_TYPES
8
8
 
9
9
  FlexibleScheduleList: TypeAlias = Sequence[
10
- Union[DeploymentScheduleCreate, dict[str, Any], "SCHEDULE_TYPES"]
10
+ Union["DeploymentScheduleCreate", dict[str, Any], "SCHEDULE_TYPES"]
11
11
  ]