prefect-client 3.1.13__py3-none-any.whl → 3.1.15__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/artifacts.py CHANGED
@@ -2,22 +2,27 @@
2
2
  Interface for creating and reading artifacts.
3
3
  """
4
4
 
5
- import asyncio
6
- import json # noqa: I001
5
+ from __future__ import annotations
6
+
7
+ import json
7
8
  import math
8
9
  import warnings
9
- from typing import TYPE_CHECKING, Any, Optional, Union
10
+ from contextlib import nullcontext
11
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast
10
12
  from uuid import UUID
11
13
 
12
14
  from typing_extensions import Self
13
15
 
16
+ from prefect._internal.compatibility.async_dispatch import async_dispatch
17
+ from prefect.client.orchestration import PrefectClient, get_client
14
18
  from prefect.client.schemas.actions import ArtifactCreate as ArtifactRequest
15
19
  from prefect.client.schemas.actions import ArtifactUpdate
16
20
  from prefect.client.schemas.filters import ArtifactFilter, ArtifactFilterKey
21
+ from prefect.client.schemas.objects import Artifact as ArtifactResponse
17
22
  from prefect.client.schemas.sorting import ArtifactSort
18
- from prefect.client.utilities import get_or_create_client
23
+ from prefect.context import MissingContextError, get_run_context
19
24
  from prefect.logging.loggers import get_logger
20
- from prefect.utilities.asyncutils import sync_compatible
25
+ from prefect.utilities.asyncutils import asyncnullcontext
21
26
  from prefect.utilities.context import get_task_and_flow_run_ids
22
27
 
23
28
  if TYPE_CHECKING:
@@ -25,10 +30,6 @@ if TYPE_CHECKING:
25
30
 
26
31
  logger: "logging.Logger" = get_logger("artifacts")
27
32
 
28
- if TYPE_CHECKING:
29
- from prefect.client.orchestration import PrefectClient
30
- from prefect.client.schemas.objects import Artifact as ArtifactResponse
31
-
32
33
 
33
34
  class Artifact(ArtifactRequest):
34
35
  """
@@ -43,10 +44,47 @@ class Artifact(ArtifactRequest):
43
44
  data: A JSON payload that allows for a result to be retrieved.
44
45
  """
45
46
 
46
- @sync_compatible
47
- async def create(
47
+ async def acreate(
48
+ self,
49
+ client: "PrefectClient | None" = None,
50
+ ) -> "ArtifactResponse":
51
+ """
52
+ An async method to create an artifact.
53
+
54
+ Arguments:
55
+ client: The PrefectClient
56
+
57
+ Returns:
58
+ - The created artifact.
59
+ """
60
+
61
+ local_client_context = asyncnullcontext(client) if client else get_client()
62
+ async with local_client_context as client:
63
+ task_run_id, flow_run_id = get_task_and_flow_run_ids()
64
+
65
+ try:
66
+ get_run_context()
67
+ except MissingContextError:
68
+ warnings.warn(
69
+ "Artifact creation outside of a flow or task run is deprecated and will be removed in a later version.",
70
+ FutureWarning,
71
+ )
72
+
73
+ return await client.create_artifact(
74
+ artifact=ArtifactRequest(
75
+ type=self.type,
76
+ key=self.key,
77
+ description=self.description,
78
+ task_run_id=self.task_run_id or task_run_id,
79
+ flow_run_id=self.flow_run_id or flow_run_id,
80
+ data=await self.aformat(),
81
+ )
82
+ )
83
+
84
+ @async_dispatch(acreate)
85
+ def create(
48
86
  self: Self,
49
- client: Optional["PrefectClient"] = None,
87
+ client: "PrefectClient | None" = None,
50
88
  ) -> "ArtifactResponse":
51
89
  """
52
90
  A method to create an artifact.
@@ -57,9 +95,9 @@ class Artifact(ArtifactRequest):
57
95
  Returns:
58
96
  - The created artifact.
