prefect-client 3.4.0__py3-none-any.whl → 3.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ Routes for interacting with work queue objects.
3
3
  """
4
4
 
5
5
  from typing import TYPE_CHECKING, List, Optional
6
- from uuid import UUID, uuid4
6
+ from uuid import UUID
7
7
 
8
8
  import sqlalchemy as sa
9
9
  from fastapi import (
@@ -20,6 +20,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
20
20
  import prefect.server.api.dependencies as dependencies
21
21
  import prefect.server.models as models
22
22
  import prefect.server.schemas as schemas
23
+ from prefect._internal.uuid7 import uuid7
23
24
  from prefect.server.api.validation import validate_job_variable_defaults_for_work_pool
24
25
  from prefect.server.database import PrefectDBInterface, provide_database_interface
25
26
  from prefect.server.models.deployments import mark_deployments_ready
@@ -184,7 +185,7 @@ async def create_work_pool(
184
185
  )
185
186
 
186
187
  await emit_work_pool_status_event(
187
- event_id=uuid4(),
188
+ event_id=uuid7(),
188
189
  occurred=now("UTC"),
189
190
  pre_update_work_pool=None,
190
191
  work_pool=model,
prefect/task_runners.py CHANGED
@@ -21,6 +21,7 @@ from typing import (
21
21
 
22
22
  from typing_extensions import ParamSpec, Self, TypeVar
23
23
 
24
+ from prefect._internal.uuid7 import uuid7
24
25
  from prefect.client.schemas.objects import TaskRunInput
25
26
  from prefect.exceptions import MappingLengthMismatch, MappingMissingIterable
26
27
  from prefect.futures import (
@@ -290,7 +291,7 @@ class ThreadPoolTaskRunner(TaskRunner[PrefectConcurrentFuture[R]]):
290
291
  from prefect.context import FlowRunContext
291
292
  from prefect.task_engine import run_task_async, run_task_sync
292
293
 
293
- task_run_id = uuid.uuid4()
294
+ task_run_id = uuid7()
294
295
  cancel_event = threading.Event()
295
296
  self._cancel_events[task_run_id] = cancel_event
296
297
  context = copy_context()
prefect/tasks.py CHANGED
@@ -32,6 +32,7 @@ from uuid import UUID, uuid4
32
32
  from typing_extensions import Literal, ParamSpec, Self, TypeAlias, TypeIs
33
33
 
34
34
  import prefect.states
35
+ from prefect._internal.uuid7 import uuid7
35
36
  from prefect.cache_policies import DEFAULT, NO_CACHE, CachePolicy
36
37
  from prefect.client.orchestration import get_client
37
38
  from prefect.client.schemas import TaskRun
@@ -953,7 +954,7 @@ class Task(Generic[P, R]):
953
954
  if flow_run_context and flow_run_context.flow_run
954
955
  else None
955
956
  )
956
- task_run_id = id or uuid4()
957
+ task_run_id = id or uuid7()
957
958
  state = prefect.states.Pending(
958
959
  state_details=StateDetails(
959
960
  task_run_id=task_run_id,
@@ -1551,7 +1552,7 @@ class Task(Generic[P, R]):
1551
1552
  validated_state=task_run.state,
1552
1553
  )
1553
1554
 
1554
- if task_run_url := url_for(task_run):
1555
+ if get_current_settings().ui_url and (task_run_url := url_for(task_run)):
1555
1556
  logger.info(
1556
1557
  f"Created task run {task_run.name!r}. View it in the UI at {task_run_url!r}"
1557
1558
  )
prefect/types/__init__.py CHANGED
@@ -1,13 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from functools import partial
4
- from typing import Annotated, Any, Dict, List, Optional, Set, TypeVar, Union
4
+ from typing import Annotated, Any, Optional, TypeVar, Union, cast
5
+ from uuid import UUID
5
6
  from typing_extensions import Literal
6
7
  import orjson
7
8
  import pydantic
8
9
 
9
-
10
10
  from ._datetime import DateTime, Date
11
+ from .names import (
12
+ Name,
13
+ NameOrEmpty,
14
+ NonEmptyishName,
15
+ BANNED_CHARACTERS,
16
+ WITHOUT_BANNED_CHARACTERS,
17
+ MAX_VARIABLE_NAME_LENGTH,
18
+ )
11
19
  from pydantic import (
12
20
  BeforeValidator,
13
21
  Field,
@@ -21,7 +29,6 @@ from zoneinfo import available_timezones
21
29
 
22
30
  T = TypeVar("T")
23
31
 
24
- MAX_VARIABLE_NAME_LENGTH = 255
25
32
  MAX_VARIABLE_VALUE_LENGTH = 5000
26
33
 
27
34
  NonNegativeInteger = Annotated[int, Field(ge=0)]
@@ -39,37 +46,14 @@ TimeZone = Annotated[
39
46
  ]
40
47
 
41
48
 
42
- BANNED_CHARACTERS = ["/", "%", "&", ">", "<"]
43
-
44
- WITHOUT_BANNED_CHARACTERS = r"^[^" + "".join(BANNED_CHARACTERS) + "]+$"
45
- Name = Annotated[str, Field(pattern=WITHOUT_BANNED_CHARACTERS)]
46
-
47
- WITHOUT_BANNED_CHARACTERS_EMPTY_OK = r"^[^" + "".join(BANNED_CHARACTERS) + "]*$"
48
- NameOrEmpty = Annotated[str, Field(pattern=WITHOUT_BANNED_CHARACTERS_EMPTY_OK)]
49
-
50
-
51
- def non_emptyish(value: str) -> str:
52
- if not value.strip("' \""):
53
- raise ValueError("name cannot be an empty string")
54
-
55
- return value
56
-
57
-
58
- NonEmptyishName = Annotated[
59
- str,
60
- Field(pattern=WITHOUT_BANNED_CHARACTERS),
61
- BeforeValidator(non_emptyish),
62
- ]
63
-
64
-
65
49
  VariableValue = Union[
66
50
  StrictStr,
67
51
  StrictInt,
68
52
  StrictBool,
69
53
  StrictFloat,
70
54
  None,
71
- Dict[str, Any],
72
- List[Any],
55
+ dict[str, Any],
56
+ list[Any],
73
57
  ]
74
58
 
75
59
 
@@ -100,24 +84,24 @@ def cast_none_to_empty_dict(value: Any) -> dict[str, Any]:
100
84
 
101
85
 
102
86
  KeyValueLabels = Annotated[
103
- Dict[str, Union[StrictBool, StrictInt, StrictFloat, str]],
87
+ dict[str, Union[StrictBool, StrictInt, StrictFloat, str]],
104
88
  BeforeValidator(cast_none_to_empty_dict),
105
89
  ]
106
90
 
107
91
 
108
92
  ListOfNonEmptyStrings = Annotated[
109
- List[str],
93
+ list[str],
110
94
  BeforeValidator(lambda x: [str(s) for s in x if str(s).strip()]),
111
95
  ]
112
96
 
113
97
 
114
- class SecretDict(pydantic.Secret[Dict[str, Any]]):
98
+ class SecretDict(pydantic.Secret[dict[str, Any]]):
115
99
  pass
116
100
 
117
101
 
118
102
  def validate_set_T_from_delim_string(
119
- value: Union[str, T, Set[T], None], type_: type[T], delim: str | None = None
120
- ) -> Set[T]:
103
+ value: Union[str, T, set[T], None], type_: Any, delim: str | None = None
104
+ ) -> set[T]:
121
105
  """
