prefect-client 2.16.4__py3-none-any.whl → 2.16.6__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.
@@ -15,6 +15,7 @@ from typing import (
15
15
  )
16
16
  from uuid import UUID, uuid4
17
17
 
18
+ import certifi
18
19
  import httpcore
19
20
  import httpx
20
21
  import pendulum
@@ -134,8 +135,10 @@ from prefect.settings import (
134
135
  PREFECT_API_ENABLE_HTTP2,
135
136
  PREFECT_API_KEY,
136
137
  PREFECT_API_REQUEST_TIMEOUT,
138
+ PREFECT_API_SSL_CERT_FILE,
137
139
  PREFECT_API_TLS_INSECURE_SKIP_VERIFY,
138
140
  PREFECT_API_URL,
141
+ PREFECT_CLIENT_CSRF_SUPPORT_ENABLED,
139
142
  PREFECT_CLOUD_API_URL,
140
143
  PREFECT_UNIT_TEST_MODE,
141
144
  )
@@ -220,6 +223,11 @@ class PrefectClient:
220
223
 
221
224
  if PREFECT_API_TLS_INSECURE_SKIP_VERIFY:
222
225
  httpx_settings.setdefault("verify", False)
226
+ else:
227
+ cert_file = PREFECT_API_SSL_CERT_FILE.value()
228
+ if not cert_file:
229
+ cert_file = certifi.where()
230
+ httpx_settings.setdefault("verify", cert_file)
223
231
 
224
232
  if api_version is None:
225
233
  api_version = SERVER_API_VERSION
@@ -316,7 +324,15 @@ class PrefectClient:
316
324
 
317
325
  if not PREFECT_UNIT_TEST_MODE:
318
326
  httpx_settings.setdefault("follow_redirects", True)
319
- self._client = PrefectHttpxClient(**httpx_settings)
327
+
328
+ enable_csrf_support = (
329
+ self.server_type != ServerType.CLOUD
330
+ and PREFECT_CLIENT_CSRF_SUPPORT_ENABLED.value()
331
+ )
332
+
333
+ self._client = PrefectHttpxClient(
334
+ **httpx_settings, enable_csrf_support=enable_csrf_support
335
+ )
320
336
  self._loop = None
321
337
 
322
338
  # See https://www.python-httpx.org/advanced/#custom-transports
@@ -1632,3 +1632,16 @@ class GlobalConcurrencyLimit(ObjectBaseModel):
1632
1632
  " is used as a rate limit."
1633
1633
  ),
1634
1634
  )
1635
+
1636
+
1637
+ class CsrfToken(ObjectBaseModel):
1638
+ token: str = Field(
1639
+ default=...,
1640
+ description="The CSRF token",
1641
+ )
1642
+ client: str = Field(
1643
+ default=..., description="The client id associated with the CSRF token"
1644
+ )
1645
+ expiration: DateTimeTZ = Field(
1646
+ default=..., description="The expiration time of the CSRF token"
1647
+ )
@@ -74,9 +74,9 @@ class Subscription(Generic[S]):
74
74
  auth: Dict[str, Any] = orjson.loads(await websocket.recv())
75
75
  assert auth["type"] == "auth_success", auth.get("message")
76
76
 
77
- message = {"type": "subscribe", "keys": self.keys} | {
78
- **(dict(client_id=self.client_id) if self.client_id else {})
79
- }
77
+ message = {"type": "subscribe", "keys": self.keys}
78
+ if self.client_id:
79
+ message.update({"client_id": self.client_id})
80
80
 
81
81
  await websocket.send(orjson.dumps(message).decode())