59
97
  """
60
- from prefect.context import MissingContextError, get_run_context
61
98
 
62
- client, _ = get_or_create_client(client)
99
+ # Create sync client since this is a sync method.
100
+ sync_client = get_client(sync_client=True)
63
101
  task_run_id, flow_run_id = get_task_and_flow_run_ids()
64
102
 
65
103
  try:
@@ -70,35 +108,67 @@ class Artifact(ArtifactRequest):
70
108
  FutureWarning,
71
109
  )
72
110
 
73
- return await client.create_artifact(
111
+ return sync_client.create_artifact(
74
112
  artifact=ArtifactRequest(
75
113
  type=self.type,
76
114
  key=self.key,
77
115
  description=self.description,
78
116
  task_run_id=self.task_run_id or task_run_id,
79
117
  flow_run_id=self.flow_run_id or flow_run_id,
80
- data=await self.format(),
118
+ data=cast(str, self.format(_sync=True)), # pyright: ignore[reportCallIssue] _sync is valid because .format is wrapped in async_dispatch
81
119
  )
82
120
  )
83
121
 
84
122
  @classmethod
85
- @sync_compatible
86
- async def get(
87
- cls, key: Optional[str] = None, client: Optional["PrefectClient"] = None
88
- ) -> Optional["ArtifactResponse"]:
123
+ async def aget(
124
+ cls,
125
+ key: str | None = None,
126
+ client: "PrefectClient | None" = None,
127
+ ) -> "ArtifactResponse | None":
128
+ """
129
+ A async method to get an artifact.
130
+
131
+ Arguments:
132
+ key: The key of the artifact to get.
133
+ client: A client to use when calling the Prefect API.
134
+
135
+ Returns:
136
+ The artifact (if found).
137
+ """
138
+
139
+ local_client_context = asyncnullcontext(client) if client else get_client()
140
+ async with local_client_context as client:
141
+ filter_key_value = None if key is None else [key]
142
+ artifacts = await client.read_artifacts(
143
+ limit=1,
144
+ sort=ArtifactSort.UPDATED_DESC,
145
+ artifact_filter=ArtifactFilter(
146
+ key=ArtifactFilterKey(any_=filter_key_value)
147
+ ),
148
+ )
149
+ return None if not artifacts else artifacts[0]
150
+
151
+ @classmethod
152
+ @async_dispatch(aget)
153
+ def get(
154
+ cls, key: str | None = None, client: "PrefectClient | None" = None
155
+ ) -> "ArtifactResponse | None":
89
156
  """
90
157
  A method to get an artifact.
91
158
 
92
159
  Arguments:
93
- key (str, optional): The key of the artifact to get.
94
- client (PrefectClient, optional): The PrefectClient
160
+ key: The key of the artifact to get.
161
+ client: A client to use when calling the Prefect API.
95
162
 
96
163
  Returns:
97
- (ArtifactResponse, optional): The artifact (if found).
164
+ The artifact (if found).
98
165
  """
99
- client, _ = get_or_create_client(client)
166
+
167
+ # Create sync client since this is a sync method.
168
+ sync_client = get_client(sync_client=True)
169
+
100
170
  filter_key_value = None if key is None else [key]
101
- artifacts = await client.read_artifacts(
171
+ artifacts = sync_client.read_artifacts(
102
172
  limit=1,
103
173
  sort=ArtifactSort.UPDATED_DESC,
104
174
  artifact_filter=ArtifactFilter(
@@ -108,41 +178,75 @@ class Artifact(ArtifactRequest):
108
178
  return None if not artifacts else artifacts[0]
109
179
 
110
180
  @classmethod
111
- @sync_compatible
112
- async def get_or_create(
181
+ async def aget_or_create(
113
182
  cls,
114
- key: Optional[str] = None,
115
- description: Optional[str] = None,
116
- data: Optional[Union[dict[str, Any], Any]] = None,
117
- client: Optional["PrefectClient"] = None,
183
+ key: str | None = None,
184
+ description: str | None = None,
185
+ data: dict[str, Any] | Any | None = None,
186
+ client: "PrefectClient | None" = None,
187
+ **kwargs: Any,
188
+ ) -> tuple["ArtifactResponse", bool]:
189
+ """
190
+ A async method to get or create an artifact.
191
+
192
+ Arguments:
193
+ key: The key of the artifact to get or create.
194
+ description: The description of the artifact to create.
195
+ data: The data of the artifact to create.
196
+ client: The PrefectClient
197
+ **kwargs: Additional keyword arguments to use when creating the artifact.
198
+
199
+ Returns:
200
+ The artifact, either retrieved or created.
201
+ """
202
+ artifact = await cls.aget(key, client)
203
+ if artifact:
204
+ return artifact, False
205
+
206
+ new_artifact = cls(key=key, description=description, data=data, **kwargs)
207
+ created_artifact = await new_artifact.acreate(client)
208
+ return created_artifact, True
209
+
210
+ @classmethod
211
+ @async_dispatch(aget_or_create)
212
+ def get_or_create(
213
+ cls,
214
+ key: str | None = None,
215
+ description: str | None = None,
216
+ data: dict[str, Any] | Any | None = None,
217
+ client: "PrefectClient | None" = None,
118
218
  **kwargs: Any,
119
219
  ) -> tuple["ArtifactResponse", bool]:
120
220
  """
121
221
  A method to get or create an artifact.
122
222
 
123
223
  Arguments:
124
- key (str, optional): The key of the artifact to get or create.
125
- description (str, optional): The description of the artifact to create.
126
- data (Union[Dict[str, Any], Any], optional): The data of the artifact to create.
127
- client (PrefectClient, optional): The PrefectClient
224
+ key: The key of the artifact to get or create.
225
+ description: The description of the artifact to create.
226
+ data: The data of the artifact to create.
227
+ client: The PrefectClient
228
+ **kwargs: Additional keyword arguments to use when creating the artifact.
128
229
 
129
230
  Returns:
130
- (ArtifactResponse): The artifact, either retrieved or created.
231
+ The artifact, either retrieved or created.
131
232
  """
132
- artifact_coro = cls.get(key, client)
133
- if TYPE_CHECKING:
134
- assert asyncio.iscoroutine(artifact_coro)
135
- artifact = await artifact_coro
233
+ artifact = cast(ArtifactResponse, cls.get(key, _sync=True)) # pyright: ignore[reportCallIssue] _sync is valid because .get is wrapped in async_dispatch
136
234
  if artifact:
137
235
  return artifact, False
138
236
 
139
237
  new_artifact = cls(key=key, description=description, data=data, **kwargs)
140
- create_coro = new_artifact.create(client)
141
- if TYPE_CHECKING:
142
- assert asyncio.iscoroutine(create_coro)
143
- return await create_coro, True
238
+ created_artifact = cast(
239
+ ArtifactResponse,
240
+ new_artifact.create(_sync=True), # pyright: ignore[reportCallIssue] _sync is valid because .create is wrapped in async_dispatch
241
+ )
242
+ return created_artifact, True
243
+
244
+ # TODO: Remove this when we remove async_dispatch because it doesn't need to be async
245
+ async def aformat(self) -> str | float | int | dict[str, Any]:
246
+ return json.dumps(self.data)
144
247
 
145
- async def format(self) -> Optional[Union[dict[str, Any], Any]]:
248
+ @async_dispatch(aformat)
249
+ def format(self) -> str | float | int | dict[str, Any]:
146
250
  return json.dumps(self.data)
147
251
 
148
252
 
@@ -151,19 +255,30 @@ class LinkArtifact(Artifact):
151
255
  link_text: Optional[str] = None
152
256
  type: Optional[str] = "markdown"
153
257
 
154
- async def format(self) -> str:
258
+ def _format(self) -> str:
155
259
  return (
156
260
  f"[{self.link_text}]({self.link})"
157
261
  if self.link_text
158
262
  else f"[{self.link}]({self.link})"
159
263
  )
160
264
 
265
+ async def aformat(self) -> str:
266
+ return self._format()
267
+
268
+ @async_dispatch(aformat)
269
+ def format(self) -> str:
270
+ return self._format()
271
+
161
272
 
162
273
  class MarkdownArtifact(Artifact):
163
274
  markdown: str
164
275
  type: Optional[str] = "markdown"
165
276
 
166
- async def format(self) -> str:
277
+ async def aformat(self) -> str:
278
+ return self.markdown
279
+
280
+ @async_dispatch(aformat)
281
+ def format(self) -> str:
167
282
  return self.markdown
168
283
 
169
284
 
@@ -173,8 +288,8 @@ class TableArtifact(Artifact):
173
288
 
174
289
  @classmethod
175
290
  def _sanitize(
176
- cls, item: Union[dict[str, Any], list[Any], float]
177
- ) -> Union[dict[str, Any], list[Any], int, float, None]:
291
+ cls, item: dict[str, Any] | list[Any] | float
292
+ ) -> dict[str, Any] | list[Any] | int | float | None:
178
293
  """
