prefect-client 3.4.6.dev2__py3-none-any.whl → 3.4.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. prefect/AGENTS.md +28 -0
  2. prefect/_build_info.py +3 -3
  3. prefect/_internal/websockets.py +109 -0
  4. prefect/artifacts.py +51 -2
  5. prefect/assets/core.py +2 -2
  6. prefect/blocks/core.py +82 -11
  7. prefect/client/cloud.py +11 -1
  8. prefect/client/orchestration/__init__.py +21 -15
  9. prefect/client/orchestration/_deployments/client.py +139 -4
  10. prefect/client/orchestration/_flows/client.py +4 -4
  11. prefect/client/schemas/__init__.py +5 -2
  12. prefect/client/schemas/actions.py +1 -0
  13. prefect/client/schemas/filters.py +3 -0
  14. prefect/client/schemas/objects.py +27 -10
  15. prefect/context.py +6 -4
  16. prefect/events/clients.py +2 -76
  17. prefect/events/schemas/automations.py +4 -0
  18. prefect/events/schemas/labelling.py +2 -0
  19. prefect/flow_engine.py +6 -3
  20. prefect/flows.py +64 -45
  21. prefect/futures.py +25 -4
  22. prefect/locking/filesystem.py +1 -1
  23. prefect/logging/clients.py +347 -0
  24. prefect/runner/runner.py +1 -1
  25. prefect/runner/submit.py +10 -4
  26. prefect/serializers.py +8 -3
  27. prefect/server/api/logs.py +64 -9
  28. prefect/server/api/server.py +2 -0
  29. prefect/server/api/templates.py +8 -2
  30. prefect/settings/context.py +17 -14
  31. prefect/settings/models/server/logs.py +28 -0
  32. prefect/settings/models/server/root.py +5 -0
  33. prefect/settings/models/server/services.py +26 -0
  34. prefect/task_engine.py +17 -17
  35. prefect/task_runners.py +10 -10
  36. prefect/tasks.py +52 -9
  37. prefect/types/__init__.py +2 -0
  38. prefect/types/names.py +50 -0
  39. prefect/utilities/_ast.py +2 -2
  40. prefect/utilities/callables.py +1 -1
  41. prefect/utilities/collections.py +6 -6
  42. prefect/utilities/engine.py +67 -72
  43. prefect/utilities/pydantic.py +19 -1
  44. prefect/workers/base.py +2 -0
  45. {prefect_client-3.4.6.dev2.dist-info → prefect_client-3.4.7.dist-info}/METADATA +1 -1
  46. {prefect_client-3.4.6.dev2.dist-info → prefect_client-3.4.7.dist-info}/RECORD +48 -44
  47. {prefect_client-3.4.6.dev2.dist-info → prefect_client-3.4.7.dist-info}/WHEEL +0 -0
  48. {prefect_client-3.4.6.dev2.dist-info → prefect_client-3.4.7.dist-info}/licenses/LICENSE +0 -0
prefect/AGENTS.md ADDED
@@ -0,0 +1,28 @@
1
+ # Core Prefect SDK
2
+
3
+ The foundation for building and executing workflows with Python.
4
+
5
+ ## Key Components
6
+
7
+ - **Flows & Tasks**: Workflow definition with `@flow` and `@task` decorators
8
+ - **States**: Execution status tracking (Pending, Running, Completed, Failed)
9
+ - **Context**: Runtime information and dependency injection
10
+ - **Results**: Task output persistence and retrieval
11
+ - **Deployments**: Packaging flows for scheduled/triggered execution
12
+ - **Blocks**: Reusable configuration for external systems
13
+
14
+ ## Main Modules
15
+
16
+ - `flows.py` - Flow lifecycle and execution
17
+ - `tasks.py` - Task definition and dependency resolution
18
+ - `engine.py` - Core execution engine
19
+ - `client/` - Server/Cloud API communication
20
+ - `deployments/` - Deployment management
21
+ - `blocks/` - Infrastructure and storage blocks
22
+
23
+ ## SDK-Specific Notes
24
+
25
+ - Async-first execution model with sync support
26
+ - Immutable flow/task definitions after creation
27
+ - State transitions handle retries and caching
28
+ - Backward compatibility required for public APIs
prefect/_build_info.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # Generated by versioningit
2
- __version__ = "3.4.6.dev2"
3
- __build_date__ = "2025-06-11 08:09:26.576114+00:00"
4
- __git_commit__ = "4e82c8d6b8d529a8d8804a58781a68023465c7d4"
2
+ __version__ = "3.4.7"
3
+ __build_date__ = "2025-06-26 21:16:06.964232+00:00"
4
+ __git_commit__ = "cd81d15a41aece3a78ae7664b59a2ad841aef1f8"
5
5
  __dirty__ = False