82
82
  except (
prefect/engine.py CHANGED
@@ -2002,10 +2002,14 @@ async def orchestrate_task_run(
2002
2002
  )
2003
2003
 
2004
2004
  # Emit an event to capture that the task run was in the `PENDING` state.
2005
- last_event = _emit_task_run_state_change_event(
2005
+ last_event = emit_task_run_state_change_event(
2006
2006
  task_run=task_run, initial_state=None, validated_state=task_run.state
2007
2007
  )
2008
- last_state = task_run.state
2008
+ last_state = (
2009
+ Pending()
2010
+ if flow_run_context and flow_run_context.autonomous_task_run
2011
+ else task_run.state
2012
+ )
2009
2013
 
2010
2014
  # Completed states with persisted results should have result data. If it's missing,
2011
2015
  # this could be a manual state transition, so we should use the Unknown result type
@@ -2094,7 +2098,7 @@ async def orchestrate_task_run(
2094
2098
  break
2095
2099
 
2096
2100
  # Emit an event to capture the result of proposing a `RUNNING` state.
2097
- last_event = _emit_task_run_state_change_event(
2101
+ last_event = emit_task_run_state_change_event(
2098
2102
  task_run=task_run,
2099
2103
  initial_state=last_state,
2100
2104
  validated_state=state,
@@ -2187,7 +2191,7 @@ async def orchestrate_task_run(
2187
2191
  await _check_task_failure_retriable(task, task_run, terminal_state)
2188
2192
  )
2189
2193
  state = await propose_state(client, terminal_state, task_run_id=task_run.id)
2190
- last_event = _emit_task_run_state_change_event(
2194
+ last_event = emit_task_run_state_change_event(
2191
2195
  task_run=task_run,
2192
2196
  initial_state=last_state,
2193
2197
  validated_state=state,
@@ -2220,7 +2224,7 @@ async def orchestrate_task_run(
2220
2224
  )
2221
2225
  # Attempt to enter a running state again
2222
2226
  state = await propose_state(client, Running(), task_run_id=task_run.id)
2223
- last_event = _emit_task_run_state_change_event(
2227
+ last_event = emit_task_run_state_change_event(
2224
2228
  task_run=task_run,
2225
2229
  initial_state=last_state,
2226
2230
  validated_state=state,
@@ -2896,7 +2900,7 @@ async def check_api_reachable(client: PrefectClient, fail_message: str):
2896
2900
  API_HEALTHCHECKS[api_url] = get_deadline(60 * 10)
2897
2901
 
2898
2902
 
2899
- def _emit_task_run_state_change_event(
2903
+ def emit_task_run_state_change_event(
2900
2904
  task_run: TaskRun,
2901
2905
  initial_state: Optional[State],
2902
2906
  validated_state: State,
prefect/events/related.py CHANGED
@@ -74,7 +74,7 @@ async def related_resources_from_run_context(
74
74
  if flow_run_id is None:
75
75
  return []
76
76
 
77
- related_objects: list[ResourceCacheEntry] = []
77
+ related_objects: List[ResourceCacheEntry] = []
78
78
 
79
79
  async with get_client() as client:
80
80
 
prefect/events/schemas.py CHANGED
@@ -372,9 +372,53 @@ class MetricTrigger(ResourceTrigger):
372
372
  )
373
373
 
374
374
 
375
- TriggerTypes: TypeAlias = Union[EventTrigger, MetricTrigger]
375
+ class CompositeTrigger(Trigger, abc.ABC):
376
+ """
377
+ Requires some number of triggers to have fired within the given time period.
378
+ """
379
+
380
+ type: Literal["compound", "sequence"]
381
+ triggers: List["TriggerTypes"]
382
+ within: Optional[timedelta]
383
+
384
+
385
+ class CompoundTrigger(CompositeTrigger):
386
+ """A composite trigger that requires some number of triggers to have
387
+ fired within the given time period"""
388
+
389
+ type: Literal["compound"] = "compound"
390
+ require: Union[int, Literal["any", "all"]]
391
+
392
+ @root_validator
393
+ def validate_require(cls, values: Dict[str, Any]) -> Dict[str, Any]:
394
+ require = values.get("require")
395
+
396
+ if isinstance(require, int):
397
+ if require < 1:
398
+ raise ValueError("required must be at least 1")
399
+ if require > len(values["triggers"]):
400
+ raise ValueError(
401
+ "required must be less than or equal to the number of triggers"
402
+ )
403
+
404
+ return values
405
+
406
+
407
+ class SequenceTrigger(CompositeTrigger):
408
+ """A composite trigger that requires some number of triggers to have fired
409
+ within the given time period in a specific order"""
410
+
411
+ type: Literal["sequence"] = "sequence"
412
+
413
+
414
+ TriggerTypes: TypeAlias = Union[
415
+ EventTrigger, MetricTrigger, CompoundTrigger, SequenceTrigger
416
+ ]
376
417
  """The union of all concrete trigger types that a user may actually create"""
377
418
 
419
+ CompoundTrigger.update_forward_refs()
420
+ SequenceTrigger.update_forward_refs()
421
+
378
422
 
379
423
  class Automation(PrefectBaseModel):
380
424
  """Defines an action a user wants to take when a certain number of events
prefect/flows.py CHANGED
@@ -1345,8 +1345,12 @@ def flow(
1345
1345
  result_serializer: Optional[ResultSerializer] = None,
1346
1346
  cache_result_in_memory: bool = True,
1347
1347
  log_prints: Optional[bool] = None,
1348
- on_completion: Optional[List[Callable[[FlowSchema, FlowRun, State], None]]] = None,
1349
- on_failure: Optional[List[Callable[[FlowSchema, FlowRun, State], None]]] = None,
1348
+ on_completion: Optional[
1349
+ List[Callable[[FlowSchema, FlowRun, State], Union[Awaitable[None], None]]]
1350
+ ] = None,
1351
+ on_failure: Optional[
1352
+ List[Callable[[FlowSchema, FlowRun, State], Union[Awaitable[None], None]]]
1353
+ ] = None,
1350
1354
  on_cancellation: Optional[
1351
1355
  List[Callable[[FlowSchema, FlowRun, State], None]]
1352
1356
  ] = None,
@@ -1373,8 +1377,12 @@ def flow(
1373
1377
  result_serializer: Optional[ResultSerializer] = None,
1374
1378
  cache_result_in_memory: bool = True,
1375
1379
  log_prints: Optional[bool] = None,
1376
- on_completion: Optional[List[Callable[[FlowSchema, FlowRun, State], None]]] = None,
1377
- on_failure: Optional[List[Callable[[FlowSchema, FlowRun, State], None]]] = None,
1380
+ on_completion: Optional[
1381
+ List[Callable[[FlowSchema, FlowRun, State], Union[Awaitable[None], None]]]
1382
+ ] = None,
1383
+ on_failure: Optional[
1384
+ List[Callable[[FlowSchema, FlowRun, State], Union[Awaitable[None], None]]]
1385
+ ] = None,
1378
1386
  on_cancellation: Optional[
1379
1387
  List[Callable[[FlowSchema, FlowRun, State], None]]
1380
1388
  ] = None,
@@ -912,8 +912,7 @@
912
912
  "metadata": {
913
913
  "name": "{{ name }}",
914
914
  "annotations": {
915
- "run.googleapis.com/launch-stage": "BETA",
916
- "run.googleapis.com/vpc-access-connector": "{{ vpc_connector_name }}"
915
+ "run.googleapis.com/launch-stage": "BETA"
917
916
  }
918
917
  },
919
918
  "spec": {
@@ -941,6 +940,11 @@
941
940
  "serviceAccountName": "{{ service_account_name }}"
942
941
  }
943
942
  }
943
+ },
944
+ "metadata": {
945
+ "annotations": {
946
+ "run.googleapis.com/vpc-access-connector": "{{ vpc_connector_name }}"
947
+ }
944
948
  }
945
949
  }
946
950
  }
@@ -1093,8 +1097,10 @@
1093
1097
  "launchStage": "{{ launch_stage }}",
1094
1098
  "template": {
1095
1099
  "template": {
1100
+ "serviceAccount": "{{ service_account_name }}",
1096
1101
  "maxRetries": "{{ max_retries }}",
1097
1102
  "timeout": "{{ timeout }}",
1103
+ "vpcAccess": "{{ vpc_connector_name }}",
1098
1104
  "containers": [
1099
1105
  {
1100
1106
  "env": [],
@@ -1229,6 +1235,12 @@
1229
1235
  "title": "VPC Connector Name",
1230
1236
  "description": "The name of the VPC connector to use for the Cloud Run job.",
1231
1237
  "type": "string"
1238
+ },
1239
+ "service_account_name": {
1240
+ "title": "Service Account Name",
1241
+ "description": "The name of the service account to use for the task execution of Cloud Run Job. By default Cloud Run jobs run as the default Compute Engine Service Account.",
1242
+ "example": "service-account@example.iam.gserviceaccount.com",
1243
+ "type": "string"
1232
1244
  }
1233
1245
  },
1234
1246
  "definitions": {
prefect/settings.py CHANGED
@@ -102,7 +102,10 @@ T = TypeVar("T")
102
102
 
103
103
  DEFAULT_PROFILES_PATH = Path(__file__).parent.joinpath("profiles.toml")
104
104
 
105
- REMOVED_EXPERIMENTAL_FLAGS = {"PREFECT_EXPERIMENTAL_ENABLE_ENHANCED_SCHEDULING_UI"}
105
+ REMOVED_EXPERIMENTAL_FLAGS = {
106
+ "PREFECT_EXPERIMENTAL_ENABLE_ENHANCED_SCHEDULING_UI",
107
+ "PREFECT_EXPERIMENTAL_ENABLE_ENHANCED_DEPLOYMENT_PARAMETERS",
108
+ }
106
109
 
107
110
 
108
111
  class Setting(Generic[T]):
@@ -592,6 +595,16 @@ PREFECT_API_TLS_INSECURE_SKIP_VERIFY = Setting(
592
595
  This is recommended only during development, e.g. when using self-signed certificates.
593
596
  """
594
597
 
598
+ PREFECT_API_SSL_CERT_FILE = Setting(
599
+ str,
600
+ default=os.environ.get("SSL_CERT_FILE"),
601
+ )
602
+ """
603
+ This configuration settings option specifies the path to an SSL certificate file.
604
+ When set, it allows the application to use the specified certificate for secure communication.
605
+ If left unset, the setting will default to the value provided by the `SSL_CERT_FILE` environment variable.
606
+ """
607
+
595
608
  PREFECT_API_URL = Setting(
596
609
  str,
597
610
  default=None,
@@ -657,6 +670,21 @@ A comma-separated list of extra HTTP status codes to retry on. Defaults to an em
657
670
  may result in unexpected behavior.
658
671
  """
659
672
 
673
+ PREFECT_CLIENT_CSRF_SUPPORT_ENABLED = Setting(bool, default=True)
674
+ """
675
+ Determines if CSRF token handling is active in the Prefect client for API
676
+ requests.
677
+
678
+ When enabled (`True`), the client automatically manages CSRF tokens by
679
+ retrieving, storing, and including them in applicable state-changing requests
680
+ (POST, PUT, PATCH, DELETE) to the API.
681
+
682
+ Disabling this setting (`False`) means the client will not handle CSRF tokens,
683
+ which might be suitable for environments where CSRF protection is disabled.
684
+
685
+ Defaults to `True`, ensuring CSRF protection is enabled by default.
686
+ """
687
+
660
688
  PREFECT_CLOUD_API_URL = Setting(
661
689
  str,
662
690
  default="https://api.prefect.cloud/api",
@@ -1207,6 +1235,33 @@ Note this setting only applies when calling `prefect server start`; if hosting t
1207
1235
  API with another tool you will need to configure this there instead.
1208
1236
  """
1209
1237
 
1238
+ PREFECT_SERVER_CSRF_PROTECTION_ENABLED = Setting(bool, default=True)
1239
+ """
1240
+ Controls the activation of CSRF protection for the Prefect server API.
1241
+
1242
+ When enabled (`True`), the server enforces CSRF validation checks on incoming
1243
+ state-changing requests (POST, PUT, PATCH, DELETE), requiring a valid CSRF
1244
+ token to be included in the request headers or body. This adds a layer of
1245
+ security by preventing unauthorized or malicious sites from making requests on
1246
+ behalf of authenticated users.
1247
+
1248
+ It is recommended to enable this setting in production environments where the
1249
+ API is exposed to web clients to safeguard against CSRF attacks.
1250
+
1251
+ Note: Enabling this setting requires corresponding support in the client for
1252
+ CSRF token management. See PREFECT_CLIENT_CSRF_SUPPORT_ENABLED for more.
1253
+ """
1254
+
1255
+ PREFECT_SERVER_CSRF_TOKEN_EXPIRATION = Setting(timedelta, default=timedelta(hours=1))
1256
+ """
1257
+ Specifies the duration for which a CSRF token remains valid after being issued
1258
+ by the server.
1259
+
1260
+ The default expiration time is set to 1 hour, which offers a reasonable
1261
+ compromise. Adjust this setting based on your specific security requirements
1262
+ and usage patterns.
1263
+ """
1264
+
1210
1265
  PREFECT_UI_ENABLED = Setting(
1211
1266
  bool,
1212
1267
  default=True,
@@ -1292,12 +1347,12 @@ PREFECT_API_MAX_FLOW_RUN_GRAPH_ARTIFACTS = Setting(int, default=10000)
1292
1347
  The maximum number of artifacts to show on a flow run graph on the v2 API
1293
1348
  """
1294
1349
 
1295
- PREFECT_EXPERIMENTAL_ENABLE_ARTIFACTS_ON_FLOW_RUN_GRAPH = Setting(bool, default=False)
1350
+ PREFECT_EXPERIMENTAL_ENABLE_ARTIFACTS_ON_FLOW_RUN_GRAPH = Setting(bool, default=True)
1296
1351
  """
1297
1352
  Whether or not to enable artifacts on the flow run graph.
1298
1353
  """
1299
1354
 
1300
- PREFECT_EXPERIMENTAL_ENABLE_STATES_ON_FLOW_RUN_GRAPH = Setting(bool, default=False)
1355
+ PREFECT_EXPERIMENTAL_ENABLE_STATES_ON_FLOW_RUN_GRAPH = Setting(bool, default=True)
1301
1356
  """
1302
1357
  Whether or not to enable flow run states on the flow run graph.
1303
1358
  """
@@ -1342,11 +1397,6 @@ PREFECT_EXPERIMENTAL_ENABLE_ENHANCED_CANCELLATION = Setting(bool, default=True)
1342
1397
  Whether or not to enable experimental enhanced flow run cancellation.
1343
1398
  """
1344
1399
 
1345
- PREFECT_EXPERIMENTAL_ENABLE_ENHANCED_DEPLOYMENT_PARAMETERS = Setting(bool, default=True)
1346
- """
1347
- Whether or not to enable enhanced deployment parameters.
1348
- """
1349
-
1350
1400
  PREFECT_EXPERIMENTAL_WARN_ENHANCED_CANCELLATION = Setting(bool, default=False)
1351
1401
  """
1352
1402
  Whether or not to warn when experimental enhanced flow run cancellation is used.
@@ -1525,6 +1575,10 @@ PREFECT_EXPERIMENTAL_ENABLE_WORK_QUEUE_STATUS = Setting(bool, default=True)
1525
1575
  Whether or not to enable experimental work queue status in-place of work queue health.
1526
1576
  """
1527
1577
 
1578
+ PREFECT_EXPERIMENTAL_ENABLE_PYDANTIC_V2_INTERNALS = Setting(bool, default=False)
1579
+ """
1580
+ Whether or not to enable internal experimental Pydantic v2 behavior.
1581
+ """
1528
1582
 
1529
1583
  # Defaults -----------------------------------------------------------------------------
1530
1584
 
prefect/task_server.py CHANGED
@@ -6,7 +6,7 @@ import socket
6
6
  import sys
7
7
  from contextlib import AsyncExitStack
8
8
  from functools import partial
9
- from typing import Optional, Type
9
+ from typing import List, Optional, Type
10
10
 
11
11
  import anyio
12
12
  from websockets.exceptions import InvalidStatusCode
@@ -15,7 +15,7 @@ from prefect import Task, get_client
15
15
  from prefect._internal.concurrency.api import create_call, from_sync
16
16
  from prefect.client.schemas.objects import TaskRun
17
17
  from prefect.client.subscriptions import Subscription
18
- from prefect.engine import propose_state
18
+ from prefect.engine import emit_task_run_state_change_event, propose_state
19
19
  from prefect.logging.loggers import get_logger
20
20
  from prefect.results import ResultFactory
21
21
  from prefect.settings import (
@@ -72,7 +72,7 @@ class TaskServer:
72
72
  *tasks: Task,
73
73
  task_runner: Optional[Type[BaseTaskRunner]] = None,
74
74
  ):
75
- self.tasks: list[Task] = tasks
75
+ self.tasks: List[Task] = tasks
76
76
 
77
77
  self.task_runner: BaseTaskRunner = task_runner or ConcurrentTaskRunner()
78
78
  self.started: bool = False
@@ -205,6 +205,12 @@ class TaskServer:
205
205
  " Task run may have already begun execution."
206
206
  )
207
207
 
208
+ emit_task_run_state_change_event(
209
+ task_run=task_run,
210
+ initial_state=task_run.state,
211
+ validated_state=state,
212
+ )
213
+
208
214
  self._runs_task_group.start_soon(
209
215
  partial(
210
216
  submit_autonomous_task_run_to_engine,
prefect/tasks.py CHANGED
@@ -283,13 +283,14 @@ class Task(Generic[P, R]):
283
283
  if not hasattr(self.fn, "__qualname__"):
284
284
  self.task_key = to_qualified_name(type(self.fn))
285
285
  else:
286
- if self.fn.__module__ == "__main__":
287
- task_definition_path = inspect.getsourcefile(self.fn)
288
- self.task_key = hash_objects(
289
- self.name, os.path.abspath(task_definition_path)
286
+ try:
287
+ task_origin_hash = hash_objects(
288
+ self.name, os.path.abspath(inspect.getsourcefile(self.fn))
290
289
  )
291
- else:
292
- self.task_key = to_qualified_name(self.fn)
290
+ except TypeError:
291
+ task_origin_hash = "unknown-source-file"
292
+
293
+ self.task_key = f"{self.fn.__qualname__}-{task_origin_hash}"
293
294
 
294
295
  self.cache_key_fn = cache_key_fn
295
296
  self.cache_expiration = cache_expiration
@@ -390,8 +391,12 @@ class Task(Generic[P, R]):
390
391
  timeout_seconds: Union[int, float] = None,
391
392
  log_prints: Optional[bool] = NotSet,
392
393
  refresh_cache: Optional[bool] = NotSet,
393
- on_completion: Optional[List[Callable[["Task", TaskRun, State], None]]] = None,
394
- on_failure: Optional[List[Callable[["Task", TaskRun, State], None]]] = None,
394
+ on_completion: Optional[
395
+ List[Callable[["Task", TaskRun, State], Union[Awaitable[None], None]]]
396
+ ] = None,
397
+ on_failure: Optional[
398
+ List[Callable[["Task", TaskRun, State], Union[Awaitable[None], None]]]
399
+ ] = None,
395
400
  retry_condition_fn: Optional[Callable[["Task", TaskRun, State], bool]] = None,
396
401
  viz_return_value: Optional[Any] = None,
397
402
  ):
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Utilities for working with Python callables.
3
3
  """
4
+
4
5
  import inspect
5
6
  from functools import partial
6
7
  from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
@@ -8,13 +9,13 @@ from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
8
9
  import cloudpickle
9
10
 
10
11
  from prefect._internal.pydantic import HAS_PYDANTIC_V2
12
+ from prefect._internal.pydantic.v1_schema import has_v1_type_as_param
11
13
 
12
14
  if HAS_PYDANTIC_V2:
13
15
  import pydantic.v1 as pydantic
14
16
 
15
17
  from prefect._internal.pydantic.v2_schema import (
16
18
  create_v2_schema,
17
- has_v2_type_as_param,
18
19
  process_v2_params,
19
20
  )
20
21
  else:
@@ -312,8 +313,9 @@ def parameter_schema(fn: Callable) -> ParameterSchema:
312
313
  ParameterSchema: the argument schema
313
314
  """
314
315
  try:
315
- signature = inspect.signature(fn, eval_str=True)
316
+ signature = inspect.signature(fn, eval_str=True) # novm
316
317
  except (NameError, TypeError):
318
+ # `eval_str` is not available in Python < 3.10
317
319
  signature = inspect.signature(fn)
318
320
 
319
321
  model_fields = {}
@@ -323,7 +325,7 @@ def parameter_schema(fn: Callable) -> ParameterSchema:
323
325
  class ModelConfig:
324
326
  arbitrary_types_allowed = True
325
327
 
326
- if HAS_PYDANTIC_V2 and has_v2_type_as_param(signature):
328
+ if HAS_PYDANTIC_V2 and not has_v1_type_as_param(signature):
327
329
  create_schema = create_v2_schema
328
330
  process_params = process_v2_params
329
331
  else:
@@ -149,9 +149,13 @@ def null_handler(obj: Dict, ctx: HydrationContext):
149
149
  @handler("json")
150
150
  def json_handler(obj: Dict, ctx: HydrationContext):
151
151
  if "value" in obj:
152
+ if isinstance(obj["value"], dict):
153
+ dehydrated_json = _hydrate(obj["value"], ctx)
154
+ else:
155
+ dehydrated_json = obj["value"]
152
156
  try:
153
- return json.loads(obj["value"])
154
- except json.decoder.JSONDecodeError as e:
157
+ return json.loads(dehydrated_json)
158
+ except (json.decoder.JSONDecodeError, TypeError) as e:
155
159
  return InvalidJSON(detail=str(e))
156
160
  else:
157
161
  # If `value` is not in the object, we need special handling to help
@@ -166,11 +170,15 @@ def json_handler(obj: Dict, ctx: HydrationContext):
166
170
  @handler("workspace_variable")
167
171
  def workspace_variable_handler(obj: Dict, ctx: HydrationContext):
168
172
  if "variable_name" in obj:
169
- variable = obj["variable_name"]
170
- if variable in ctx.workspace_variables:
171
- return ctx.workspace_variables[variable]
173
+ if isinstance(obj["variable_name"], dict):
174
+ dehydrated_variable = _hydrate(obj["variable_name"], ctx)
175
+ else:
176
+ dehydrated_variable = obj["variable_name"]
177
+
178
+ if dehydrated_variable in ctx.workspace_variables:
179
+ return ctx.workspace_variables[dehydrated_variable]
172
180
  else:
173
- return WorkspaceVariableNotFound(detail=variable)
181
+ return WorkspaceVariableNotFound(detail=dehydrated_variable)
174
182
  else:
175
183
  # Special handling if `variable_name` is not in the object.
176
184
  # If an object looks like {"__prefect_kind": "workspace_variable"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prefect-client
3
- Version: 2.16.4
3
+ Version: 2.16.6
4
4
  Summary: Workflow orchestration and management.
5
5
  Home-page: https://www.prefect.io
6
6
  Author: Prefect Technologies, Inc.