prefect-client 3.0.0rc13__py3-none-any.whl → 3.0.0rc15__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/_internal/compatibility/deprecated.py +0 -53
- prefect/blocks/core.py +132 -4
- prefect/blocks/notifications.py +26 -3
- prefect/client/base.py +30 -24
- prefect/client/orchestration.py +121 -47
- prefect/client/utilities.py +4 -4
- prefect/concurrency/asyncio.py +48 -7
- prefect/concurrency/context.py +24 -0
- prefect/concurrency/services.py +24 -8
- prefect/concurrency/sync.py +30 -3
- prefect/context.py +85 -24
- prefect/events/clients.py +93 -60
- prefect/events/utilities.py +0 -2
- prefect/events/worker.py +9 -2
- prefect/flow_engine.py +6 -3
- prefect/flows.py +176 -12
- prefect/futures.py +84 -7
- prefect/profiles.toml +16 -2
- prefect/runner/runner.py +6 -1
- prefect/runner/storage.py +4 -0
- prefect/settings.py +108 -14
- prefect/task_engine.py +901 -285
- prefect/task_runs.py +24 -1
- prefect/task_worker.py +7 -1
- prefect/tasks.py +9 -5
- prefect/utilities/asyncutils.py +0 -6
- prefect/utilities/callables.py +5 -3
- prefect/utilities/engine.py +3 -0
- prefect/utilities/importtools.py +138 -58
- prefect/utilities/schema_tools/validation.py +30 -0
- prefect/utilities/services.py +32 -0
- {prefect_client-3.0.0rc13.dist-info → prefect_client-3.0.0rc15.dist-info}/METADATA +39 -39
- {prefect_client-3.0.0rc13.dist-info → prefect_client-3.0.0rc15.dist-info}/RECORD +36 -35
- {prefect_client-3.0.0rc13.dist-info → prefect_client-3.0.0rc15.dist-info}/WHEEL +1 -1
- {prefect_client-3.0.0rc13.dist-info → prefect_client-3.0.0rc15.dist-info}/LICENSE +0 -0
- {prefect_client-3.0.0rc13.dist-info → prefect_client-3.0.0rc15.dist-info}/top_level.txt +0 -0
@@ -16,7 +16,6 @@ import warnings
|
|
16
16
|
from typing import Any, Callable, List, Optional, Type, TypeVar
|
17
17
|
|
18
18
|
import pendulum
|
19
|
-
import wrapt
|
20
19
|
from pydantic import BaseModel
|
21
20
|
|
22
21
|
from prefect.utilities.callables import get_call_parameters
|
@@ -273,55 +272,3 @@ def register_renamed_module(old_name: str, new_name: str, start_date: str):
|
|
273
272
|
DEPRECATED_MODULE_ALIASES.append(
|
274
273
|
AliasedModuleDefinition(old_name, new_name, callback)
|
275
274
|
)
|
276
|
-
|
277
|
-
|
278
|
-
class AsyncCompatProxy(wrapt.ObjectProxy):
|
279
|
-
"""
|
280
|
-
A proxy object that allows for awaiting a method that is no longer async.
|
281
|
-
|
282
|
-
See https://wrapt.readthedocs.io/en/master/wrappers.html#object-proxy for more
|
283
|
-
"""
|
284
|
-
|
285
|
-
def __init__(self, wrapped, class_name: str, method_name: str):
|
286
|
-
super().__init__(wrapped)
|
287
|
-
self._self_class_name = class_name
|
288
|
-
self._self_method_name = method_name
|
289
|
-
self._self_already_awaited = False
|
290
|
-
|
291
|
-
def __await__(self):
|
292
|
-
if not self._self_already_awaited:
|
293
|
-
warnings.warn(
|
294
|
-
(
|
295
|
-
f"The {self._self_method_name!r} method on {self._self_class_name!r}"
|
296
|
-
" is no longer async and awaiting it will raise an error after Dec 2024"
|
297
|
-
" - please remove the `await` keyword."
|
298
|
-
),
|
299
|
-
DeprecationWarning,
|
300
|
-
stacklevel=2,
|
301
|
-
)
|
302
|
-
self._self_already_awaited = True
|
303
|
-
yield
|
304
|
-
return self.__wrapped__
|
305
|
-
|
306
|
-
def __repr__(self):
|
307
|
-
return repr(self.__wrapped__)
|
308
|
-
|
309
|
-
def __reduce_ex__(self, protocol):
|
310
|
-
return (
|
311
|
-
type(self),
|
312
|
-
(self.__wrapped__,),
|
313
|
-
{"_self_already_awaited": self._self_already_awaited},
|
314
|
-
)
|
315
|
-
|
316
|
-
|
317
|
-
def deprecated_async_method(wrapped):
|
318
|
-
"""Decorator that wraps a sync method to allow awaiting it even though it is no longer async."""
|
319
|
-
|
320
|
-
@wrapt.decorator
|
321
|
-
def wrapper(wrapped, instance, args, kwargs):
|
322
|
-
result = wrapped(*args, **kwargs)
|
323
|
-
return AsyncCompatProxy(
|
324
|
-
result, class_name=instance.__class__.__name__, method_name=wrapped.__name__
|
325
|
-
)
|
326
|
-
|
327
|
-
return wrapper(wrapped)
|
prefect/blocks/core.py
CHANGED
@@ -2,6 +2,7 @@ import hashlib
|
|
2
2
|
import html
|
3
3
|
import inspect
|
4
4
|
import sys
|
5
|
+
import uuid
|
5
6
|
import warnings
|
6
7
|
from abc import ABC
|
7
8
|
from functools import partial
|
@@ -790,6 +791,33 @@ class Block(BaseModel, ABC):
|
|
790
791
|
|
791
792
|
return block_document, block_document_name
|
792
793
|
|
794
|
+
@classmethod
|
795
|
+
@sync_compatible
|
796
|
+
@inject_client
|
797
|
+
async def _get_block_document_by_id(
|
798
|
+
cls,
|
799
|
+
block_document_id: Union[str, uuid.UUID],
|
800
|
+
client: Optional["PrefectClient"] = None,
|
801
|
+
):
|
802
|
+
if isinstance(block_document_id, str):
|
803
|
+
try:
|
804
|
+
block_document_id = UUID(block_document_id)
|
805
|
+
except ValueError:
|
806
|
+
raise ValueError(
|
807
|
+
f"Block document ID {block_document_id!r} is not a valid UUID"
|
808
|
+
)
|
809
|
+
|
810
|
+
try:
|
811
|
+
block_document = await client.read_block_document(
|
812
|
+
block_document_id=block_document_id
|
813
|
+
)
|
814
|
+
except prefect.exceptions.ObjectNotFound:
|
815
|
+
raise ValueError(
|
816
|
+
f"Unable to find block document with ID {block_document_id!r}"
|
817
|
+
)
|
818
|
+
|
819
|
+
return block_document, block_document.name
|
820
|
+
|
793
821
|
@classmethod
|
794
822
|
@sync_compatible
|
795
823
|
@inject_client
|
@@ -874,8 +902,108 @@ class Block(BaseModel, ABC):
|
|
874
902
|
loaded_block.save("my-custom-message", overwrite=True)
|
875
903
|
```
|
876
904
|
"""
|
877
|
-
block_document, block_document_name = await cls._get_block_document(
|
905
|
+
block_document, block_document_name = await cls._get_block_document(
|
906
|
+
name, client=client
|
907
|
+
)
|
908
|
+
|
909
|
+
return cls._load_from_block_document(block_document, validate=validate)
|
910
|
+
|
911
|
+
@classmethod
|
912
|
+
@sync_compatible
|
913
|
+
@inject_client
|
914
|
+
async def load_from_ref(
|
915
|
+
cls,
|
916
|
+
ref: Union[str, UUID, Dict[str, Any]],
|
917
|
+
validate: bool = True,
|
918
|
+
client: Optional["PrefectClient"] = None,
|
919
|
+
) -> "Self":
|
920
|
+
"""
|
921
|
+
Retrieves data from the block document by given reference for the block type
|
922
|
+
that corresponds with the current class and returns an instantiated version of
|
923
|
+
the current class with the data stored in the block document.
|
924
|
+
|
925
|
+
Provided reference can be a block document ID, or a reference data in dictionary format.
|
926
|
+
Supported dictionary reference formats are:
|
927
|
+
- {"block_document_id": <block_document_id>}
|
928
|
+
- {"block_document_slug": <block_document_slug>}
|
929
|
+
|
930
|
+
If a block document for a given block type is saved with a different schema
|
931
|
+
than the current class calling `load`, a warning will be raised.
|
878
932
|
|
933
|
+
If the current class schema is a subset of the block document schema, the block
|
934
|
+
can be loaded as normal using the default `validate = True`.
|
935
|
+
|
936
|
+
If the current class schema is a superset of the block document schema, `load`
|
937
|
+
must be called with `validate` set to False to prevent a validation error. In
|
938
|
+
this case, the block attributes will default to `None` and must be set manually
|
939
|
+
and saved to a new block document before the block can be used as expected.
|
940
|
+
|
941
|
+
Args:
|
942
|
+
ref: The reference to the block document. This can be a block document ID,
|
943
|
+
or one of supported dictionary reference formats.
|
944
|
+
validate: If False, the block document will be loaded without Pydantic
|
945
|
+
validating the block schema. This is useful if the block schema has
|
946
|
+
changed client-side since the block document referred to by `name` was saved.
|
947
|
+
client: The client to use to load the block document. If not provided, the
|
948
|
+
default client will be injected.
|
949
|
+
|
950
|
+
Raises:
|
951
|
+
ValueError: If invalid reference format is provided.
|
952
|
+
ValueError: If the requested block document is not found.
|
953
|
+
|
954
|
+
Returns:
|
955
|
+
An instance of the current class hydrated with the data stored in the
|
956
|
+
block document with the specified name.
|
957
|
+
|
958
|
+
"""
|
959
|
+
block_document = None
|
960
|
+
if isinstance(ref, (str, UUID)):
|
961
|
+
block_document, _ = await cls._get_block_document_by_id(ref)
|
962
|
+
elif isinstance(ref, dict):
|
963
|
+
if block_document_id := ref.get("block_document_id"):
|
964
|
+
block_document, _ = await cls._get_block_document_by_id(
|
965
|
+
block_document_id
|
966
|
+
)
|
967
|
+
elif block_document_slug := ref.get("block_document_slug"):
|
968
|
+
block_document, _ = await cls._get_block_document(block_document_slug)
|
969
|
+
|
970
|
+
if not block_document:
|
971
|
+
raise ValueError(f"Invalid reference format {ref!r}.")
|
972
|
+
|
973
|
+
return cls._load_from_block_document(block_document, validate=validate)
|
974
|
+
|
975
|
+
@classmethod
|
976
|
+
def _load_from_block_document(
|
977
|
+
cls, block_document: BlockDocument, validate: bool = True
|
978
|
+
) -> "Self":
|
979
|
+
"""
|
980
|
+
Loads a block from a given block document.
|
981
|
+
|
982
|
+
If a block document for a given block type is saved with a different schema
|
983
|
+
than the current class calling `load`, a warning will be raised.
|
984
|
+
|
985
|
+
If the current class schema is a subset of the block document schema, the block
|
986
|
+
can be loaded as normal using the default `validate = True`.
|
987
|
+
|
988
|
+
If the current class schema is a superset of the block document schema, `load`
|
989
|
+
must be called with `validate` set to False to prevent a validation error. In
|
990
|
+
this case, the block attributes will default to `None` and must be set manually
|
991
|
+
and saved to a new block document before the block can be used as expected.
|
992
|
+
|
993
|
+
Args:
|
994
|
+
block_document: The block document used to instantiate a block.
|
995
|
+
validate: If False, the block document will be loaded without Pydantic
|
996
|
+
validating the block schema. This is useful if the block schema has
|
997
|
+
changed client-side since the block document referred to by `name` was saved.
|
998
|
+
|
999
|
+
Raises:
|
1000
|
+
ValueError: If the requested block document is not found.
|
1001
|
+
|
1002
|
+
Returns:
|
1003
|
+
An instance of the current class hydrated with the data stored in the
|
1004
|
+
block document with the specified name.
|
1005
|
+
|
1006
|
+
"""
|
879
1007
|
try:
|
880
1008
|
return cls._from_block_document(block_document)
|
881
1009
|
except ValidationError as e:
|
@@ -883,18 +1011,18 @@ class Block(BaseModel, ABC):
|
|
883
1011
|
missing_fields = tuple(err["loc"][0] for err in e.errors())
|
884
1012
|
missing_block_data = {field: None for field in missing_fields}
|
885
1013
|
warnings.warn(
|
886
|
-
f"Could not fully load {
|
1014
|
+
f"Could not fully load {block_document.name!r} of block type"
|
887
1015
|
f" {cls.get_block_type_slug()!r} - this is likely because one or more"
|
888
1016
|
" required fields were added to the schema for"
|
889
1017
|
f" {cls.__name__!r} that did not exist on the class when this block"
|
890
1018
|
" was last saved. Please specify values for new field(s):"
|
891
1019
|
f" {listrepr(missing_fields)}, then run"
|
892
|
-
f' `{cls.__name__}.save("{
|
1020
|
+
f' `{cls.__name__}.save("{block_document.name}", overwrite=True)`,'
|
893
1021
|
" and load this block again before attempting to use it."
|
894
1022
|
)
|
895
1023
|
return cls.model_construct(**block_document.data, **missing_block_data)
|
896
1024
|
raise RuntimeError(
|
897
|
-
f"Unable to load {
|
1025
|
+
f"Unable to load {block_document.name!r} of block type"
|
898
1026
|
f" {cls.get_block_type_slug()!r} due to failed validation. To load without"
|
899
1027
|
" validation, try loading again with `validate=False`."
|
900
1028
|
) from e
|
prefect/blocks/notifications.py
CHANGED
@@ -129,14 +129,37 @@ class MicrosoftTeamsWebhook(AppriseNotificationBlock):
|
|
129
129
|
_documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/notifications/#prefect.blocks.notifications.MicrosoftTeamsWebhook"
|
130
130
|
|
131
131
|
url: SecretStr = Field(
|
132
|
-
|
132
|
+
default=...,
|
133
133
|
title="Webhook URL",
|
134
|
-
description="The
|
134
|
+
description="The Microsoft Power Automate (Workflows) URL used to send notifications to Teams.",
|
135
135
|
examples=[
|
136
|
-
"https://
|
136
|
+
"https://prod-NO.LOCATION.logic.azure.com:443/workflows/WFID/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=SIGNATURE"
|
137
137
|
],
|
138
138
|
)
|
139
139
|
|
140
|
+
include_image: bool = Field(
|
141
|
+
default=True,
|
142
|
+
description="Include an image with the notification.",
|
143
|
+
)
|
144
|
+
|
145
|
+
wrap: bool = Field(
|
146
|
+
default=True,
|
147
|
+
description="Wrap the notification text.",
|
148
|
+
)
|
149
|
+
|
150
|
+
def block_initialization(self) -> None:
|
151
|
+
"""see https://github.com/caronc/apprise/pull/1172"""
|
152
|
+
from apprise.plugins.workflows import NotifyWorkflows
|
153
|
+
|
154
|
+
if not (
|
155
|
+
parsed_url := NotifyWorkflows.parse_native_url(self.url.get_secret_value())
|
156
|
+
):
|
157
|
+
raise ValueError("Invalid Microsoft Teams Workflow URL provided.")
|
158
|
+
|
159
|
+
parsed_url |= {"include_image": self.include_image, "wrap": self.wrap}
|
160
|
+
|
161
|
+
self._start_apprise_client(SecretStr(NotifyWorkflows(**parsed_url).url()))
|
162
|
+
|
140
163
|
|
141
164
|
class PagerDutyWebHook(AbstractAppriseNotificationBlock):
|
142
165
|
"""
|
prefect/client/base.py
CHANGED
@@ -26,7 +26,6 @@ import httpx
|
|
26
26
|
from asgi_lifespan import LifespanManager
|
27
27
|
from httpx import HTTPStatusError, Request, Response
|
28
28
|
from starlette import status
|
29
|
-
from starlette.testclient import TestClient
|
30
29
|
from typing_extensions import Self
|
31
30
|
|
32
31
|
import prefect
|
@@ -35,10 +34,14 @@ from prefect.client.schemas.objects import CsrfToken
|
|
35
34
|
from prefect.exceptions import PrefectHTTPStatusError
|
36
35
|
from prefect.logging import get_logger
|
37
36
|
from prefect.settings import (
|
37
|
+
PREFECT_API_URL,
|
38
38
|
PREFECT_CLIENT_MAX_RETRIES,
|
39
39
|
PREFECT_CLIENT_RETRY_EXTRA_CODES,
|
40
40
|
PREFECT_CLIENT_RETRY_JITTER_FACTOR,
|
41
|
+
PREFECT_CLOUD_API_URL,
|
42
|
+
PREFECT_SERVER_ALLOW_EPHEMERAL_MODE,
|
41
43
|
)
|
44
|
+
from prefect.utilities.collections import AutoEnum
|
42
45
|
from prefect.utilities.math import bounded_poisson_interval, clamped_poisson_interval
|
43
46
|
|
44
47
|
# Datastores for lifespan management, keys should be a tuple of thread and app
|
@@ -612,28 +615,31 @@ class PrefectHttpxSyncClient(httpx.Client):
|
|
612
615
|
request.headers["Prefect-Csrf-Client"] = str(self.csrf_client_id)
|
613
616
|
|
614
617
|
|
615
|
-
class
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
It is a subclass of both Starlette's `TestClient` and Prefect's
|
621
|
-
`PrefectHttpxSyncClient`, so it combines the synchronous testing
|
622
|
-
capabilities of `TestClient` with the Prefect-specific behaviors of
|
623
|
-
`PrefectHttpxSyncClient`.
|
624
|
-
"""
|
618
|
+
class ServerType(AutoEnum):
|
619
|
+
EPHEMERAL = AutoEnum.auto()
|
620
|
+
SERVER = AutoEnum.auto()
|
621
|
+
CLOUD = AutoEnum.auto()
|
622
|
+
UNCONFIGURED = AutoEnum.auto()
|
625
623
|
|
626
|
-
def __init__(
|
627
|
-
self,
|
628
|
-
*args,
|
629
|
-
# override TestClient default
|
630
|
-
raise_server_exceptions=False,
|
631
|
-
**kwargs,
|
632
|
-
):
|
633
|
-
super().__init__(
|
634
|
-
*args,
|
635
|
-
raise_server_exceptions=raise_server_exceptions,
|
636
|
-
**kwargs,
|
637
|
-
)
|
638
624
|
|
639
|
-
|
625
|
+
def determine_server_type() -> ServerType:
|
626
|
+
"""
|
627
|
+
Determine the server type based on the current settings.
|
628
|
+
|
629
|
+
Returns:
|
630
|
+
- `ServerType.EPHEMERAL` if the ephemeral server is enabled
|
631
|
+
- `ServerType.SERVER` if a API URL is configured and it is not a cloud URL
|
632
|
+
- `ServerType.CLOUD` if an API URL is configured and it is a cloud URL
|
633
|
+
- `ServerType.UNCONFIGURED` if no API URL is configured and ephemeral mode is
|
634
|
+
not enabled
|
635
|
+
"""
|
636
|
+
api_url = PREFECT_API_URL.value()
|
637
|
+
if api_url is None:
|
638
|
+
if PREFECT_SERVER_ALLOW_EPHEMERAL_MODE.value():
|
639
|
+
return ServerType.EPHEMERAL
|
640
|
+
else:
|
641
|
+
return ServerType.UNCONFIGURED
|
642
|
+
if api_url.startswith(PREFECT_CLOUD_API_URL.value()):
|
643
|
+
return ServerType.CLOUD
|
644
|
+
else:
|
645
|
+
return ServerType.SERVER
|
prefect/client/orchestration.py
CHANGED
@@ -132,9 +132,9 @@ from prefect.settings import (
|
|
132
132
|
PREFECT_API_URL,
|
133
133
|
PREFECT_CLIENT_CSRF_SUPPORT_ENABLED,
|
134
134
|
PREFECT_CLOUD_API_URL,
|
135
|
+
PREFECT_SERVER_ALLOW_EPHEMERAL_MODE,
|
135
136
|
PREFECT_UNIT_TEST_MODE,
|
136
137
|
)
|
137
|
-
from prefect.utilities.collections import AutoEnum
|
138
138
|
|
139
139
|
if TYPE_CHECKING:
|
140
140
|
from prefect.flows import Flow as FlowObject
|
@@ -144,7 +144,7 @@ from prefect.client.base import (
|
|
144
144
|
ASGIApp,
|
145
145
|
PrefectHttpxAsyncClient,
|
146
146
|
PrefectHttpxSyncClient,
|
147
|
-
|
147
|
+
ServerType,
|
148
148
|
app_lifespan_context,
|
149
149
|
)
|
150
150
|
|
@@ -152,12 +152,6 @@ P = ParamSpec("P")
|
|
152
152
|
R = TypeVar("R")
|
153
153
|
|
154
154
|
|
155
|
-
class ServerType(AutoEnum):
|
156
|
-
EPHEMERAL = AutoEnum.auto()
|
157
|
-
SERVER = AutoEnum.auto()
|
158
|
-
CLOUD = AutoEnum.auto()
|
159
|
-
|
160
|
-
|
161
155
|
@overload
|
162
156
|
def get_client(
|
163
157
|
httpx_settings: Optional[Dict[str, Any]] = None, sync_client: Literal[False] = False
|
@@ -194,8 +188,6 @@ def get_client(
|
|
194
188
|
"""
|
195
189
|
import prefect.context
|
196
190
|
|
197
|
-
settings_ctx = prefect.context.get_settings_context()
|
198
|
-
|
199
191
|
# try to load clients from a client context, if possible
|
200
192
|
# only load clients that match the provided config / loop
|
201
193
|
try:
|
@@ -203,40 +195,50 @@ def get_client(
|
|
203
195
|
except RuntimeError:
|
204
196
|
loop = None
|
205
197
|
|
206
|
-
if
|
207
|
-
if (
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
):
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
):
|
219
|
-
return client_ctx.async_client
|
198
|
+
if sync_client:
|
199
|
+
if client_ctx := prefect.context.SyncClientContext.get():
|
200
|
+
if client_ctx.client and client_ctx._httpx_settings == httpx_settings:
|
201
|
+
return client_ctx.client
|
202
|
+
else:
|
203
|
+
if client_ctx := prefect.context.AsyncClientContext.get():
|
204
|
+
if (
|
205
|
+
client_ctx.client
|
206
|
+
and client_ctx._httpx_settings == httpx_settings
|
207
|
+
and loop in (client_ctx.client._loop, None)
|
208
|
+
):
|
209
|
+
return client_ctx.client
|
220
210
|
|
221
211
|
api = PREFECT_API_URL.value()
|
212
|
+
server_type = None
|
222
213
|
|
223
|
-
if not api:
|
214
|
+
if not api and PREFECT_SERVER_ALLOW_EPHEMERAL_MODE:
|
224
215
|
# create an ephemeral API if none was provided
|
225
|
-
from prefect.server.api.server import
|
216
|
+
from prefect.server.api.server import SubprocessASGIServer
|
217
|
+
|
218
|
+
server = SubprocessASGIServer()
|
219
|
+
server.start()
|
220
|
+
assert server.server_process is not None, "Server process did not start"
|
226
221
|
|
227
|
-
api =
|
222
|
+
api = server.api_url
|
223
|
+
server_type = ServerType.EPHEMERAL
|
224
|
+
elif not api and not PREFECT_SERVER_ALLOW_EPHEMERAL_MODE:
|
225
|
+
raise ValueError(
|
226
|
+
"No Prefect API URL provided. Please set PREFECT_API_URL to the address of a running Prefect server."
|
227
|
+
)
|
228
228
|
|
229
229
|
if sync_client:
|
230
230
|
return SyncPrefectClient(
|
231
231
|
api,
|
232
232
|
api_key=PREFECT_API_KEY.value(),
|
233
233
|
httpx_settings=httpx_settings,
|
234
|
+
server_type=server_type,
|
234
235
|
)
|
235
236
|
else:
|
236
237
|
return PrefectClient(
|
237
238
|
api,
|
238
239
|
api_key=PREFECT_API_KEY.value(),
|
239
240
|
httpx_settings=httpx_settings,
|
241
|
+
server_type=server_type,
|
240
242
|
)
|
241
243
|
|
242
244
|
|
@@ -273,6 +275,7 @@ class PrefectClient:
|
|
273
275
|
api_key: Optional[str] = None,
|
274
276
|
api_version: Optional[str] = None,
|
275
277
|
httpx_settings: Optional[Dict[str, Any]] = None,
|
278
|
+
server_type: Optional[ServerType] = None,
|
276
279
|
) -> None:
|
277
280
|
httpx_settings = httpx_settings.copy() if httpx_settings else {}
|
278
281
|
httpx_settings.setdefault("headers", {})
|
@@ -335,11 +338,14 @@ class PrefectClient:
|
|
335
338
|
# client will use a standard HTTP/1.1 connection instead.
|
336
339
|
httpx_settings.setdefault("http2", PREFECT_API_ENABLE_HTTP2.value())
|
337
340
|
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
341
|
+
if server_type:
|
342
|
+
self.server_type = server_type
|
343
|
+
else:
|
344
|
+
self.server_type = (
|
345
|
+
ServerType.CLOUD
|
346
|
+
if api.startswith(PREFECT_CLOUD_API_URL.value())
|
347
|
+
else ServerType.SERVER
|
348
|
+
)
|
343
349
|
|
344
350
|
# Connect to an in-process application
|
345
351
|
elif isinstance(api, ASGIApp):
|
@@ -3012,16 +3018,34 @@ class PrefectClient:
|
|
3012
3018
|
return response.json()
|
3013
3019
|
|
3014
3020
|
async def increment_concurrency_slots(
|
3015
|
-
self, names: List[str], slots: int, mode: str
|
3021
|
+
self, names: List[str], slots: int, mode: str, create_if_missing: Optional[bool]
|
3016
3022
|
) -> httpx.Response:
|
3017
3023
|
return await self._client.post(
|
3018
3024
|
"/v2/concurrency_limits/increment",
|
3019
|
-
json={
|
3025
|
+
json={
|
3026
|
+
"names": names,
|
3027
|
+
"slots": slots,
|
3028
|
+
"mode": mode,
|
3029
|
+
"create_if_missing": create_if_missing,
|
3030
|
+
},
|
3020
3031
|
)
|
3021
3032
|
|
3022
3033
|
async def release_concurrency_slots(
|
3023
3034
|
self, names: List[str], slots: int, occupancy_seconds: float
|
3024
3035
|
) -> httpx.Response:
|
3036
|
+
"""
|
3037
|
+
Release concurrency slots for the specified limits.
|
3038
|
+
|
3039
|
+
Args:
|
3040
|
+
names (List[str]): A list of limit names for which to release slots.
|
3041
|
+
slots (int): The number of concurrency slots to release.
|
3042
|
+
occupancy_seconds (float): The duration in seconds that the slots
|
3043
|
+
were occupied.
|
3044
|
+
|
3045
|
+
Returns:
|
3046
|
+
httpx.Response: The HTTP response from the server.
|
3047
|
+
"""
|
3048
|
+
|
3025
3049
|
return await self._client.post(
|
3026
3050
|
"/v2/concurrency_limits/decrement",
|
3027
3051
|
json={
|
@@ -3370,6 +3394,7 @@ class SyncPrefectClient:
|
|
3370
3394
|
api_key: Optional[str] = None,
|
3371
3395
|
api_version: Optional[str] = None,
|
3372
3396
|
httpx_settings: Optional[Dict[str, Any]] = None,
|
3397
|
+
server_type: Optional[ServerType] = None,
|
3373
3398
|
) -> None:
|
3374
3399
|
httpx_settings = httpx_settings.copy() if httpx_settings else {}
|
3375
3400
|
httpx_settings.setdefault("headers", {})
|
@@ -3428,11 +3453,14 @@ class SyncPrefectClient:
|
|
3428
3453
|
# client will use a standard HTTP/1.1 connection instead.
|
3429
3454
|
httpx_settings.setdefault("http2", PREFECT_API_ENABLE_HTTP2.value())
|
3430
3455
|
|
3431
|
-
|
3432
|
-
|
3433
|
-
|
3434
|
-
|
3435
|
-
|
3456
|
+
if server_type:
|
3457
|
+
self.server_type = server_type
|
3458
|
+
else:
|
3459
|
+
self.server_type = (
|
3460
|
+
ServerType.CLOUD
|
3461
|
+
if api.startswith(PREFECT_CLOUD_API_URL.value())
|
3462
|
+
else ServerType.SERVER
|
3463
|
+
)
|
3436
3464
|
|
3437
3465
|
# Connect to an in-process application
|
3438
3466
|
elif isinstance(api, ASGIApp):
|
@@ -3464,14 +3492,9 @@ class SyncPrefectClient:
|
|
3464
3492
|
and PREFECT_CLIENT_CSRF_SUPPORT_ENABLED.value()
|
3465
3493
|
)
|
3466
3494
|
|
3467
|
-
|
3468
|
-
|
3469
|
-
|
3470
|
-
)
|
3471
|
-
else:
|
3472
|
-
self._client = PrefectHttpxSyncClient(
|
3473
|
-
**httpx_settings, enable_csrf_support=enable_csrf_support
|
3474
|
-
)
|
3495
|
+
self._client = PrefectHttpxSyncClient(
|
3496
|
+
**httpx_settings, enable_csrf_support=enable_csrf_support
|
3497
|
+
)
|
3475
3498
|
|
3476
3499
|
# See https://www.python-httpx.org/advanced/#custom-transports
|
3477
3500
|
#
|
@@ -4046,6 +4069,33 @@ class SyncPrefectClient:
|
|
4046
4069
|
raise
|
4047
4070
|
return DeploymentResponse.model_validate(response.json())
|
4048
4071
|
|
4072
|
+
def read_deployment_by_name(
|
4073
|
+
self,
|
4074
|
+
name: str,
|
4075
|
+
) -> DeploymentResponse:
|
4076
|
+
"""
|
4077
|
+
Query the Prefect API for a deployment by name.
|
4078
|
+
|
4079
|
+
Args:
|
4080
|
+
name: A deployed flow's name: <FLOW_NAME>/<DEPLOYMENT_NAME>
|
4081
|
+
|
4082
|
+
Raises:
|
4083
|
+
prefect.exceptions.ObjectNotFound: If request returns 404
|
4084
|
+
httpx.RequestError: If request fails
|
4085
|
+
|
4086
|
+
Returns:
|
4087
|
+
a Deployment model representation of the deployment
|
4088
|
+
"""
|
4089
|
+
try:
|
4090
|
+
response = self._client.get(f"/deployments/name/{name}")
|
4091
|
+
except httpx.HTTPStatusError as e:
|
4092
|
+
if e.response.status_code == status.HTTP_404_NOT_FOUND:
|
4093
|
+
raise prefect.exceptions.ObjectNotFound(http_exc=e) from e
|
4094
|
+
else:
|
4095
|
+
raise
|
4096
|
+
|
4097
|
+
return DeploymentResponse.model_validate(response.json())
|
4098
|
+
|
4049
4099
|
def create_artifact(
|
4050
4100
|
self,
|
4051
4101
|
artifact: ArtifactCreate,
|
@@ -4065,3 +4115,27 @@ class SyncPrefectClient:
|
|
4065
4115
|
)
|
4066
4116
|
|
4067
4117
|
return Artifact.model_validate(response.json())
|
4118
|
+
|
4119
|
+
def release_concurrency_slots(
|
4120
|
+
self, names: List[str], slots: int, occupancy_seconds: float
|
4121
|
+
) -> httpx.Response:
|
4122
|
+
"""
|
4123
|
+
Release concurrency slots for the specified limits.
|
4124
|
+
|
4125
|
+
Args:
|
4126
|
+
names (List[str]): A list of limit names for which to release slots.
|
4127
|
+
slots (int): The number of concurrency slots to release.
|
4128
|
+
occupancy_seconds (float): The duration in seconds that the slots
|
4129
|
+
were occupied.
|
4130
|
+
|
4131
|
+
Returns:
|
4132
|
+
httpx.Response: The HTTP response from the server.
|
4133
|
+
"""
|
4134
|
+
return self._client.post(
|
4135
|
+
"/v2/concurrency_limits/decrement",
|
4136
|
+
json={
|
4137
|
+
"names": names,
|
4138
|
+
"slots": slots,
|
4139
|
+
"occupancy_seconds": occupancy_seconds,
|
4140
|
+
},
|
4141
|
+
)
|
prefect/client/utilities.py
CHANGED
@@ -42,14 +42,14 @@ def get_or_create_client(
|
|
42
42
|
if client is not None:
|
43
43
|
return client, True
|
44
44
|
from prefect._internal.concurrency.event_loop import get_running_loop
|
45
|
-
from prefect.context import
|
45
|
+
from prefect.context import AsyncClientContext, FlowRunContext, TaskRunContext
|
46
46
|
|
47
|
-
|
47
|
+
async_client_context = AsyncClientContext.get()
|
48
48
|
flow_run_context = FlowRunContext.get()
|
49
49
|
task_run_context = TaskRunContext.get()
|
50
50
|
|
51
|
-
if
|
52
|
-
return
|
51
|
+
if async_client_context and async_client_context.client._loop == get_running_loop():
|
52
|
+
return async_client_context.client, True
|
53
53
|
elif (
|
54
54
|
flow_run_context
|
55
55
|
and getattr(flow_run_context.client, "_loop", None) == get_running_loop()
|