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.
- prefect/__init__.py +0 -3
- prefect/_internal/compatibility/migration.py +1 -1
- prefect/artifacts.py +1 -1
- prefect/blocks/core.py +8 -5
- prefect/blocks/notifications.py +10 -10
- prefect/blocks/system.py +52 -16
- prefect/blocks/webhook.py +3 -1
- prefect/client/cloud.py +57 -7
- prefect/client/collections.py +1 -1
- prefect/client/orchestration.py +68 -7
- prefect/client/schemas/objects.py +40 -2
- prefect/concurrency/asyncio.py +8 -2
- prefect/concurrency/services.py +16 -6
- prefect/concurrency/sync.py +4 -1
- prefect/context.py +7 -9
- prefect/deployments/runner.py +3 -3
- prefect/exceptions.py +12 -0
- prefect/filesystems.py +5 -3
- prefect/flow_engine.py +16 -10
- prefect/flows.py +2 -4
- prefect/futures.py +2 -1
- prefect/locking/__init__.py +0 -0
- prefect/locking/memory.py +213 -0
- prefect/locking/protocol.py +122 -0
- prefect/logging/handlers.py +4 -1
- prefect/main.py +8 -6
- prefect/records/filesystem.py +4 -2
- prefect/records/result_store.py +12 -6
- prefect/results.py +768 -363
- prefect/settings.py +24 -10
- prefect/states.py +82 -27
- prefect/task_engine.py +51 -26
- prefect/task_worker.py +6 -4
- prefect/tasks.py +24 -6
- prefect/transactions.py +57 -36
- prefect/utilities/annotations.py +4 -3
- prefect/utilities/asyncutils.py +1 -1
- prefect/utilities/callables.py +1 -3
- prefect/utilities/dispatch.py +16 -11
- prefect/utilities/schema_tools/hydration.py +13 -0
- prefect/variables.py +34 -24
- prefect/workers/base.py +78 -18
- prefect/workers/process.py +1 -3
- {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/METADATA +2 -2
- {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/RECORD +48 -46
- prefect/manifests.py +0 -21
- {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/LICENSE +0 -0
- {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/WHEEL +0 -0
- {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/
|
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/
|
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
|
555
|
-
|
556
|
-
|
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
|
prefect/blocks/notifications.py
CHANGED
@@ -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/
|
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/
|
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/
|
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/
|
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/
|
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/
|
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/
|
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/
|
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/
|
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/
|
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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
from
|
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/
|
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/
|
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/
|
102
|
+
_documentation_url = "https://docs.prefect.io/latest/develop/blocks"
|
88
103
|
|
89
|
-
value:
|
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
|
113
|
+
this block is viewed or edited in the UI.
|
99
114
|
|
100
115
|
Attributes:
|
101
|
-
value: A
|
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/
|
132
|
+
_documentation_url = "https://docs.prefect.io/latest/develop/blocks"
|
116
133
|
|
117
|
-
value: SecretStr = Field(
|
118
|
-
default=...,
|
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
|
-
|
122
|
-
|
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 =
|
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
|
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
|
-
|
90
|
-
|
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:
|
prefect/client/collections.py
CHANGED
@@ -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
|
prefect/client/orchestration.py
CHANGED
@@ -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=
|
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.
|
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=
|
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(
|
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 {}),
|
prefect/concurrency/asyncio.py
CHANGED
@@ -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:
|
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(
|
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):
|