@@ -0,0 +1,109 @@
1
+ """
2
+ Internal WebSocket proxy utilities for Prefect client connections.
3
+
4
+ This module provides shared WebSocket proxy connection logic and SSL configuration
5
+ to avoid duplication between events and logs clients.
6
+ """
7
+
8
+ import os
9
+ import ssl
10
+ from typing import Any, Generator, Optional
11
+ from urllib.parse import urlparse
12
+ from urllib.request import proxy_bypass
13
+
14
+ import certifi
15
+ from python_socks.async_.asyncio import Proxy
16
+ from typing_extensions import Self
17
+ from websockets.asyncio.client import ClientConnection, connect
18
+
19
+ from prefect.settings import get_current_settings
20
+
21
+
22
+ def create_ssl_context_for_websocket(uri: str) -> Optional[ssl.SSLContext]:
23
+ """Create SSL context for WebSocket connections based on URI scheme."""
24
+ u = urlparse(uri)
25
+
26
+ if u.scheme != "wss":
27
+ return None
28
+
29
+ if get_current_settings().api.tls_insecure_skip_verify:
30
+ # Create an unverified context for insecure connections
31
+ ctx = ssl.create_default_context()
32
+ ctx.check_hostname = False
33
+ ctx.verify_mode = ssl.CERT_NONE
34
+ return ctx
35
+ else:
36
+ # Create a verified context with the certificate file
37
+ cert_file = get_current_settings().api.ssl_cert_file
38
+ if not cert_file:
39
+ cert_file = certifi.where()
40
+ return ssl.create_default_context(cafile=cert_file)
41
+
42
+
43
+ class WebsocketProxyConnect(connect):
44
+ """
45
+ WebSocket connection class with proxy and SSL support.
46
+
47
+ Extends the websockets.asyncio.client.connect class to add HTTP/HTTPS
48
+ proxy support via environment variables, proxy bypass logic, and SSL
49
+ certificate verification.
50
+ """
51
+
52
+ def __init__(self: Self, uri: str, **kwargs: Any):
53
+ # super() is intentionally deferred to the _proxy_connect method
54
+ # to allow for the socket to be established first
55
+
56
+ self.uri = uri
57
+ self._kwargs = kwargs
58
+
59
+ u = urlparse(uri)
60
+ host = u.hostname
61
+
62
+ if not host:
63
+ raise ValueError(f"Invalid URI {uri}, no hostname found")
64
+
65
+ if u.scheme == "ws":
66
+ port = u.port or 80
67
+ proxy_url = os.environ.get("HTTP_PROXY")
68
+ elif u.scheme == "wss":
69
+ port = u.port or 443
70
+ proxy_url = os.environ.get("HTTPS_PROXY")
71
+ kwargs["server_hostname"] = host
72
+ else:
73
+ raise ValueError(
74
+ "Unsupported scheme %s. Expected 'ws' or 'wss'. " % u.scheme
75
+ )
76
+
77
+ # Store proxy URL for deferred creation. Creating the proxy object here
78
+ # can bind asyncio futures to the wrong event loop when multiple WebSocket
79
+ # connections are initialized at different times (e.g., events + logs clients).
80
+ self._proxy_url = proxy_url if proxy_url and not proxy_bypass(host) else None
81
+ self._host = host
82
+ self._port = port
83
+
84
+ # Configure SSL context for HTTPS connections
85
+ ssl_context = create_ssl_context_for_websocket(uri)
86
+ if ssl_context:
87
+ self._kwargs.setdefault("ssl", ssl_context)
88
+
89
+ async def _proxy_connect(self: Self) -> ClientConnection:
90
+ if self._proxy_url:
91
+ # Create proxy in the current event loop context
92
+ proxy = Proxy.from_url(self._proxy_url)
93
+ sock = await proxy.connect(
94
+ dest_host=self._host,
95
+ dest_port=self._port,
96
+ )
97
+ self._kwargs["sock"] = sock
98
+
99
+ super().__init__(self.uri, **self._kwargs)
100
+ proto = await self.__await_impl__()
101
+ return proto
102
+
103
+ def __await__(self: Self) -> Generator[Any, None, ClientConnection]:
104
+ return self._proxy_connect().__await__()
105
+
106
+
107
+ def websocket_connect(uri: str, **kwargs: Any) -> WebsocketProxyConnect:
108
+ """Create a WebSocket connection with proxy and SSL support."""
109
+ return WebsocketProxyConnect(uri, **kwargs)
prefect/artifacts.py CHANGED
@@ -55,7 +55,7 @@ class Artifact(ArtifactRequest):
55
55
  client: The PrefectClient
