prefect-client 3.0.0rc20__py3-none-any.whl → 3.0.2__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 (57) hide show
  1. prefect/_internal/compatibility/deprecated.py +1 -1
  2. prefect/_internal/compatibility/migration.py +1 -1
  3. prefect/artifacts.py +1 -1
  4. prefect/blocks/core.py +3 -4
  5. prefect/blocks/notifications.py +31 -10
  6. prefect/blocks/system.py +4 -4
  7. prefect/blocks/webhook.py +11 -1
  8. prefect/client/cloud.py +2 -1
  9. prefect/client/orchestration.py +93 -21
  10. prefect/client/schemas/actions.py +2 -2
  11. prefect/client/schemas/objects.py +24 -6
  12. prefect/client/types/flexible_schedule_list.py +1 -1
  13. prefect/concurrency/asyncio.py +45 -6
  14. prefect/concurrency/services.py +1 -1
  15. prefect/concurrency/sync.py +21 -27
  16. prefect/concurrency/v1/asyncio.py +3 -0
  17. prefect/concurrency/v1/sync.py +4 -5
  18. prefect/context.py +11 -9
  19. prefect/deployments/runner.py +4 -3
  20. prefect/events/actions.py +6 -0
  21. prefect/exceptions.py +6 -0
  22. prefect/filesystems.py +5 -3
  23. prefect/flow_engine.py +22 -11
  24. prefect/flows.py +0 -2
  25. prefect/futures.py +2 -1
  26. prefect/locking/__init__.py +0 -0
  27. prefect/locking/filesystem.py +243 -0
  28. prefect/locking/memory.py +213 -0
  29. prefect/locking/protocol.py +122 -0
  30. prefect/logging/handlers.py +0 -2
  31. prefect/logging/loggers.py +0 -18
  32. prefect/logging/logging.yml +1 -0
  33. prefect/main.py +19 -5
  34. prefect/records/base.py +12 -0
  35. prefect/records/filesystem.py +10 -4
  36. prefect/records/memory.py +6 -0
  37. prefect/records/result_store.py +18 -6
  38. prefect/results.py +702 -205
  39. prefect/runner/runner.py +74 -5
  40. prefect/settings.py +11 -4
  41. prefect/states.py +40 -23
  42. prefect/task_engine.py +39 -37
  43. prefect/task_worker.py +6 -4
  44. prefect/tasks.py +24 -6
  45. prefect/transactions.py +116 -54
  46. prefect/utilities/callables.py +1 -3
  47. prefect/utilities/engine.py +16 -8
  48. prefect/utilities/importtools.py +1 -0
  49. prefect/utilities/urls.py +70 -12
  50. prefect/variables.py +34 -24
  51. prefect/workers/base.py +14 -6
  52. prefect/workers/process.py +1 -3
  53. {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.2.dist-info}/METADATA +2 -2
  54. {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.2.dist-info}/RECORD +57 -53
  55. {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.2.dist-info}/LICENSE +0 -0
  56. {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.2.dist-info}/WHEEL +0 -0
  57. {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.2.dist-info}/top_level.txt +0 -0
