prefect-client 3.0.0rc19__py3-none-any.whl → 3.0.1__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 (49) hide show
  1. prefect/__init__.py +0 -3
  2. prefect/_internal/compatibility/migration.py +1 -1
  3. prefect/artifacts.py +1 -1
  4. prefect/blocks/core.py +8 -5
  5. prefect/blocks/notifications.py +10 -10
  6. prefect/blocks/system.py +52 -16
  7. prefect/blocks/webhook.py +3 -1
  8. prefect/client/cloud.py +57 -7
  9. prefect/client/collections.py +1 -1
  10. prefect/client/orchestration.py +68 -7
  11. prefect/client/schemas/objects.py +40 -2
  12. prefect/concurrency/asyncio.py +8 -2
  13. prefect/concurrency/services.py +16 -6
  14. prefect/concurrency/sync.py +4 -1
  15. prefect/context.py +7 -9
  16. prefect/deployments/runner.py +3 -3
  17. prefect/exceptions.py +12 -0
  18. prefect/filesystems.py +5 -3
  19. prefect/flow_engine.py +16 -10
  20. prefect/flows.py +2 -4
  21. prefect/futures.py +2 -1
  22. prefect/locking/__init__.py +0 -0
  23. prefect/locking/memory.py +213 -0
  24. prefect/locking/protocol.py +122 -0
  25. prefect/logging/handlers.py +4 -1
  26. prefect/main.py +8 -6
  27. prefect/records/filesystem.py +4 -2
  28. prefect/records/result_store.py +12 -6
  29. prefect/results.py +768 -363
  30. prefect/settings.py +24 -10
  31. prefect/states.py +82 -27
  32. prefect/task_engine.py +51 -26
  33. prefect/task_worker.py +6 -4
  34. prefect/tasks.py +24 -6
  35. prefect/transactions.py +57 -36
  36. prefect/utilities/annotations.py +4 -3
  37. prefect/utilities/asyncutils.py +1 -1
  38. prefect/utilities/callables.py +1 -3
  39. prefect/utilities/dispatch.py +16 -11
  40. prefect/utilities/schema_tools/hydration.py +13 -0
  41. prefect/variables.py +34 -24
  42. prefect/workers/base.py +78 -18
  43. prefect/workers/process.py +1 -3
  44. {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/METADATA +2 -2
  45. {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/RECORD +48 -46
  46. prefect/manifests.py +0 -21
  47. {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/LICENSE +0 -0
  48. {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/WHEEL +0 -0
  49. {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/top_level.txt +0 -0
prefect/__init__.py CHANGED
@@ -31,7 +31,6 @@ if TYPE_CHECKING:
31
31
  Flow,
32
32
  get_client,
33
33
  get_run_logger,
34
- Manifest,
35
34
  State,
36
35
  tags,
37
36
  task,
@@ -60,7 +59,6 @@ _public_api: dict[str, tuple[str, str]] = {
60
59
  "Flow": (__spec__.parent, ".main"),
61
60
  "get_client": (__spec__.parent, ".main"),
62
61
  "get_run_logger": (__spec__.parent, ".main"),
63
- "Manifest": (__spec__.parent, ".main"),
64
62
  "State": (__spec__.parent, ".main"),
65
63
  "tags": (__spec__.parent, ".main"),
66
64
  "task": (__spec__.parent, ".main"),
@@ -81,7 +79,6 @@ __all__ = [
81
79
  "Flow",
82
80
  "get_client",
83
81
  "get_run_logger",
84
- "Manifest",
85
82
  "State",
86
83
  "tags",
87
84
  "task",
@@ -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
@@ -150,7 +150,11 @@ def _collect_secret_fields(
150
150
  _collect_secret_fields(f"{name}.{field_name}", field.annotation, secrets)
151
151
  return
152
152
 
153
- if type_ in (SecretStr, SecretBytes):
153
+ if type_ in (SecretStr, SecretBytes) or (
154
+ isinstance(type_, type)
155
+ and getattr(type_, "__module__", None) == "pydantic.types"
156
+ and getattr(type_, "__name__", None) == "Secret"
157
+ ):
154
158
  secrets.append(name)
155
159
  elif type_ == SecretDict:
156
160
  # Append .* to field name to signify that all values under this
@@ -551,10 +555,9 @@ class Block(BaseModel, ABC):
551
555
  `<module>:11: No type or annotation for parameter 'write_json'`
552
556
  because griffe is unable to parse the types from pydantic.BaseModel.
553
557
  """
554
- with disable_logger("griffe.docstrings.google"):
555
- with disable_logger("griffe.agents.nodes"):
556
- docstring = Docstring(cls.__doc__)
557
- parsed = parse(docstring, Parser.google)
558
+ with disable_logger("griffe"):
559
+ docstring = Docstring(cls.__doc__)
560
+ parsed = parse(docstring, Parser.google)
558
561
  return parsed
559
562
 
560
563
  @classmethod
@@ -73,7 +73,7 @@ class AppriseNotificationBlock(AbstractAppriseNotificationBlock, ABC):
73
73
  A base class for sending notifications using Apprise, through webhook URLs.
74
74
  """
75
75
 
76
- _documentation_url = "https://docs.prefect.io/ui/notifications/"
76
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
77
77
  url: SecretStr = Field(
78
78
  default=...,
79
79
  title="Webhook URL",
@@ -100,7 +100,7 @@ class SlackWebhook(AppriseNotificationBlock):
100
100
 
101
101
  _block_type_name = "Slack Webhook"
102
102
  _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"
103
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
104
104
 
105
105
  url: SecretStr = Field(
106
106
  default=...,
@@ -126,7 +126,7 @@ class MicrosoftTeamsWebhook(AppriseNotificationBlock):
126
126
  _block_type_name = "Microsoft Teams Webhook"
127
127
  _block_type_slug = "ms-teams-webhook"
128
128
  _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"
129
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
130
130
 
131
131
  url: SecretStr = Field(
132
132
  default=...,
@@ -181,7 +181,7 @@ class PagerDutyWebHook(AbstractAppriseNotificationBlock):
181
181
  _block_type_name = "Pager Duty Webhook"
182
182
  _block_type_slug = "pager-duty-webhook"
183
183
  _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"
184
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
185
185
 
186
186
  # The default cannot be prefect_default because NotifyPagerDuty's
187
187
  # PAGERDUTY_SEVERITY_MAP only has these notify types defined as keys
@@ -291,7 +291,7 @@ class TwilioSMS(AbstractAppriseNotificationBlock):
291
291
  _block_type_name = "Twilio SMS"
292
292
  _block_type_slug = "twilio-sms"
293
293
  _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"
294
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
295
295
 
296
296
  account_sid: str = Field(
297
297
  default=...,
@@ -360,7 +360,7 @@ class OpsgenieWebhook(AbstractAppriseNotificationBlock):
360
360
  _block_type_name = "Opsgenie Webhook"
361
361
  _block_type_slug = "opsgenie-webhook"
362
362
  _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"
363
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
364
364
 
365
365
  apikey: SecretStr = Field(
366
366
  default=...,
@@ -478,7 +478,7 @@ class MattermostWebhook(AbstractAppriseNotificationBlock):
478
478
  _block_type_name = "Mattermost Webhook"
479
479
  _block_type_slug = "mattermost-webhook"
480
480
  _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"
481
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
482
482
 
483
483
  hostname: str = Field(
484
484
  default=...,
@@ -559,7 +559,7 @@ class DiscordWebhook(AbstractAppriseNotificationBlock):
559
559
  _block_type_name = "Discord Webhook"
560
560
  _block_type_slug = "discord-webhook"
561
561
  _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"
562
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
563
563
 
564
564
  webhook_id: SecretStr = Field(
565
565
  default=...,
@@ -658,7 +658,7 @@ class CustomWebhookNotificationBlock(NotificationBlock):
658
658
 
659
659
  _block_type_name = "Custom Webhook"
660
660
  _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"
661
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
662
662
 
663
663
  name: str = Field(title="Name", description="Name of the webhook.")
664
664
 
@@ -789,7 +789,7 @@ class SendgridEmail(AbstractAppriseNotificationBlock):
789
789
  _block_type_name = "Sendgrid Email"
790
790
  _block_type_slug = "sendgrid-email"
791
791
  _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"
792
+ _documentation_url = "https://docs.prefect.io/latest/automate/events/automations-triggers#sending-notifications-with-automations"
793
793
 
794
794
  api_key: SecretStr = Field(
795
795
  default=...,
prefect/blocks/system.py CHANGED
@@ -1,11 +1,26 @@
1
- from typing import Any
2
-
3
- from pydantic import Field, SecretStr
4
- from pydantic_extra_types.pendulum_dt import DateTime
1
+ import json
2
+ from typing import Annotated, Any, Generic, TypeVar, Union
3
+
4
+ from pydantic import (
5
+ Field,
6
+ JsonValue,
7
+ SecretStr,
8
+ StrictStr,
9
+ field_validator,
10
+ )
11
+ from pydantic import Secret as PydanticSecret
12
+ from pydantic_extra_types.pendulum_dt import DateTime as PydanticDateTime
5
13
 
6
14
  from prefect._internal.compatibility.deprecated import deprecated_class
7
15
  from prefect.blocks.core import Block
8
16
 
17
+ _SecretValueType = Union[
18
+ Annotated[StrictStr, Field(title="string")],
19
+ Annotated[JsonValue, Field(title="JSON")],
20
+ ]
21
+
22
+ T = TypeVar("T", bound=_SecretValueType)
23
+
9
24
 
10
25
  @deprecated_class(
11
26
  start_date="Jun 2024",
@@ -29,7 +44,7 @@ class JSON(Block):
29
44
  """
30
45
 
31
46
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/4fcef2294b6eeb423b1332d1ece5156bf296ff96-48x48.png"
32
- _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"
33
48
 
34
49
  value: Any = Field(default=..., description="A JSON-compatible value.")
35
50
 
@@ -56,7 +71,7 @@ class String(Block):
56
71
  """
57
72
 
58
73
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/c262ea2c80a2c043564e8763f3370c3db5a6b3e6-48x48.png"
59
- _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"
60
75
 
61
76
  value: str = Field(default=..., description="A string value.")
62
77
 
@@ -84,26 +99,28 @@ class DateTime(Block):
84
99
 
85
100
  _block_type_name = "Date Time"
86
101
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/8b3da9a6621e92108b8e6a75b82e15374e170ff7-48x48.png"
87
- _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"
88
103
 
89
- value: DateTime = Field(
104
+ value: PydanticDateTime = Field(
90
105
  default=...,
91
106
  description="An ISO 8601-compatible datetime value.",
92
107
  )
93
108
 
94
109
 
95
- class Secret(Block):
110
+ class Secret(Block, Generic[T]):
96
111
  """
97
112
  A block that represents a secret value. The value stored in this block will be obfuscated when
98
- this block is logged or shown in the UI.
113
+ this block is viewed or edited in the UI.
99
114
 
100
115
  Attributes:
101
- value: A string value that should be kept secret.
116
+ value: A value that should be kept secret.
102
117
 
103
118
  Example:
104
119
  ```python
105
120
  from prefect.blocks.system import Secret
106
121
 
122
+ Secret(value="sk-1234567890").save("BLOCK_NAME", overwrite=True)
123
+
107
124
  secret_block = Secret.load("BLOCK_NAME")
108
125
 
109
126
  # Access the stored secret
@@ -112,11 +129,30 @@ class Secret(Block):
112
129
  """
113
130
 
114
131
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/c6f20e556dd16effda9df16551feecfb5822092b-48x48.png"
115
- _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"
116
133
 
117
- value: SecretStr = Field(
118
- default=..., description="A string value that should be kept secret."
134
+ value: Union[SecretStr, PydanticSecret[T]] = Field(
135
+ default=...,
136
+ description="A value that should be kept secret.",
137
+ examples=["sk-1234567890", {"username": "johndoe", "password": "s3cr3t"}],
138
+ json_schema_extra={
139
+ "writeOnly": True,
140
+ "format": "password",
141
+ },
119
142
  )
120
143
 
121
- def get(self):
122
- return self.value.get_secret_value()
144
+ @field_validator("value", mode="before")
145
+ def validate_value(
146
+ cls, value: Union[T, SecretStr, PydanticSecret[T]]
147
+ ) -> Union[SecretStr, PydanticSecret[T]]:
148
+ if isinstance(value, (PydanticSecret, SecretStr)):
149
+ return value
150
+ else:
151
+ return PydanticSecret[type(value)](value)
152
+
153
+ def get(self) -> T:
154
+ try:
155
+ value = self.value.get_secret_value()
156
+ return json.loads(value)
157
+ except (TypeError, json.JSONDecodeError):
158
+ return value
prefect/blocks/webhook.py CHANGED
@@ -19,7 +19,9 @@ class Webhook(Block):
19
19
 
20
20
  _block_type_name = "Webhook"
21
21
  _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"
22
+ _documentation_url = (
23
+ "https://docs.prefect.io/latest/automate/events/webhook-triggers"
24
+ )
23
25
 
24
26
  method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"] = Field(
25
27
  default="POST", description="The webhook request method. Defaults to `POST`."
prefect/client/cloud.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import re
2
- from typing import Any, Dict, List, Optional
2
+ from typing import Any, Dict, List, Optional, cast
3
3
 
4
4
  import anyio
5
5
  import httpx
@@ -9,7 +9,11 @@ from starlette import status
9
9
  import prefect.context
10
10
  import prefect.settings
11
11
  from prefect.client.base import PrefectHttpxAsyncClient
12
- from prefect.client.schemas.objects import Workspace
12
+ from prefect.client.schemas.objects import (
13
+ IPAllowlist,
14
+ IPAllowlistMyAccessResponse,
15
+ Workspace,
16
+ )
13
17
  from prefect.exceptions import ObjectNotFound, PrefectException
14
18
  from prefect.settings import (
15
19
  PREFECT_API_KEY,
@@ -69,6 +73,27 @@ class CloudClient:
69
73
  **httpx_settings, enable_csrf_support=False
70
74
  )
71
75
 
76
+ api_url = prefect.settings.PREFECT_API_URL.value() or ""
77
+ if match := (
78
+ re.search(PARSE_API_URL_REGEX, host)
79
+ or re.search(PARSE_API_URL_REGEX, api_url)
80
+ ):
81
+ self.account_id, self.workspace_id = match.groups()
82
+
83
+ @property
84
+ def account_base_url(self) -> str:
85
+ if not self.account_id:
86
+ raise ValueError("Account ID not set")
87
+
88
+ return f"accounts/{self.account_id}"
89
+
90
+ @property
91
+ def workspace_base_url(self) -> str:
92
+ if not self.workspace_id:
93
+ raise ValueError("Workspace ID not set")
94
+
95
+ return f"{self.account_base_url}/workspaces/{self.workspace_id}"
96
+
72
97
  async def api_healthcheck(self):
73
98
  """
74
99
  Attempts to connect to the Cloud API and raises the encountered exception if not
@@ -86,11 +111,36 @@ class CloudClient:
86
111
  return workspaces
87
112
 
88
113
  async def read_worker_metadata(self) -> Dict[str, Any]:
89
- configured_url = prefect.settings.PREFECT_API_URL.value()
90
- account_id, workspace_id = re.findall(PARSE_API_URL_REGEX, configured_url)[0]
91
- return await self.get(
92
- f"accounts/{account_id}/workspaces/{workspace_id}/collections/work_pool_types"
114
+ response = await self.get(
115
+ f"{self.workspace_base_url}/collections/work_pool_types"
93
116
  )
117
+ return cast(Dict[str, Any], response)
118
+
119
+ async def read_account_settings(self) -> Dict[str, Any]:
120
+ response = await self.get(f"{self.account_base_url}/settings")
121
+ return cast(Dict[str, Any], response)
122
+
123
+ async def update_account_settings(self, settings: Dict[str, Any]):
124
+ await self.request(
125
+ "PATCH",
126
+ f"{self.account_base_url}/settings",
127
+ json=settings,
128
+ )
129
+
130
+ async def read_account_ip_allowlist(self) -> IPAllowlist:
131
+ response = await self.get(f"{self.account_base_url}/ip_allowlist")
132
+ return IPAllowlist.model_validate(response)
133
+
134
+ async def update_account_ip_allowlist(self, updated_allowlist: IPAllowlist):
135
+ await self.request(
136
+ "PUT",
137
+ f"{self.account_base_url}/ip_allowlist",
138
+ json=updated_allowlist.model_dump(mode="json"),
139
+ )
140
+
141
+ async def check_ip_allowlist_access(self) -> IPAllowlistMyAccessResponse:
142
+ response = await self.get(f"{self.account_base_url}/ip_allowlist/my_access")
143
+ return IPAllowlistMyAccessResponse.model_validate(response)
94
144
 
95
145
  async def __aenter__(self):
96
146
  await self._client.__aenter__()
@@ -120,7 +170,7 @@ class CloudClient:
120
170
  status.HTTP_401_UNAUTHORIZED,
121
171
  status.HTTP_403_FORBIDDEN,
122
172
  ):
123
- raise CloudUnauthorizedError
173
+ raise CloudUnauthorizedError(str(exc)) from exc
124
174
  elif exc.response.status_code == status.HTTP_404_NOT_FOUND:
125
175
  raise ObjectNotFound(http_exc=exc) from exc
126
176
  else:
@@ -29,6 +29,6 @@ def get_collections_metadata_client(
29
29
  """
30
30
  orchestration_client = get_client(httpx_settings=httpx_settings)
31
31
  if orchestration_client.server_type == ServerType.CLOUD:
32
- return get_cloud_client(httpx_settings=httpx_settings)
32
+ return get_cloud_client(httpx_settings=httpx_settings, infer_cloud_url=True)
33
33
  else:
34
34
  return orchestration_client
@@ -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
 
@@ -1324,15 +1325,17 @@ class PrefectClient:
1324
1325
  `SecretBytes` fields. Note Blocks may not work as expected if
1325
1326
  this is set to `False`.
1326
1327
  """
1328
+ block_document_data = block_document.model_dump(
1329
+ mode="json",
1330
+ exclude_unset=True,
1331
+ exclude={"id", "block_schema", "block_type"},
1332
+ context={"include_secrets": include_secrets},
1333
+ serialize_as_any=True,
1334
+ )
1327
1335
  try:
1328
1336
  response = await self._client.post(
1329
1337
  "/block_documents/",
1330
- json=block_document.model_dump(
1331
- mode="json",
1332
- exclude_unset=True,
1333
- exclude={"id", "block_schema", "block_type"},
1334
- context={"include_secrets": include_secrets},
1335
- ),
1338
+ json=block_document_data,
1336
1339
  )
1337
1340
  except httpx.HTTPStatusError as e:
1338
1341
  if e.response.status_code == status.HTTP_409_CONFLICT:
@@ -1786,6 +1789,12 @@ class PrefectClient:
1786
1789
  Returns:
1787
1790
  a [Deployment model][prefect.client.schemas.objects.Deployment] representation of the deployment
1788
1791
  """
1792
+ if not isinstance(deployment_id, UUID):
1793
+ try:
1794
+ deployment_id = UUID(deployment_id)
1795
+ except ValueError:
1796
+ raise ValueError(f"Invalid deployment ID: {deployment_id}")
1797
+
1789
1798
  try:
1790
1799
  response = await self._client.get(f"/deployments/{deployment_id}")
1791
1800
  except httpx.HTTPStatusError as e:
@@ -3321,6 +3330,32 @@ class PrefectClient:
3321
3330
  async def delete_resource_owned_automations(self, resource_id: str):
3322
3331
  await self._client.delete(f"/automations/owned-by/{resource_id}")
3323
3332
 
3333
+ async def api_version(self) -> str:
3334
+ res = await self._client.get("/admin/version")
3335
+ return res.json()
3336
+
3337
+ def client_version(self) -> str:
3338
+ return prefect.__version__
3339
+
3340
+ async def raise_for_api_version_mismatch(self):
3341
+ # Cloud is always compatible as a server
3342
+ if self.server_type == ServerType.CLOUD:
3343
+ return
3344
+
3345
+ try:
3346
+ api_version = await self.api_version()
3347
+ except Exception as e:
3348
+ raise RuntimeError(f"Failed to reach API at {self.api_url}") from e
3349
+
3350
+ api_version = version.parse(api_version)
3351
+ client_version = version.parse(self.client_version())
3352
+
3353
+ if api_version.major != client_version.major:
3354
+ raise RuntimeError(
3355
+ f"Found incompatible versions: client: {client_version}, server: {api_version}. "
3356
+ f"Major versions must match."
3357
+ )
3358
+
3324
3359
  async def __aenter__(self):
3325
3360
  """
3326
3361
  Start the client.
@@ -3614,6 +3649,32 @@ class SyncPrefectClient:
3614
3649
  """
3615
3650
  return self._client.get("/hello")
3616
3651
 
3652
+ def api_version(self) -> str:
3653
+ res = self._client.get("/admin/version")
3654
+ return res.json()
3655
+
3656
+ def client_version(self) -> str:
3657
+ return prefect.__version__
3658
+
3659
+ def raise_for_api_version_mismatch(self):
3660
+ # Cloud is always compatible as a server
3661
+ if self.server_type == ServerType.CLOUD:
3662
+ return
3663
+
3664
+ try:
3665
+ api_version = self.api_version()
3666
+ except Exception as e:
3667
+ raise RuntimeError(f"Failed to reach API at {self.api_url}") from e
3668
+
3669
+ api_version = version.parse(api_version)
3670
+ client_version = version.parse(self.client_version())
3671
+
3672
+ if api_version.major != client_version.major:
3673
+ raise RuntimeError(
3674
+ f"Found incompatible versions: client: {client_version}, server: {api_version}. "
3675
+ f"Major versions must match."
3676
+ )
3677
+
3617
3678
  def create_flow(self, flow: "FlowObject") -> UUID:
3618
3679
  """
3619
3680
  Create a flow in the Prefect API.
@@ -4139,7 +4200,7 @@ class SyncPrefectClient:
4139
4200
 
4140
4201
  response = self._client.post(
4141
4202
  "/artifacts/",
4142
- json=artifact.model_dump_json(exclude_unset=True),
4203
+ json=artifact.model_dump(mode="json", exclude_unset=True),
4143
4204
  )
4144
4205
 
4145
4206
  return Artifact.model_validate(response.json())
@@ -19,11 +19,13 @@ from pydantic import (
19
19
  ConfigDict,
20
20
  Field,
21
21
  HttpUrl,
22
+ IPvAnyNetwork,
22
23
  SerializationInfo,
23
24
  field_validator,
24
25
  model_serializer,
25
26
  model_validator,
26
27
  )
28
+ from pydantic.functional_validators import ModelWrapValidatorHandler
27
29
  from pydantic_extra_types.pendulum_dt import DateTime
28
30
  from typing_extensions import Literal, Self, TypeVar
29
31
 
@@ -276,11 +278,16 @@ class State(ObjectBaseModel, Generic[R]):
276
278
  from prefect.client.schemas.actions import StateCreate
277
279
  from prefect.results import BaseResult
278
280
 
281
+ if isinstance(self.data, BaseResult) and self.data.serialize_to_none is False:
282
+ data = self.data
283
+ else:
284
+ data = None
285
+
279
286
  return StateCreate(
280
287
  type=self.type,
281
288
  name=self.name,
282
289
  message=self.message,
283
- data=self.data if isinstance(self.data, BaseResult) else None,
290
+ data=data,
284
291
  state_details=self.state_details,
285
292
  )
286
293
 
@@ -848,6 +855,35 @@ class Workspace(PrefectBaseModel):
848
855
  return hash(self.handle)
849
856
 
850
857
 
858
+ class IPAllowlistEntry(PrefectBaseModel):
859
+ ip_network: IPvAnyNetwork
860
+ enabled: bool
861
+ description: Optional[str] = Field(
862
+ default=None, description="A description of the IP entry."
863
+ )
864
+ last_seen: Optional[str] = Field(
865
+ default=None,
866
+ description="The last time this IP was seen accessing Prefect Cloud.",
867
+ )
868
+
869
+
870
+ class IPAllowlist(PrefectBaseModel):
871
+ """
872
+ A Prefect Cloud IP allowlist.
873
+
874
+ Expected payload for an IP allowlist from the Prefect Cloud API.
875
+ """
876
+
877
+ entries: List[IPAllowlistEntry]
878
+
879
+
880
+ class IPAllowlistMyAccessResponse(PrefectBaseModel):
881
+ """Expected payload for an IP allowlist access response from the Prefect Cloud API."""
882
+
883
+ allowed: bool
884
+ detail: str
885
+
886
+
851
887
  class BlockType(ObjectBaseModel):
852
888
  """An ORM representation of a block type"""
853
889
 
@@ -933,7 +969,9 @@ class BlockDocument(ObjectBaseModel):
933
969
  return validate_name_present_on_nonanonymous_blocks(values)
934
970
 
935
971
  @model_serializer(mode="wrap")
936
- def serialize_data(self, handler, info: SerializationInfo):
972
+ def serialize_data(
973
+ self, handler: ModelWrapValidatorHandler, info: SerializationInfo
974
+ ):
937
975
  self.data = visit_collection(
938
976
  self.data,
939
977
  visit_fn=partial(handle_secret_render, context=info.context or {}),
@@ -36,7 +36,8 @@ async def concurrency(
36
36
  names: Union[str, List[str]],
37
37
  occupy: int = 1,
38
38
  timeout_seconds: Optional[float] = None,
39
- create_if_missing: Optional[bool] = True,
39
+ create_if_missing: bool = True,
40
+ max_retries: Optional[int] = None,
40
41
  ) -> AsyncGenerator[None, None]:
41
42
  """A context manager that acquires and releases concurrency slots from the
42
43
  given concurrency limits.
@@ -47,6 +48,7 @@ async def concurrency(
47
48
  timeout_seconds: The number of seconds to wait for the slots to be acquired before
48
49
  raising a `TimeoutError`. A timeout of `None` will wait indefinitely.
49
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.
50
52
 
51
53
  Raises:
52
54
  TimeoutError: If the slots are not acquired within the given timeout.
@@ -75,6 +77,7 @@ async def concurrency(
75
77
  occupy,
76
78
  timeout_seconds=timeout_seconds,
77
79
  create_if_missing=create_if_missing,
80
+ max_retries=max_retries,
78
81
  )
79
82
  acquisition_time = pendulum.now("UTC")
80
83
  emitted_events = _emit_concurrency_acquisition_events(limits, occupy)
@@ -137,9 +140,12 @@ async def _acquire_concurrency_slots(
137
140
  mode: Union[Literal["concurrency"], Literal["rate_limit"]] = "concurrency",
138
141
  timeout_seconds: Optional[float] = None,
139
142
  create_if_missing: Optional[bool] = True,
143
+ max_retries: Optional[int] = None,
140
144
  ) -> List[MinimalConcurrencyLimitResponse]:
141
145
  service = ConcurrencySlotAcquisitionService.instance(frozenset(names))
142
- future = service.send((slots, mode, timeout_seconds, create_if_missing))
146
+ future = service.send(
147
+ (slots, mode, timeout_seconds, create_if_missing, max_retries)
148
+ )
143
149
  response_or_exception = await asyncio.wrap_future(future)
144
150
 
145
151
  if isinstance(response_or_exception, Exception):