56
56
 
57
57
  Returns:
58
- - The created artifact.
58
+ The created artifact.
59
59
  """
60
60
 
61
61
  local_client_context = asyncnullcontext(client) if client else get_client()
@@ -93,7 +93,7 @@ class Artifact(ArtifactRequest):
93
93
  client: The PrefectClient
94
94
 
95
95
  Returns:
96
- - The created artifact.
96
+ The created artifact.
97
97
  """
98
98
 
99
99
  # Create sync client since this is a sync method.
@@ -417,6 +417,23 @@ def create_link_artifact(
417
417
 
418
418
  Returns:
419
419
  The table artifact ID.
420
+
421
+ Example:
422
+ ```python
423
+ from prefect import flow
424
+ from prefect.artifacts import create_link_artifact
425
+
426
+ @flow
427
+ def my_flow():
428
+ create_link_artifact(
429
+ link="https://www.prefect.io",
430
+ link_text="Prefect",
431
+ key="prefect-link",
432
+ description="This is a link to the Prefect website",
433
+ )
434
+
435
+ my_flow()
436
+ ```
420
437
  """
421
438
  new_artifact = LinkArtifact(
422
439
  key=key,
@@ -475,6 +492,22 @@ def create_markdown_artifact(
475
492
 
476
493
  Returns:
477
494
  The table artifact ID.
495
+
496
+ Example:
497
+ ```python
498
+ from prefect import flow
499
+ from prefect.artifacts import create_markdown_artifact
500
+
501
+ @flow
502
+ def my_flow():
503
+ create_markdown_artifact(
504
+ markdown="## Prefect",
505
+ key="prefect-markdown",
506
+ description="This is a markdown artifact",
507
+ )
508
+
509
+ my_flow()
510
+ ```
478
511
  """
479
512
  new_artifact = MarkdownArtifact(
480
513
  key=key,
@@ -533,6 +566,22 @@ def create_table_artifact(
533
566
 
534
567
  Returns:
535
568
  The table artifact ID.
569
+
570
+ Example:
571
+ ```python
572
+ from prefect import flow
573
+ from prefect.artifacts import create_table_artifact
574
+
575
+ @flow
576
+ def my_flow():
577
+ create_table_artifact(
578
+ table=[{"name": "John", "age": 30}, {"name": "Jane", "age": 25}],
579
+ key="prefect-table",
580
+ description="This is a table artifact",
581
+ )
582
+
583
+ my_flow()
584
+ ```
536
585
  """
537
586
 
538
587
  new_artifact = TableArtifact(
prefect/assets/core.py CHANGED
@@ -5,7 +5,7 @@ from typing import Any, ClassVar, Optional
5
5
  from pydantic import ConfigDict, Field
6
6
 
7
7
  from prefect._internal.schemas.bases import PrefectBaseModel
8
- from prefect.types import URILike
8
+ from prefect.types import ValidAssetKey
9
9
 
10
10
 
11
11
  class AssetProperties(PrefectBaseModel):
@@ -37,7 +37,7 @@ class Asset(PrefectBaseModel):
37
37
 
38
38
  model_config: ClassVar[ConfigDict] = ConfigDict(frozen=True)
39
39
 
40
- key: URILike
40
+ key: ValidAssetKey
41
41
  properties: Optional[AssetProperties] = Field(
42
42
  default=None,
43
43
  description="Properties of the asset. "
prefect/blocks/core.py CHANGED
@@ -175,11 +175,29 @@ def _collect_secret_fields(
175
175
  )
176
176
  return
177
177
 
178
- if type_ in (SecretStr, SecretBytes) or (
178
+ # Check if this is a pydantic Secret type (including generic Secret[T])
179
+ is_pydantic_secret = False
180
+
181
+ # Direct check for SecretStr, SecretBytes
182
+ if type_ in (SecretStr, SecretBytes):
183
+ is_pydantic_secret = True
184
+ # Check for base Secret class
185
+ elif (
179
186
  isinstance(type_, type) # type: ignore[unnecessaryIsInstance]
180
187
  and getattr(type_, "__module__", None) == "pydantic.types"
181
188
  and getattr(type_, "__name__", None) == "Secret"
182
189
  ):
190
+ is_pydantic_secret = True
191
+ # Check for generic Secret[T] (e.g., Secret[str], Secret[int])
192
+ elif get_origin(type_) is not None:
193
+ origin = get_origin(type_)
194
+ if (
195
+ getattr(origin, "__module__", None) == "pydantic.types"
196
+ and getattr(origin, "__name__", None) == "Secret"
197
+ ):
198
+ is_pydantic_secret = True
199
+
200
+ if is_pydantic_secret:
183
201
  secrets.append(name)
184
202
  elif type_ == SecretDict:
185
203
  # Append .* to field name to signify that all values under a given key are secret and should be obfuscated.
@@ -370,16 +388,27 @@ class Block(BaseModel, ABC):
370
388
  ) -> Any:
371
389
  jsonable_self = handler(self)
372
390
  if (ctx := info.context) and ctx.get("include_secrets") is True:
373
- jsonable_self.update(
374
- {
375
- field_name: visit_collection(
376
- expr=getattr(self, field_name),
377
- visit_fn=partial(handle_secret_render, context=ctx),
378
- return_data=True,
379
- )
380
- for field_name in type(self).model_fields
381
- }
382
- )
391
+ # Add serialization mode to context so handle_secret_render knows how to process nested models
392
+ ctx["serialization_mode"] = info.mode
393
+
394
+ for field_name in type(self).model_fields:
395
+ field_value = getattr(self, field_name)
396
+
397
+ # In JSON mode, skip fields that don't contain secrets
398
+ # as they're already properly serialized by the handler
399
+ if (
400
+ info.mode == "json"
401
+ and field_name in jsonable_self
402
+ and not self._field_has_secrets(field_name)
403
+ ):
404
+ continue
405
+
406
+ # For all other fields, use visit_collection with handle_secret_render
407
+ jsonable_self[field_name] = visit_collection(
408
+ expr=field_value,
409
+ visit_fn=partial(handle_secret_render, context=ctx),
410
+ return_data=True,
411
+ )
383
412
  extra_fields = {
384
413
  "block_type_slug": self.get_block_type_slug(),
385
414
  "_block_document_id": self._block_document_id,
@@ -477,6 +506,25 @@ class Block(BaseModel, ABC):
477
506
  else:
478
507
  return f"sha256:{checksum}"
479
508
 
509
+ def _field_has_secrets(self, field_name: str) -> bool:
510
+ """Check if a field contains secrets based on the schema's secret_fields."""
511
+ secret_fields = self.model_json_schema().get("secret_fields", [])
512
+
513
+ # Check if field_name matches any secret field pattern
514
+ for secret_field in secret_fields:
515
+ if secret_field == field_name:
516
+ return True
517
+ elif secret_field.startswith(f"{field_name}."):
518
+ # This field contains nested secrets
519
+ return True
520
+ elif secret_field.endswith(".*"):
521
+ # Handle wildcard patterns like "field.*"
522
+ prefix = secret_field[:-2] # Remove .*
523
+ if field_name == prefix:
524
+ return True
525
+
526
+ return False
527
+
480
528
  def _to_block_document(
481
529
  self,
482
530
  name: Optional[str] = None,
@@ -530,6 +578,29 @@ class Block(BaseModel, ABC):
530
578
  context={"include_secrets": include_secrets},
531
579
  )
532
580
 
581
+ # Ensure non-secret fields are JSON-serializable to avoid issues with types
582
+ # like SemanticVersion when the BlockDocument is later serialized
583
+ try:
584
+ json_data = self.model_dump(
585
+ mode="json",
586
+ by_alias=True,
587
+ include=data_keys,
588
+ context={"include_secrets": include_secrets},
589
+ )
590
+ # Replace non-secret, non-Block fields with their JSON representation
591
+ # We need to check the original field to determine if it's a secret or Block
592
+ for key in data_keys:
593
+ if key in block_document_data and key in json_data:
594
+ field_value = getattr(self, key)
595
+ # Only replace if the field doesn't contain secrets and is not a Block
596
+ if not self._field_has_secrets(key) and not isinstance(
597
+ field_value, Block
598
+ ):
599
+ block_document_data[key] = json_data[key]
600
+ except Exception:
601
+ # If JSON serialization fails, we'll handle it later
602
+ pass
603
+
533
604
  # Iterate through and find blocks that already have saved block documents to
534
605
  # create references to those saved block documents.
535
606
  for key in data_keys:
prefect/client/cloud.py CHANGED
@@ -24,6 +24,16 @@ from prefect.settings import (
24
24
 
25
25
  PARSE_API_URL_REGEX = re.compile(r"accounts/(.{36})/workspaces/(.{36})")
26
26
 
27
+ # Cache for TypeAdapter instances to avoid repeated instantiation
28
+ _TYPE_ADAPTER_CACHE: dict[type, pydantic.TypeAdapter[Any]] = {}
29
+
30
+
31
+ def _get_type_adapter(type_: type) -> pydantic.TypeAdapter[Any]:
32
+ """Get or create a cached TypeAdapter for the given type."""
33
+ if type_ not in _TYPE_ADAPTER_CACHE:
34
+ _TYPE_ADAPTER_CACHE[type_] = pydantic.TypeAdapter(type_)
35
+ return _TYPE_ADAPTER_CACHE[type_]
36
+
27
37
 
28
38
  def get_cloud_client(
29
39
  host: Optional[str] = None,
@@ -112,7 +122,7 @@ class CloudClient:
112
122
  await self.read_workspaces()
113
123
 
114
124
  async def read_workspaces(self) -> list[Workspace]:
115
- workspaces = pydantic.TypeAdapter(list[Workspace]).validate_python(
125
+ workspaces = _get_type_adapter(list[Workspace]).validate_python(
116
126
  await self.get("/me/workspaces")
117
127
  )
118
128
  return workspaces
@@ -101,10 +101,11 @@ from prefect.client.schemas.filters import (
101
101
  WorkQueueFilterName,
102
102
  )
103
103
  from prefect.client.schemas.objects import (
104
- Constant,
104
+ TaskRunResult,
105
+ FlowRunResult,
105
106
  Parameter,
107
+ Constant,
106
108
  TaskRunPolicy,
107
- TaskRunResult,
108
109
  WorkQueue,
109
110
  WorkQueueStatusDetail,
110
111
  )
@@ -144,6 +145,16 @@ P = ParamSpec("P")
144
145
  R = TypeVar("R", infer_variance=True)
145
146
  T = TypeVar("T")
146
147
 
148
+ # Cache for TypeAdapter instances to avoid repeated instantiation
149
+ _TYPE_ADAPTER_CACHE: dict[type, pydantic.TypeAdapter[Any]] = {}
150
+
151
+
152
+ def _get_type_adapter(type_: type) -> pydantic.TypeAdapter[Any]:
153
+ """Get or create a cached TypeAdapter for the given type."""
154
+ if type_ not in _TYPE_ADAPTER_CACHE:
155
+ _TYPE_ADAPTER_CACHE[type_] = pydantic.TypeAdapter(type_)
156
+ return _TYPE_ADAPTER_CACHE[type_]
157
+
147
158
 
148
159
  @overload
149
160
  def get_client(
@@ -635,7 +646,7 @@ class PrefectClient(
635
646
  raise prefect.exceptions.ObjectNotFound(http_exc=e) from e
636
647
  else:
637
648
  raise
638
- return pydantic.TypeAdapter(list[FlowRun]).validate_python(response.json())
649
+ return _get_type_adapter(list[FlowRun]).validate_python(response.json())
639
650
 
640
651
  async def read_work_queue(
641
652
  self,
@@ -768,13 +779,7 @@ class PrefectClient(
768
779
  task_inputs: Optional[
769
780
  dict[
770
781
  str,
771
- list[
772
- Union[
773
- TaskRunResult,
774
- Parameter,
775
- Constant,
776
- ]
777
- ],
782
+ list[Union[TaskRunResult, FlowRunResult, Parameter, Constant]],
778
783
  ]
779
784
  ] = None,
780
785
  ) -> TaskRun:
@@ -894,7 +899,7 @@ class PrefectClient(
894
899
  "offset": offset,
895
900
  }
896
901
  response = await self._client.post("/task_runs/filter", json=body)
897
- return pydantic.TypeAdapter(list[TaskRun]).validate_python(response.json())
902
+ return _get_type_adapter(list[TaskRun]).validate_python(response.json())
898
903
 
899
904
  async def delete_task_run(self, task_run_id: UUID) -> None:
900
905
  """
@@ -958,7 +963,7 @@ class PrefectClient(
958
963
  response = await self._client.get(
959
964
  "/task_run_states/", params=dict(task_run_id=str(task_run_id))
960
965
  )
961
- return pydantic.TypeAdapter(list[prefect.states.State]).validate_python(
966
+ return _get_type_adapter(list[prefect.states.State]).validate_python(
962
967
  response.json()
963
968
  )
964
969
 
@@ -1005,7 +1010,7 @@ class PrefectClient(
1005
1010
  else:
1006
1011
  response = await self._client.post("/work_queues/filter", json=json)
1007
1012
 
1008
- return pydantic.TypeAdapter(list[WorkQueue]).validate_python(response.json())
1013
+ return _get_type_adapter(list[WorkQueue]).validate_python(response.json())
1009
1014
 
1010
1015
  async def read_worker_metadata(self) -> dict[str, Any]:
1011
1016
  """Reads worker metadata stored in Prefect collection registry."""
@@ -1430,6 +1435,7 @@ class SyncPrefectClient(
1430
1435
  list[
1431
1436
  Union[
1432
1437
  TaskRunResult,
1438
+ FlowRunResult,
1433
1439
  Parameter,
1434
1440
  Constant,
1435
1441
  ]
@@ -1554,7 +1560,7 @@ class SyncPrefectClient(
1554
1560
  "offset": offset,
1555
1561
  }
1556
1562
  response = self._client.post("/task_runs/filter", json=body)
1557
- return pydantic.TypeAdapter(list[TaskRun]).validate_python(response.json())
1563
+ return _get_type_adapter(list[TaskRun]).validate_python(response.json())
1558
1564
 
1559
1565
  def set_task_run_state(
1560
1566
  self,
@@ -1598,6 +1604,6 @@ class SyncPrefectClient(
1598
1604
  response = self._client.get(
1599
1605
  "/task_run_states/", params=dict(task_run_id=str(task_run_id))
1600
1606
  )
1601
- return pydantic.TypeAdapter(list[prefect.states.State]).validate_python(
1607
+ return _get_type_adapter(list[prefect.states.State]).validate_python(
1602
1608
  response.json()
1603
1609
  )