prefect/transactions.py CHANGED
@@ -17,17 +17,19 @@ from typing import (
17
17
  from pydantic import Field, PrivateAttr
18
18
  from typing_extensions import Self
19
19
 
20
- from prefect.context import ContextModel, FlowRunContext, TaskRunContext
20
+ from prefect.context import ContextModel
21
21
  from prefect.exceptions import MissingContextError, SerializationError
22
22
  from prefect.logging.loggers import get_logger, get_run_logger
23
23
  from prefect.records import RecordStore
24
+ from prefect.records.base import TransactionRecord
24
25
  from prefect.results import (
25
26
  BaseResult,
26
- ResultFactory,
27
- get_default_result_storage,
27
+ ResultRecord,
28
+ ResultStore,
29
+ get_result_store,
30
+ should_persist_result,
28
31
  )
29
32
  from prefect.utilities.annotations import NotSet
30
- from prefect.utilities.asyncutils import run_coro_as_sync
31
33
  from prefect.utilities.collections import AutoEnum
32
34
  from prefect.utilities.engine import _get_hook_name
33
35
 
@@ -56,7 +58,7 @@ class Transaction(ContextModel):
56
58
  A base model for transaction state.
57
59
  """
58
60
 
59
- store: Optional[RecordStore] = None
61
+ store: Union[RecordStore, ResultStore, None] = None
60
62
  key: Optional[str] = None
61
63
  children: List["Transaction"] = Field(default_factory=list)
62
64
  commit_mode: Optional[CommitMode] = None
@@ -70,19 +72,91 @@ class Transaction(ContextModel):
70
72
  logger: Union[logging.Logger, logging.LoggerAdapter] = Field(
71
73
  default_factory=partial(get_logger, "transactions")
72
74
  )
75
+ write_on_commit: bool = True
73
76
  _stored_values: Dict[str, Any] = PrivateAttr(default_factory=dict)
74
77
  _staged_value: Any = None
75
78
  __var__: ContextVar = ContextVar("transaction")
76
79
 
77
80
  def set(self, name: str, value: Any) -> None:
81
+ """
82
+ Set a stored value in the transaction.
83
+
84
+ Args:
85
+ name: The name of the value to set
86
+ value: The value to set
87
+
88
+ Examples:
89
+ Set a value for use later in the transaction:
90
+ ```python
91
+ with transaction() as txn:
92
+ txn.set("key", "value")
93
+ ...
94
+ assert txn.get("key") == "value"
95
+ ```
96
+ """
78
97
  self._stored_values[name] = value
79
98
 
80
99
  def get(self, name: str, default: Any = NotSet) -> Any:
81
- if name not in self._stored_values:
82
- if default is not NotSet:
83
- return default
84
- raise ValueError(f"Could not retrieve value for unknown key: {name}")
85
- return self._stored_values.get(name)
100
+ """
101
+ Get a stored value from the transaction.
102
+
103
+ Child transactions will return values from their parents unless a value with
104
+ the same name is set in the child transaction.
105
+
106
+ Direct changes to returned values will not update the stored value. To update the
107
+ stored value, use the `set` method.
108
+
109
+ Args:
110
+ name: The name of the value to get
111
+ default: The default value to return if the value is not found
112
+
113
+ Returns:
114
+ The value from the transaction
115
+
116
+ Examples:
117
+ Get a value from the transaction:
118
+ ```python
119
+ with transaction() as txn:
120
+ txn.set("key", "value")
121
+ ...
122
+ assert txn.get("key") == "value"
123
+ ```
124
+
125
+ Get a value from a parent transaction:
126
+ ```python
127
+ with transaction() as parent:
128
+ parent.set("key", "parent_value")
129
+ with transaction() as child:
130
+ assert child.get("key") == "parent_value"
131
+ ```
132
+
133
+ Update a stored value:
134
+ ```python
135
+ with transaction() as txn:
136
+ txn.set("key", [1, 2, 3])
137
+ value = txn.get("key")
138
+ value.append(4)
139
+ # Stored value is not updated until `.set` is called
140
+ assert value == [1, 2, 3, 4]
141
+ assert txn.get("key") == [1, 2, 3]
142
+
143
+ txn.set("key", value)
144
+ assert txn.get("key") == [1, 2, 3, 4]
145
+ ```
146
+ """
147
+ # deepcopy to prevent mutation of stored values
148
+ value = copy.deepcopy(self._stored_values.get(name, NotSet))
149
+ if value is NotSet:
150
+ # if there's a parent transaction, get the value from the parent
151
+ parent = self.get_parent()
152
+ if parent is not None:
153
+ value = parent.get(name, default)
154
+ # if there's no parent transaction, use the default
155
+ elif default is not NotSet:
156
+ value = default
157
+ else:
158
+ raise ValueError(f"Could not retrieve value for unknown key: {name}")
159
+ return value
86
160
 
87
161
  def is_committed(self) -> bool:
88
162
  return self.state == TransactionState.COMMITTED
@@ -105,8 +179,6 @@ class Transaction(ContextModel):
105
179
  "Context already entered. Context enter calls cannot be nested."
106
180
  )
107
181
  parent = get_transaction()
108
- if parent:
109
- self._stored_values = copy.deepcopy(parent._stored_values)
110
182
  # set default commit behavior; either inherit from parent or set a default of eager
111
183
  if self.commit_mode is None:
112
184
  self.commit_mode = parent.commit_mode if parent else CommitMode.LAZY
@@ -123,7 +195,7 @@ class Transaction(ContextModel):
123
195
  and not self.store.supports_isolation_level(self.isolation_level)
124
196
  ):
125
197
  raise ValueError(
126
- f"Isolation level {self.isolation_level.name} is not supported by record store type {self.store.__class__.__name__}"
198
+ f"Isolation level {self.isolation_level.name} is not supported by provided result store."
127
199
  )
128
200
 
129
201
  # this needs to go before begin, which could set the state to committed
@@ -177,10 +249,14 @@ class Transaction(ContextModel):
177
249
  ):
178
250
  self.state = TransactionState.COMMITTED
179
251
 
180
- def read(self) -> Optional[BaseResult]:
252
+ def read(self) -> Union["BaseResult", ResultRecord, None]:
181
253
  if self.store and self.key:
182
254
  record = self.store.read(key=self.key)
183
- if record is not None:
255
+ if isinstance(record, ResultRecord):
256
+ return record
257
+ # for backwards compatibility, if we encounter a transaction record, return the result
258
+ # This happens when the transaction is using a `ResultStore`
259
+ if isinstance(record, TransactionRecord):
184
260
  return record.result
185
261
  return None
186
262
 
@@ -229,8 +305,21 @@ class Transaction(ContextModel):
229
305
  for hook in self.on_commit_hooks:
230
306
  self.run_hook(hook, "commit")
231
307
 
232
- if self.store and self.key:
233
- self.store.write(key=self.key, result=self._staged_value)
308
+ if self.store and self.key and self.write_on_commit:
309
+ if isinstance(self.store, ResultStore):
310
+ if isinstance(self._staged_value, BaseResult):
311
+ self.store.write(
312
+ key=self.key, obj=self._staged_value.get(_sync=True)
313
+ )
314
+ elif isinstance(self._staged_value, ResultRecord):
315
+ self.store.persist_result_record(
316
+ result_record=self._staged_value
317
+ )
318
+ else:
319
+ self.store.write(key=self.key, obj=self._staged_value)
320
+ else:
321
+ self.store.write(key=self.key, result=self._staged_value)
322
+
234
323
  self.state = TransactionState.COMMITTED
235
324
  if (
236
325
  self.store
@@ -281,7 +370,7 @@ class Transaction(ContextModel):
281
370
 
282
371
  def stage(
283
372
  self,
284
- value: BaseResult,
373
+ value: Any,
285
374
  on_rollback_hooks: Optional[List] = None,
286
375
  on_commit_hooks: Optional[List] = None,
287
376
  ) -> None:
@@ -339,10 +428,11 @@ def get_transaction() -> Optional[Transaction]:
339
428
  @contextmanager
340
429
  def transaction(
341
430
  key: Optional[str] = None,
342
- store: Optional[RecordStore] = None,
431
+ store: Union[RecordStore, ResultStore, None] = None,
343
432
  commit_mode: Optional[CommitMode] = None,
344
433
  isolation_level: Optional[IsolationLevel] = None,
345
434
  overwrite: bool = False,
435
+ write_on_commit: Optional[bool] = None,
346
436
  logger: Union[logging.Logger, logging.LoggerAdapter, None] = None,
347
437
  ) -> Generator[Transaction, None, None]:
348
438
  """
@@ -355,47 +445,16 @@ def transaction(
355
445
  - commit_mode: The commit mode controlling when the transaction and
356
446
  child transactions are committed
357
447
  - overwrite: Whether to overwrite an existing transaction record in the store
448
+ - write_on_commit: Whether to write the result to the store on commit. If not provided,
449
+ will default will be determined by the current run context. If no run context is
450
+ available, the value of `PREFECT_RESULTS_PERSIST_BY_DEFAULT` will be used.
358
451
 
359
452
  Yields:
360
453
  - Transaction: An object representing the transaction state
361
454
  """
362
455
  # if there is no key, we won't persist a record
363
456
  if key and not store:
364
- flow_run_context = FlowRunContext.get()
365
- task_run_context = TaskRunContext.get()
366
- existing_factory = getattr(task_run_context, "result_factory", None) or getattr(
367
- flow_run_context, "result_factory", None
368
- )
369
-
370
- new_factory: ResultFactory
371
- if existing_factory and existing_factory.storage_block_id:
372
- new_factory = existing_factory.model_copy(
373
- update={
374
- "persist_result": True,
375
- }
376
- )
377
- else:
378
- default_storage = get_default_result_storage(_sync=True)
379
- if existing_factory:
380
- new_factory = existing_factory.model_copy(
381
- update={
382
- "persist_result": True,
383
- "storage_block": default_storage,
384
- "storage_block_id": default_storage._block_document_id,
385
- }
386
- )
387
- else:
388
- new_factory = run_coro_as_sync(
389
- ResultFactory.default_factory(
390
- persist_result=True,
391
- result_storage=default_storage,
392
- )
393
- )
394
- from prefect.records.result_store import ResultFactoryStore
395
-
396
- store = ResultFactoryStore(
397
- result_factory=new_factory,
398
- )
457
+ store = get_result_store()
399
458
 
400
459
  try:
401
460
  logger = logger or get_run_logger()
@@ -408,6 +467,9 @@ def transaction(
408
467
  commit_mode=commit_mode,
409
468
  isolation_level=isolation_level,
410
469
  overwrite=overwrite,
470
+ write_on_commit=write_on_commit
471
+ if write_on_commit is not None
472
+ else should_persist_result(),
411
473
  logger=logger,
412
474
  ) as txn:
413
475
  yield txn
@@ -263,9 +263,7 @@ def parameter_docstrings(docstring: Optional[str]) -> Dict[str, str]:
263
263
  if not docstring:
264
264
  return param_docstrings
265
265
 
266
- with disable_logger("griffe.docstrings.google"), disable_logger(
267
- "griffe.agents.nodes"
268
- ):
266
+ with disable_logger("griffe"):
269
267
  parsed = parse(Docstring(docstring), Parser.google)
270
268
  for section in parsed:
271
269
  if section.kind != DocstringSectionKind.parameters:
@@ -44,12 +44,11 @@ from prefect.exceptions import (
44
44
  )
45
45
  from prefect.flows import Flow
46
46
  from prefect.futures import PrefectFuture
47
- from prefect.futures import PrefectFuture as NewPrefectFuture
48
47
  from prefect.logging.loggers import (
49
48
  get_logger,
50
49
  task_run_logger,
51
50
  )
52
- from prefect.results import BaseResult
51
+ from prefect.results import BaseResult, ResultRecord, should_persist_result
53
52
  from prefect.settings import (
54
53
  PREFECT_LOGGING_LOG_PRINTS,
55
54
  )
@@ -122,7 +121,7 @@ async def collect_task_run_inputs(expr: Any, max_depth: int = -1) -> Set[TaskRun
122
121
 
123
122
 
124
123
  def collect_task_run_inputs_sync(
125
- expr: Any, future_cls: Any = NewPrefectFuture, max_depth: int = -1
124
+ expr: Any, future_cls: Any = PrefectFuture, max_depth: int = -1
126
125
  ) -> Set[TaskRunInput]:
127
126
  """
128
127
  This function recurses through an expression to generate a set of any discernible
@@ -131,7 +130,7 @@ def collect_task_run_inputs_sync(
131
130
 
132
131
  Examples:
133
132
  >>> task_inputs = {
134
- >>> k: collect_task_run_inputs(v) for k, v in parameters.items()
133
+ >>> k: collect_task_run_inputs_sync(v) for k, v in parameters.items()
135
134
  >>> }
136
135
  """
137
136
  # TODO: This function needs to be updated to detect parameters and constants
@@ -401,6 +400,8 @@ async def propose_state(
401
400
  # Avoid fetching the result unless it is cached, otherwise we defeat
402
401
  # the purpose of disabling `cache_result_in_memory`
403
402
  result = await state.result(raise_on_failure=False, fetch=True)
403
+ elif isinstance(state.data, ResultRecord):
404
+ result = state.data.result
404
405
  else:
405
406
  result = state.data
406
407
 
@@ -504,6 +505,8 @@ def propose_state_sync(
504
505
  result = state.result(raise_on_failure=False, fetch=True)
505
506
  if inspect.isawaitable(result):
506
507
  result = run_coro_as_sync(result)
508
+ elif isinstance(state.data, ResultRecord):
509
+ result = state.data.result
507
510
  else:
508
511
  result = state.data
509
512
 
@@ -732,6 +735,13 @@ def emit_task_run_state_change_event(
732
735
  ) -> Event:
733
736
  state_message_truncation_length = 100_000
734
737
 
738
+ if isinstance(validated_state.data, ResultRecord) and should_persist_result():
739
+ data = validated_state.data.metadata.model_dump(mode="json")
740
+ elif isinstance(validated_state.data, BaseResult):
741
+ data = validated_state.data.model_dump(mode="json")
742
+ else:
743
+ data = None
744
+
735
745
  return emit_event(
736
746
  id=validated_state.id,
737
747
  occurred=validated_state.timestamp,
@@ -770,9 +780,7 @@ def emit_task_run_state_change_event(
770
780
  exclude_unset=True,
771
781
  exclude={"flow_run_id", "task_run_id"},
772
782
  ),
773
- "data": validated_state.data.model_dump(mode="json")
774
- if isinstance(validated_state.data, BaseResult)
775
- else None,
783
+ "data": data,
776
784
  },
777
785
  "task_run": task_run.model_dump(
778
786
  mode="json",
@@ -822,7 +830,7 @@ def resolve_to_final_result(expr, context):
822
830
  if isinstance(context.get("annotation"), quote):
823
831
  raise StopVisiting()
824
832
 
825
- if isinstance(expr, NewPrefectFuture):
833
+ if isinstance(expr, PrefectFuture):
826
834
  upstream_task_run = context.get("current_task_run")
827
835
  upstream_task = context.get("current_task")
828
836
  if (
@@ -398,6 +398,7 @@ def safe_load_namespace(
398
398
  # Save original sys.path and modify it
399
399
  original_sys_path = sys.path.copy()
400
400
  sys.path.insert(0, parent_dir)
401
+ sys.path.insert(0, file_dir)
401
402
 
402
403
  # Create a temporary module for import context
403
404
  temp_module = ModuleType(package_name)
prefect/utilities/urls.py CHANGED
@@ -1,17 +1,22 @@
1
1
  import inspect
2
+ import ipaddress
3
+ import socket
2
4
  import urllib.parse
3
- from typing import Any, Literal, Optional, Union
5
+ from typing import TYPE_CHECKING, Any, Literal, Optional, Union
6
+ from urllib.parse import urlparse
4
7
  from uuid import UUID
5
8
 
6
9
  from pydantic import BaseModel
7
10
 
8
11
  from prefect import settings
9
- from prefect.blocks.core import Block
10
- from prefect.events.schemas.automations import Automation
11
- from prefect.events.schemas.events import ReceivedEvent, Resource
12
- from prefect.futures import PrefectFuture
13
12
  from prefect.logging.loggers import get_logger
14
- from prefect.variables import Variable
13
+
14
+ if TYPE_CHECKING:
15
+ from prefect.blocks.core import Block
16
+ from prefect.events.schemas.automations import Automation
17
+ from prefect.events.schemas.events import ReceivedEvent, Resource
18
+ from prefect.futures import PrefectFuture
19
+ from prefect.variables import Variable
15
20
 
16
21
  logger = get_logger("utilities.urls")
17
22
 
@@ -58,6 +63,54 @@ URLType = Literal["ui", "api"]
58
63
  RUN_TYPES = {"flow-run", "task-run"}
59
64
 
60
65
 
66
+ def validate_restricted_url(url: str):
67
+ """
68
+ Validate that the provided URL is safe for outbound requests. This prevents
69
+ attacks like SSRF (Server Side Request Forgery), where an attacker can make
70
+ requests to internal services (like the GCP metadata service, localhost addresses,
71
+ or in-cluster Kubernetes services)
72
+
73
+ Args:
74
+ url: The URL to validate.
75
+
76
+ Raises:
77
+ ValueError: If the URL is a restricted URL.
78
+ """
79
+
80
+ try:
81
+ parsed_url = urlparse(url)
82
+ except ValueError:
83
+ raise ValueError(f"{url!r} is not a valid URL.")
84
+
85
+ if parsed_url.scheme not in ("http", "https"):
86
+ raise ValueError(
87
+ f"{url!r} is not a valid URL. Only HTTP and HTTPS URLs are allowed."
88
+ )
89
+
90
+ hostname = parsed_url.hostname or ""
91
+
92
+ # Remove IPv6 brackets if present
93
+ if hostname.startswith("[") and hostname.endswith("]"):
94
+ hostname = hostname[1:-1]
95
+
96
+ if not hostname:
97
+ raise ValueError(f"{url!r} is not a valid URL.")
98
+
99
+ try:
100
+ ip_address = socket.gethostbyname(hostname)
101
+ ip = ipaddress.ip_address(ip_address)
102
+ except socket.gaierror:
103
+ try:
104
+ ip = ipaddress.ip_address(hostname)
105
+ except ValueError:
106
+ raise ValueError(f"{url!r} is not a valid URL. It could not be resolved.")
107
+
108
+ if ip.is_private:
109
+ raise ValueError(
110
+ f"{url!r} is not a valid URL. It resolves to the private address {ip}."
111
+ )
112
+
113
+
61
114
  def convert_class_to_name(obj: Any) -> str:
62
115
  """
63
116
  Convert CamelCase class name to dash-separated lowercase name
@@ -69,12 +122,12 @@ def convert_class_to_name(obj: Any) -> str:
69
122
 
70
123
  def url_for(
71
124
  obj: Union[
72
- PrefectFuture,
73
- Block,
74
- Variable,
75
- Automation,
76
- Resource,
77
- ReceivedEvent,
125
+ "PrefectFuture",
126
+ "Block",
127
+ "Variable",
128
+ "Automation",
129
+ "Resource",
130
+ "ReceivedEvent",
78
131
  BaseModel,
79
132
  str,
80
133
  ],
@@ -105,6 +158,11 @@ def url_for(
105
158
  url_for(obj=my_flow_run)
106
159
  url_for("flow-run", obj_id="123e4567-e89b-12d3-a456-426614174000")
107
160
  """
161
+ from prefect.blocks.core import Block
162
+ from prefect.events.schemas.automations import Automation
163
+ from prefect.events.schemas.events import ReceivedEvent, Resource
164
+ from prefect.futures import PrefectFuture
165
+
108
166
  if isinstance(obj, PrefectFuture):
109
167
  name = "task-run"
110
168
  elif isinstance(obj, Block):
prefect/variables.py CHANGED
@@ -1,19 +1,18 @@
1
- from typing import List, Optional, Union
1
+ from typing import List, Optional
2
+
3
+ from pydantic import BaseModel, Field
2
4
 
3
5
  from prefect._internal.compatibility.migration import getattr_migration
4
- from prefect.client.schemas.actions import VariableCreate as VariableRequest
5
- from prefect.client.schemas.actions import VariableUpdate as VariableUpdateRequest
6
- from prefect.client.schemas.objects import Variable as VariableResponse
6
+ from prefect.client.schemas.actions import VariableCreate, VariableUpdate
7
7
  from prefect.client.utilities import get_or_create_client
8
8
  from prefect.exceptions import ObjectNotFound
9
- from prefect.types import StrictVariableValue
9
+ from prefect.types import MAX_VARIABLE_NAME_LENGTH, StrictVariableValue
10
10
  from prefect.utilities.asyncutils import sync_compatible
11
11
 
12
12
 
13
- class Variable(VariableRequest):
13
+ class Variable(BaseModel):
14
14
  """
15
- Variables are named, mutable string values, much like environment variables. Variables are scoped to a Prefect server instance or a single workspace in Prefect Cloud.
16
- https://docs.prefect.io/latest/concepts/variables/
15
+ Variables are named, mutable JSON values that can be shared across tasks and flows.
17
16
 
18
17
  Arguments:
19
18
  name: A string identifying the variable.
@@ -21,6 +20,19 @@ class Variable(VariableRequest):
21
20
  tags: An optional list of strings to associate with the variable.
22
21
  """
23
22
 
23
+ name: str = Field(
24
+ default=...,
25
+ description="The name of the variable",
26
+ examples=["my_variable"],
27
+ max_length=MAX_VARIABLE_NAME_LENGTH,
28
+ )
29
+ value: StrictVariableValue = Field(
30
+ default=...,
31
+ description="The value of the variable",
32
+ examples=["my-value"],
33
+ )
34
+ tags: Optional[List[str]] = Field(default=None)
35
+
24
36
  @classmethod
25
37
  @sync_compatible
26
38
  async def set(
@@ -29,22 +41,21 @@ class Variable(VariableRequest):
29
41
  value: StrictVariableValue,
30
42
  tags: Optional[List[str]] = None,
31
43
  overwrite: bool = False,
32
- as_object: bool = False,
33
- ):
44
+ ) -> "Variable":
34
45
  """
35
46
  Sets a new variable. If one exists with the same name, must pass `overwrite=True`
36
47
 
37
- Returns the newly set value. If `as_object=True`, return the full Variable object
48
+ Returns the newly set variable object.
38
49
 
39
50
  Args:
40
51
  - name: The name of the variable to set.
41
52
  - value: The value of the variable to set.
42
53
  - tags: An optional list of strings to associate with the variable.
43
54
  - overwrite: Whether to overwrite the variable if it already exists.
44
- - as_object: Whether to return the full Variable object.
45
55
 
46
56
  Example:
47
57
  Set a new variable and overwrite it if it already exists.
58
+
48
59
  ```
49
60
  from prefect.variables import Variable
50
61
 
@@ -62,14 +73,17 @@ class Variable(VariableRequest):
62
73
  raise ValueError(
63
74
  f"Variable {name!r} already exists. Use `overwrite=True` to update it."
64
75
  )
65
- await client.update_variable(variable=VariableUpdateRequest(**var_dict))
76
+ await client.update_variable(variable=VariableUpdate(**var_dict))
66
77
  variable = await client.read_variable_by_name(name)
78
+ var_dict = {
79
+ "name": variable.name,
80
+ "value": variable.value,
81
+ "tags": variable.tags or [],
82
+ }
67
83
  else:
68
- variable = await client.create_variable(
69
- variable=VariableRequest(**var_dict)
70
- )
84
+ await client.create_variable(variable=VariableCreate(**var_dict))
71
85
 
72
- return variable if as_object else variable.value
86
+ return cls(**var_dict)
73
87
 
74
88
  @classmethod
75
89
  @sync_compatible
@@ -77,19 +91,15 @@ class Variable(VariableRequest):
77
91
  cls,
78
92
  name: str,
79
93
  default: StrictVariableValue = None,
80
- as_object: bool = False,
81
- ) -> Union[StrictVariableValue, VariableResponse]:
94
+ ) -> StrictVariableValue:
82
95
  """
83
96
  Get a variable's value by name.
84
97
 
85
98
  If the variable does not exist, return the default value.
86
99
 
87
- If `as_object=True`, return the full variable object. `default` is ignored in this case.
88
-
89
100
  Args:
90
- - name: The name of the variable to get.
101
+ - name: The name of the variable value to get.
91
102
  - default: The default value to return if the variable does not exist.
92
- - as_object: Whether to return the full variable object.
93
103
 
94
104
  Example:
95
105
  Get a variable's value by name.
@@ -105,7 +115,7 @@ class Variable(VariableRequest):
105
115
  client, _ = get_or_create_client()
106
116
  variable = await client.read_variable_by_name(name)
107
117
 
108
- return variable if as_object else (variable.value if variable else default)
118
+ return variable.value if variable else default
109
119
 
110
120
  @classmethod
111
121
  @sync_compatible
prefect/workers/base.py CHANGED
@@ -146,6 +146,12 @@ class BaseJobConfiguration(BaseModel):
146
146
  )
147
147
  variables.update(values)
148
148
 
149
+ # deep merge `env`
150
+ if isinstance(job_config.get("env"), dict) and (
151
+ hardcoded_env := variables.get("env")
152
+ ):
153
+ job_config["env"] = hardcoded_env | job_config.get("env")
154
+
149
155
  populated_configuration = apply_values(template=job_config, values=variables)
150
156
  populated_configuration = await resolve_block_document_references(
151
157
  template=populated_configuration, client=client
@@ -865,17 +871,19 @@ class BaseWorker(abc.ABC):
865
871
  deployment = await self._client.read_deployment(flow_run.deployment_id)
866
872
  if deployment and deployment.concurrency_limit:
867
873
  limit_name = f"deployment:{deployment.id}"
868
- concurrency_limit = deployment.concurrency_limit
869
874
  concurrency_ctx = concurrency
875
+
876
+ # ensure that the global concurrency limit is available
877
+ # and up-to-date before attempting to acquire a slot
878
+ await self._client.upsert_global_concurrency_limit_by_name(
879
+ limit_name, deployment.concurrency_limit
880
+ )
870
881
  else:
871
- limit_name = None
872
- concurrency_limit = None
882
+ limit_name = ""
873
883
  concurrency_ctx = asyncnullcontext
874
884
 
875
885
  try:
876
- async with concurrency_ctx(
877
- limit_name, occupy=concurrency_limit, max_retries=0
878
- ):
886
+ async with concurrency_ctx(limit_name, max_retries=0, strict=True):
879
887
  configuration = await self._get_configuration(flow_run, deployment)
880
888
  submitted_event = self._emit_flow_run_submitted_event(configuration)
881
889
  result = await self.run(
@@ -144,9 +144,7 @@ class ProcessWorker(BaseWorker):
144
144
  " when first getting started."
145
145
  )
146
146
  _display_name = "Process"
147
- _documentation_url = (
148
- "https://docs.prefect.io/latest/api-ref/prefect/workers/process/"
149
- )
147
+ _documentation_url = "https://docs.prefect.io/latest/get-started/quickstart"
150
148
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/356e6766a91baf20e1d08bbe16e8b5aaef4d8643-48x48.png"
151
149
 
152
150
  async def start(
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prefect-client
3
- Version: 3.0.0rc20
3
+ Version: 3.0.2
4
4
  Summary: Workflow orchestration and management.
5
5
  Home-page: https://www.prefect.io
6
6
  Author: Prefect Technologies, Inc.
7
7
  Author-email: help@prefect.io
8
8
  License: UNKNOWN
9
- Project-URL: Changelog, https://github.com/PrefectHQ/prefect/blob/main/RELEASE-NOTES.md
9
+ Project-URL: Changelog, https://github.com/PrefectHQ/prefect/releases
10
10
  Project-URL: Documentation, https://docs.prefect.io
11
11
  Project-URL: Source, https://github.com/PrefectHQ/prefect
12
12
  Project-URL: Tracker, https://github.com/PrefectHQ/prefect/issues