179
294
  Sanitize NaN values in a given item.
180
295
  The item can be a dict, list or float.
@@ -188,7 +303,11 @@ class TableArtifact(Artifact):
188
303
  else:
189
304
  return item
190
305
 
191
- async def format(self) -> str:
306
+ async def aformat(self) -> str:
307
+ return json.dumps(self._sanitize(self.table))
308
+
309
+ @async_dispatch(aformat)
310
+ def format(self) -> str:
192
311
  return json.dumps(self._sanitize(self.table))
193
312
 
194
313
 
@@ -196,7 +315,7 @@ class ProgressArtifact(Artifact):
196
315
  progress: float
197
316
  type: Optional[str] = "progress"
198
317
 
199
- async def format(self) -> float:
318
+ def _format(self) -> float:
200
319
  # Ensure progress is between 0 and 100
201
320
  min_progress = 0.0
202
321
  max_progress = 100.0
@@ -209,6 +328,13 @@ class ProgressArtifact(Artifact):
209
328
 
210
329
  return self.progress
211
330
 
331
+ async def aformat(self) -> float:
332
+ return self._format()
333
+
334
+ @async_dispatch(aformat)
335
+ def format(self) -> float:
336
+ return self._format()
337
+
212
338
 
213
339
  class ImageArtifact(Artifact):
214
340
  """
@@ -221,11 +347,14 @@ class ImageArtifact(Artifact):
221
347
  image_url: str
222
348
  type: Optional[str] = "image"
223
349
 
224
- async def format(self) -> str:
350
+ async def aformat(self) -> str:
351
+ return self.image_url
352
+
353
+ @async_dispatch(aformat)
354
+ def format(self) -> str:
225
355
  """
226
356
  This method is used to format the artifact data so it can be properly sent
227
- to the API when the .create() method is called. It is async because the
228
- method is awaited in the parent class.
357
+ to the API when the .create() method is called.
229
358
 
230
359
  Returns:
231
360
  str: The image URL.
@@ -233,13 +362,46 @@ class ImageArtifact(Artifact):
233
362
  return self.image_url
234
363
 
235
364
 