122
106
  "no-info" before validator useful in scooping env vars
123
107
 
@@ -131,20 +115,20 @@ def validate_set_T_from_delim_string(
131
115
  delim = delim or ","
132
116
  if isinstance(value, str):
133
117
  return {T_adapter.validate_strings(s.strip()) for s in value.split(delim)}
134
- errors = []
118
+ errors: list[pydantic.ValidationError] = []
135
119
  try:
136
120
  return {T_adapter.validate_python(value)}
137
121
  except pydantic.ValidationError as e:
138
122
  errors.append(e)
139
123
  try:
140
- return TypeAdapter(Set[type_]).validate_python(value)
124
+ return TypeAdapter(set[type_]).validate_python(value)
141
125
  except pydantic.ValidationError as e:
142
126
  errors.append(e)
143
127
  raise ValueError(f"Invalid set[{type_}]: {errors}")
144
128
 
145
129
 
146
130
  ClientRetryExtraCodes = Annotated[
147
- Union[str, StatusCode, Set[StatusCode], None],
131
+ Union[str, StatusCode, set[StatusCode], None],
148
132
  BeforeValidator(partial(validate_set_T_from_delim_string, type_=StatusCode)),
149
133
  ]
150
134
 
@@ -170,11 +154,15 @@ KeyValueLabelsField = Annotated[
170
154
 
171
155
 
172
156
  __all__ = [
157
+ "BANNED_CHARACTERS",
158
+ "WITHOUT_BANNED_CHARACTERS",
173
159
  "ClientRetryExtraCodes",
174
160
  "Date",
175
161
  "DateTime",
176
162
  "LogLevel",
177
163
  "KeyValueLabelsField",
164
+ "MAX_VARIABLE_NAME_LENGTH",
165
+ "MAX_VARIABLE_VALUE_LENGTH",
178
166
  "NonNegativeInteger",
179
167
  "PositiveInteger",
180
168
  "ListOfNonEmptyStrings",
prefect/types/names.py ADDED
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from functools import partial
5
+ from typing import Annotated, overload
6
+
7
+ from pydantic import AfterValidator, BeforeValidator, Field
8
+
9
+ LOWERCASE_LETTERS_NUMBERS_AND_DASHES_ONLY_REGEX = "^[a-z0-9-]*$"
10
+ LOWERCASE_LETTERS_NUMBERS_AND_UNDERSCORES_REGEX = "^[a-z0-9_]*$"
11
+ LOWERCASE_LETTERS_NUMBERS_AND_DASHES_OR_UNDERSCORES_REGEX = "^[a-z0-9-_]*$"
12
+
13
+
14
+ @overload
15
+ def raise_on_name_alphanumeric_dashes_only(
16
+ value: str, field_name: str = ...
17
+ ) -> str: ...
18
+
19
+
20
+ @overload
21
+ def raise_on_name_alphanumeric_dashes_only(
22
+ value: None, field_name: str = ...
23
+ ) -> None: ...
24
+
25
+
26
+ def raise_on_name_alphanumeric_dashes_only(
27
+ value: str | None, field_name: str = "value"
28
+ ) -> str | None:
29
+ if value is not None and not bool(
30
+ re.match(LOWERCASE_LETTERS_NUMBERS_AND_DASHES_ONLY_REGEX, value)
31
+ ):
32
+ raise ValueError(
33
+ f"{field_name} must only contain lowercase letters, numbers, and dashes."
34
+ )
35
+ return value
36
+
37
+
38
+ @overload
39
+ def raise_on_name_alphanumeric_underscores_only(
40
+ value: str, field_name: str = ...
41
+ ) -> str: ...
42
+
43
+
44
+ @overload
45
+ def raise_on_name_alphanumeric_underscores_only(
46
+ value: None, field_name: str = ...
47
+ ) -> None: ...
48
+
49
+
50
+ def raise_on_name_alphanumeric_underscores_only(
51
+ value: str | None, field_name: str = "value"
52
+ ) -> str | None:
53
+ if value is not None and not re.match(
54
+ LOWERCASE_LETTERS_NUMBERS_AND_UNDERSCORES_REGEX, value
55
+ ):
56
+ raise ValueError(
57
+ f"{field_name} must only contain lowercase letters, numbers, and"
58
+ " underscores."
59
+ )
60
+ return value
61
+
62
+
63
+ def raise_on_name_alphanumeric_dashes_underscores_only(
64
+ value: str, field_name: str = "value"
65
+ ) -> str:
66
+ if not re.match(LOWERCASE_LETTERS_NUMBERS_AND_DASHES_OR_UNDERSCORES_REGEX, value):
67
+ raise ValueError(
68
+ f"{field_name} must only contain lowercase letters, numbers, and"
69
+ " dashes or underscores."
70
+ )
71
+ return value
72
+
73
+
74
+ BANNED_CHARACTERS = ["/", "%", "&", ">", "<"]
75
+
76
+ WITHOUT_BANNED_CHARACTERS = r"^[^" + "".join(BANNED_CHARACTERS) + "]+$"
77
+ Name = Annotated[str, Field(pattern=WITHOUT_BANNED_CHARACTERS)]
78
+
79
+ WITHOUT_BANNED_CHARACTERS_EMPTY_OK = r"^[^" + "".join(BANNED_CHARACTERS) + "]*$"
80
+ NameOrEmpty = Annotated[str, Field(pattern=WITHOUT_BANNED_CHARACTERS_EMPTY_OK)]
81
+
82
+
83
+ def non_emptyish(value: str) -> str:
84
+ if not value.strip("' \""):
85
+ raise ValueError("name cannot be an empty string")
86
+
87
+ return value
88
+
89
+
90
+ NonEmptyishName = Annotated[
91
+ str,
92
+ Field(pattern=WITHOUT_BANNED_CHARACTERS),
93
+ BeforeValidator(non_emptyish),
94
+ ]
95
+
96
+
97
+ ### specific names
98
+
99
+ BlockDocumentName = Annotated[
100
+ Name,
101
+ AfterValidator(
102
+ partial(
103
+ raise_on_name_alphanumeric_dashes_only, field_name="Block document name"
104
+ )
105
+ ),
106
+ ]
107
+
108
+
109
+ BlockTypeSlug = Annotated[
110
+ str,
111
+ AfterValidator(
112
+ partial(raise_on_name_alphanumeric_dashes_only, field_name="Block type slug")
113
+ ),
114
+ ]
115
+
116
+ ArtifactKey = Annotated[
117
+ str,
118
+ AfterValidator(
119
+ partial(raise_on_name_alphanumeric_dashes_only, field_name="Artifact key")
120
+ ),
121
+ ]
122
+
123
+ MAX_VARIABLE_NAME_LENGTH = 255
124
+
125
+
126
+ VariableName = Annotated[
127
+ str,
128
+ AfterValidator(
129
+ partial(
130
+ raise_on_name_alphanumeric_dashes_underscores_only,
131
+ field_name="Variable name",
132
+ )
133
+ ),
134
+ Field(
135
+ max_length=MAX_VARIABLE_NAME_LENGTH,
136
+ description="The name of the variable",
137
+ examples=["my_variable"],
138
+ ),
139
+ ]
@@ -495,10 +495,11 @@ def parse_image_tag(name: str) -> tuple[str, Optional[str]]:
495
495
  """
496
496
  Parse Docker Image String
497
497
 
498
- - If a tag exists, this function parses and returns the image registry and tag,
498
+ - If a tag or digest exists, this function parses and returns the image registry and tag/digest,
499
499
  separately as a tuple.
500
500
  - Example 1: 'prefecthq/prefect:latest' -> ('prefecthq/prefect', 'latest')
501
501
  - Example 2: 'hostname.io:5050/folder/subfolder:latest' -> ('hostname.io:5050/folder/subfolder', 'latest')
502
+ - Example 3: 'prefecthq/prefect@sha256:abc123' -> ('prefecthq/prefect', 'sha256:abc123')
502
503
  - Supports parsing Docker Image strings that follow Docker Image Specification v1.1.0
503
504
  - Image building tools typically enforce this standard
504
505
 
@@ -506,26 +507,35 @@ def parse_image_tag(name: str) -> tuple[str, Optional[str]]:
506
507
  name (str): Name of Docker Image
507
508
 
508
509
  Return:
509
- tuple: image registry, image tag
510
+ tuple: image registry, image tag/digest
510
511
  """
511
512
  tag = None
512
513
  name_parts = name.split("/")
513
- # First handles the simplest image names (DockerHub-based, index-free, potentionally with a tag)
514
- # - Example: simplename:latest
514
+
515
+ # First handles the simplest image names (DockerHub-based, index-free, potentially with a tag or digest)
516
+ # - Example: simplename:latest or simplename@sha256:abc123
515
517
  if len(name_parts) == 1:
516
- if ":" in name_parts[0]:
518
+ if "@" in name_parts[0]:
519
+ image_name, tag = name_parts[0].split("@")
520
+ elif ":" in name_parts[0]:
517
521
  image_name, tag = name_parts[0].split(":")
522
+
518
523
  else:
519
524
  image_name = name_parts[0]
520
525
  else:
521
526
  # 1. Separates index (hostname.io or prefecthq) from path:tag (folder/subfolder:latest or prefect:latest)
522
- # 2. Separates path and tag (if tag exists)
523
- # 3. Reunites index and path (without tag) as image name
527
+ # 2. Separates path and tag/digest (if exists)
528
+ # 3. Reunites index and path (without tag/digest) as image name
524
529
  index_name = name_parts[0]
525
530
  image_path = "/".join(name_parts[1:])
526
- if ":" in image_path:
531
+
532
+ if "@" in image_path:
533
+ image_path, tag = image_path.split("@")
534
+ elif ":" in image_path:
527
535
  image_path, tag = image_path.split(":")
536
+
528
537
  image_name = f"{index_name}/{image_path}"
538
+
529
539
  return image_name, tag
530
540
 
531
541
 
@@ -145,17 +145,19 @@ def import_object(import_path: str) -> Any:
145
145
  - module.object
146
146
  - module:object
147
147
  - /path/to/script.py:object
148
+ - module:object.method
149
+ - /path/to/script.py:object.method
148
150
 
149
151
  This function is not thread safe as it modifies the 'sys' module during execution.
150
152
  """
151
153
  if ".py:" in import_path:
152
- script_path, object_name = import_path.rsplit(":", 1)
154
+ script_path, object_path = import_path.rsplit(":", 1)
153
155
  module = load_script_as_module(script_path)
154
156
  else:
155
157
  if ":" in import_path:
156
- module_name, object_name = import_path.rsplit(":", 1)
158
+ module_name, object_path = import_path.rsplit(":", 1)
157
159
  elif "." in import_path:
158
- module_name, object_name = import_path.rsplit(".", 1)
160
+ module_name, object_path = import_path.rsplit(".", 1)
159
161
  else:
160
162
  raise ValueError(
161
163
  f"Invalid format for object import. Received {import_path!r}."
@@ -163,7 +165,13 @@ def import_object(import_path: str) -> Any:
163
165
 
164
166
  module = load_module(module_name)
165
167
 
166
- return getattr(module, object_name)
168
+ # Handle nested object/method access
169
+ parts = object_path.split(".")
170
+ obj = module
171
+ for part in parts:
172
+ obj = getattr(obj, part)
173
+
174
+ return obj
167
175
 
168
176
 
169
177
  class DelayedImportErrorModule(ModuleType):
prefect/workers/base.py CHANGED
@@ -697,6 +697,25 @@ class BaseWorker(abc.ABC, Generic[C, V, R]):
697
697
  "Workers must implement a method for running submitted flow runs"
698
698
  )
699
699
 
700
+ async def _initiate_run(
701
+ self,
702
+ flow_run: "FlowRun",
703
+ configuration: C,
704
+ ) -> None:
705
+ """
706
+ This method is called by the worker to initiate a flow run and should return as
707
+ soon as possible.
708
+
709
+ This method is used in `.submit` to allow non-blocking submission of flows. For
710
+ workers that wait for completion in their `run` method, this method should be
711
+ implemented to return immediately.
712
+
713
+ If this method is not implemented, `.submit` will fall back to the `.run` method.
714
+ """
715
+ raise NotImplementedError(
716
+ "This worker has not implemented `_initiate_run`. Please use `run` instead."
717
+ )
718
+
700
719
  async def submit(
701
720
  self,
702
721
  flow: "Flow[..., FR]",
@@ -866,16 +885,19 @@ class BaseWorker(abc.ABC, Generic[C, V, R]):
866
885
  try:
867
886
  # Call the implementation-specific run method with the constructed configuration. This is where the
868
887
  # rubber meets the road.
869
- result = await self.run(flow_run, configuration)
870
-
871
- if result.status_code != 0:
872
- await self._propose_crashed_state(
873
- flow_run,
874
- (
875
- "Flow run infrastructure exited with non-zero status code"
876
- f" {result.status_code}."
877
- ),
878
- )
888
+ try:
889
+ await self._initiate_run(flow_run, configuration)
890
+ except NotImplementedError:
891
+ result = await self.run(flow_run, configuration)
892
+
893
+ if result.status_code != 0:
894
+ await self._propose_crashed_state(
895
+ flow_run,
896
+ (
897
+ "Flow run infrastructure exited with non-zero status code"
898
+ f" {result.status_code}."
899
+ ),
900
+ )
879
901
  except Exception as exc:
880
902
  # This flow run was being submitted and did not start successfully
881
903
  logger.exception(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prefect-client
3
- Version: 3.4.0
3
+ Version: 3.4.1
4
4
  Summary: Workflow orchestration and management.
5
5
  Project-URL: Changelog, https://github.com/PrefectHQ/prefect/releases
6
6
  Project-URL: Documentation, https://docs.prefect.io
@@ -60,6 +60,7 @@ Requires-Dist: sniffio<2.0.0,>=1.3.0
60
60
  Requires-Dist: toml>=0.10.0
61
61
  Requires-Dist: typing-extensions<5.0.0,>=4.10.0
62
62
  Requires-Dist: ujson<6.0.0,>=5.8.0
63
+ Requires-Dist: uuid7>=0.1.0
63
64
  Requires-Dist: uvicorn!=0.29.0,>=0.14.0
64
65
  Requires-Dist: websockets<16.0,>=13.0
65
66
  Requires-Dist: whenever<0.9.0,>=0.7.3; python_version >= '3.13'