236
- @sync_compatible
237
- async def create_link_artifact(
365
+ async def acreate_link_artifact(
366
+ link: str,
367
+ link_text: str | None = None,
368
+ key: str | None = None,
369
+ description: str | None = None,
370
+ client: "PrefectClient | None" = None,
371
+ ) -> UUID:
372
+ """
373
+ Create a link artifact.
374
+
375
+ Arguments:
376
+ link: The link to create.
377
+ link_text: The link text.
378
+ key: A user-provided string identifier.
379
+ Required for the artifact to show in the Artifacts page in the UI.
380
+ The key must only contain lowercase letters, numbers, and dashes.
381
+ description: A user-specified description of the artifact.
382
+
383
+
384
+ Returns:
385
+ The table artifact ID.
386
+ """
387
+ new_artifact = LinkArtifact(
388
+ key=key,
389
+ description=description,
390
+ link=link,
391
+ link_text=link_text,
392
+ )
393
+ artifact = await new_artifact.acreate(client)
394
+
395
+ return artifact.id
396
+
397
+
398
+ @async_dispatch(acreate_link_artifact)
399
+ def create_link_artifact(
238
400
  link: str,
239
- link_text: Optional[str] = None,
240
- key: Optional[str] = None,
241
- description: Optional[str] = None,
242
- client: Optional["PrefectClient"] = None,
401
+ link_text: str | None = None,
402
+ key: str | None = None,
403
+ description: str | None = None,
404
+ client: "PrefectClient | None" = None,
243
405
  ) -> UUID:
244
406
  """
245
407
  Create a link artifact.
@@ -262,19 +424,44 @@ async def create_link_artifact(
262
424
  link=link,
263
425
  link_text=link_text,
264
426
  )
265
- create_coro = new_artifact.create(client)
266
- if TYPE_CHECKING:
267
- assert asyncio.iscoroutine(create_coro)
268
- artifact = await create_coro
427
+ artifact = cast(ArtifactResponse, new_artifact.create(_sync=True)) # pyright: ignore[reportCallIssue] _sync is valid because .create is wrapped in async_dispatch
428
+
429
+ return artifact.id
430
+
431
+
432
+ async def acreate_markdown_artifact(
433
+ markdown: str,
434
+ key: str | None = None,
435
+ description: str | None = None,
436
+ ) -> UUID:
437
+ """
438
+ Create a markdown artifact.
439
+
440
+ Arguments:
441
+ markdown: The markdown to create.
442
+ key: A user-provided string identifier.
443
+ Required for the artifact to show in the Artifacts page in the UI.
444
+ The key must only contain lowercase letters, numbers, and dashes.
445
+ description: A user-specified description of the artifact.
446
+
447
+ Returns:
448
+ The table artifact ID.
449
+ """
450
+ new_artifact = MarkdownArtifact(
451
+ key=key,
452
+ description=description,
453
+ markdown=markdown,
454
+ )
455
+ artifact = await new_artifact.acreate()
269
456
 
270
457
  return artifact.id
271
458
 
272
459
 
273
- @sync_compatible
274
- async def create_markdown_artifact(
460
+ @async_dispatch(acreate_markdown_artifact)
461
+ def create_markdown_artifact(
275
462
  markdown: str,
276
- key: Optional[str] = None,
277
- description: Optional[str] = None,
463
+ key: str | None = None,
464
+ description: str | None = None,
278
465
  ) -> UUID:
279
466
  """
280
467
  Create a markdown artifact.
@@ -294,19 +481,45 @@ async def create_markdown_artifact(
294
481
  description=description,
295
482
  markdown=markdown,
296
483
  )
297
- create_coro = new_artifact.create()
298
- if TYPE_CHECKING:
299
- assert asyncio.iscoroutine(create_coro)
300
- artifact = await create_coro
484
+ artifact = cast(ArtifactResponse, new_artifact.create(_sync=True)) # pyright: ignore[reportCallIssue] _sync is valid because .create is wrapped in async_dispatch
301
485
 
302
486
  return artifact.id
303
487
 
304
488
 
305
- @sync_compatible
306
- async def create_table_artifact(
307
- table: Union[dict[str, list[Any]], list[dict[str, Any]], list[list[Any]]],
308
- key: Optional[str] = None,
309
- description: Optional[str] = None,
489
+ async def acreate_table_artifact(
490
+ table: dict[str, list[Any]] | list[dict[str, Any]] | list[list[Any]],
491
+ key: str | None = None,
492
+ description: str | None = None,
493
+ ) -> UUID:
494
+ """
495
+ Create a table artifact asynchronously.
496
+
497
+ Arguments:
498
+ table: The table to create.
499
+ key: A user-provided string identifier.
500
+ Required for the artifact to show in the Artifacts page in the UI.
501
+ The key must only contain lowercase letters, numbers, and dashes.
502
+ description: A user-specified description of the artifact.
503
+
504
+ Returns:
505
+ The table artifact ID.
506
+ """
507
+
508
+ new_artifact = TableArtifact(
509
+ key=key,
510
+ description=description,
511
+ table=table,
512
+ )
513
+ artifact = await new_artifact.acreate()
514
+
515
+ return artifact.id
516
+
517
+
518
+ @async_dispatch(acreate_table_artifact)
519
+ def create_table_artifact(
520
+ table: dict[str, list[Any]] | list[dict[str, Any]] | list[list[Any]],
521
+ key: str | None = None,
522
+ description: str | None = None,
310
523
  ) -> UUID:
311
524
  """
312
525
  Create a table artifact.
@@ -327,19 +540,45 @@ async def create_table_artifact(
327
540
  description=description,
328
541
  table=table,
329
542
  )
330
- create_coro = new_artifact.create()
331
- if TYPE_CHECKING:
332
- assert asyncio.iscoroutine(create_coro)
333
- artifact = await create_coro
543
+ artifact = cast(ArtifactResponse, new_artifact.create(_sync=True)) # pyright: ignore[reportCallIssue] _sync is valid because .create is wrapped in async_dispatch
334
544
 
335
545
  return artifact.id
336
546
 
337
547
 
338
- @sync_compatible
339
- async def create_progress_artifact(
548
+ async def acreate_progress_artifact(
340
549
  progress: float,
341
- key: Optional[str] = None,
342
- description: Optional[str] = None,
550
+ key: str | None = None,
551
+ description: str | None = None,
552
+ ) -> UUID:
553
+ """
554
+ Create a progress artifact asynchronously.
555
+
556
+ Arguments:
557
+ progress: The percentage of progress represented by a float between 0 and 100.
558
+ key: A user-provided string identifier.
559
+ Required for the artifact to show in the Artifacts page in the UI.
560
+ The key must only contain lowercase letters, numbers, and dashes.
561
+ description: A user-specified description of the artifact.
562
+
563
+ Returns:
564
+ The progress artifact ID.
565
+ """
566
+
567
+ new_artifact = ProgressArtifact(
568
+ key=key,
569
+ description=description,
570
+ progress=progress,
571
+ )
572
+ artifact = await new_artifact.acreate()
573
+
574
+ return artifact.id
575
+
576
+
577
+ @async_dispatch(acreate_progress_artifact)
578
+ def create_progress_artifact(
579
+ progress: float,
580
+ key: str | None = None,
581
+ description: str | None = None,
343
582
  ) -> UUID:
344
583
  """
345
584
  Create a progress artifact.
@@ -360,20 +599,58 @@ async def create_progress_artifact(
360
599
  description=description,
361
600
  progress=progress,
362
601
  )
363
- create_coro = new_artifact.create()
364
- if TYPE_CHECKING:
365
- assert asyncio.iscoroutine(create_coro)
366
- artifact = await create_coro
602
+ artifact = cast(ArtifactResponse, new_artifact.create(_sync=True)) # pyright: ignore[reportCallIssue] _sync is valid because .create is wrapped in async_dispatch
367
603
 
368
604
  return artifact.id
369
605
 
370
606
 
371
- @sync_compatible
372
- async def update_progress_artifact(
607
+ async def aupdate_progress_artifact(
608
+ artifact_id: UUID,
609
+ progress: float,
610
+ description: str | None = None,
611
+ client: "PrefectClient | None" = None,
612
+ ) -> UUID:
613
+ """
614
+ Update a progress artifact asynchronously.
615
+
616
+ Arguments:
617
+ artifact_id: The ID of the artifact to update.
618
+ progress: The percentage of progress represented by a float between 0 and 100.
619
+ description: A user-specified description of the artifact.
620
+
621
+ Returns:
622
+ The progress artifact ID.
623
+ """
624
+
625
+ local_client_context = nullcontext(client) if client else get_client()
626
+ async with local_client_context as client:
627
+ artifact = ProgressArtifact(
628
+ description=description,
629
+ progress=progress,
630
+ )
631
+ update = (
632
+ ArtifactUpdate(
633
+ description=artifact.description,
634
+ data=await artifact.aformat(),
635
+ )
636
+ if description
637
+ else ArtifactUpdate(data=await artifact.aformat())
638
+ )
639
+
640
+ await client.update_artifact(
641
+ artifact_id=artifact_id,
642
+ artifact=update,
643
+ )
644
+
645
+ return artifact_id
646
+
647
+
648
+ @async_dispatch(aupdate_progress_artifact)
649
+ def update_progress_artifact(
373
650
  artifact_id: UUID,
374
651
  progress: float,
375
- description: Optional[str] = None,
376
- client: Optional["PrefectClient"] = None,
652
+ description: str | None = None,
653
+ client: "PrefectClient | None" = None,
377
654
  ) -> UUID:
378
655
  """
379
656
  Update a progress artifact.
@@ -387,7 +664,7 @@ async def update_progress_artifact(
387
664
  The progress artifact ID.
388
665
  """
389
666
 
390
- client, _ = get_or_create_client(client)
667
+ sync_client = get_client(sync_client=True)
391
668
 
392
669
  artifact = ProgressArtifact(
393
670
  description=description,
@@ -396,13 +673,13 @@ async def update_progress_artifact(
396
673
  update = (
397
674
  ArtifactUpdate(
398
675
  description=artifact.description,
399
- data=await artifact.format(),
676
+ data=cast(float, artifact.format(_sync=True)), # pyright: ignore[reportCallIssue] _sync is valid because .format is wrapped in async_dispatch
400
677
  )
401
678
  if description
402
- else ArtifactUpdate(data=await artifact.format())
679
+ else ArtifactUpdate(data=cast(float, artifact.format(_sync=True))) # pyright: ignore[reportCallIssue] _sync is valid because .format is wrapped in async_dispatch
403
680
  )
404
681
 
405
- await client.update_artifact(
682
+ sync_client.update_artifact(
406
683
  artifact_id=artifact_id,
407
684
  artifact=update,
408
685
  )
@@ -410,11 +687,40 @@ async def update_progress_artifact(
410
687
  return artifact_id
411
688
 
412
689
 
413
- @sync_compatible
414
- async def create_image_artifact(
690
+ async def acreate_image_artifact(
691
+ image_url: str,
692
+ key: str | None = None,
693
+ description: str | None = None,
694
+ ) -> UUID:
695
+ """
696
+ Create an image artifact asynchronously.
697
+
698
+ Arguments:
699
+ image_url: The URL of the image to display.
700
+ key: A user-provided string identifier.
701
+ Required for the artifact to show in the Artifacts page in the UI.
702
+ The key must only contain lowercase letters, numbers, and dashes.
703
+ description: A user-specified description of the artifact.
704
+
705
+ Returns:
706
+ The image artifact ID.
707
+ """
708
+
709
+ new_artifact = ImageArtifact(
710
+ key=key,
711
+ description=description,
712
+ image_url=image_url,
713
+ )
714
+ artifact = await new_artifact.acreate()
715
+
716
+ return artifact.id
717
+
718
+
719
+ @async_dispatch(acreate_image_artifact)
720
+ def create_image_artifact(
415
721
  image_url: str,
416
- key: Optional[str] = None,
417
- description: Optional[str] = None,
722
+ key: str | None = None,
723
+ description: str | None = None,
418
724
  ) -> UUID:
419
725
  """
420
726
  Create an image artifact.
@@ -435,9 +741,6 @@ async def create_image_artifact(
435
741
  description=description,
436
742
  image_url=image_url,
437
743
  )
438
- create_coro = new_artifact.create()
439
- if TYPE_CHECKING:
440
- assert asyncio.iscoroutine(create_coro)
441
- artifact = await create_coro
744
+ artifact = cast(ArtifactResponse, new_artifact.create(_sync=True)) # pyright: ignore[reportCallIssue] _sync is valid because .create is wrapped in async_dispatch
442
745
 
443
746
  return artifact.id