prefect-client 3.0.8__py3-none-any.whl → 3.0.10__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/_version.py CHANGED
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2024-10-10T10:17:25-0500",
11
+ "date": "2024-10-15T13:31:59-0500",
12
12
  "dirty": true,
13
13
  "error": null,
14
- "full-revisionid": "0894bad49e256657cab8208205623c4c7b75ad1f",
15
- "version": "3.0.8"
14
+ "full-revisionid": "3aa2d89362c2fe8ee429f0c2cf7e623e34588029",
15
+ "version": "3.0.10"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
prefect/context.py CHANGED
@@ -453,23 +453,6 @@ class SettingsContext(ContextModel):
453
453
  def __hash__(self) -> int:
454
454
  return hash(self.settings)
455
455
 
456
- def __enter__(self):
457
- """
458
- Upon entrance, we ensure the home directory for the profile exists.
459
- """
460
- return_value = super().__enter__()
461
-
462
- try:
463
- prefect_home = self.settings.home
464
- prefect_home.mkdir(mode=0o0700, exist_ok=True)
465
- except OSError:
466
- warnings.warn(
467
- (f"Failed to create the Prefect home directory at {prefect_home}"),
468
- stacklevel=2,
469
- )
470
-
471
- return return_value
472
-
473
456
  @classmethod
474
457
  def get(cls) -> "SettingsContext":
475
458
  # Return the global context instead of `None` if no context exists
@@ -567,9 +550,9 @@ def tags(*new_tags: str) -> Generator[Set[str], None, None]:
567
550
  {"a", "b", "c", "d", "e", "f"}
568
551
  """
569
552
  current_tags = TagsContext.get().current_tags
570
- new_tags = current_tags.union(new_tags)
571
- with TagsContext(current_tags=new_tags):
572
- yield new_tags
553
+ _new_tags = current_tags.union(new_tags)
554
+ with TagsContext(current_tags=_new_tags):
555
+ yield _new_tags
573
556
 
574
557
 
575
558
  @contextmanager
@@ -659,7 +642,16 @@ def root_settings_context():
659
642
  )
660
643
  active_name = "ephemeral"
661
644
 
662
- return SettingsContext(profile=profiles[active_name], settings=Settings())
645
+ if not (settings := Settings()).home.exists():
646
+ try:
647
+ settings.home.mkdir(mode=0o0700, exist_ok=True)
648
+ except OSError:
649
+ warnings.warn(
650
+ (f"Failed to create the Prefect home directory at {settings.home}"),
651
+ stacklevel=2,
652
+ )
653
+
654
+ return SettingsContext(profile=profiles[active_name], settings=settings)
663
655
 
664
656
  # Note the above context is exited and the global settings context is used by
665
657
  # an override in the `SettingsContext.get` method.
prefect/flows.py CHANGED
@@ -1245,7 +1245,7 @@ class Flow(Generic[P, R]):
1245
1245
  @overload
1246
1246
  def __call__(
1247
1247
  self: "Flow[P, Coroutine[Any, Any, T]]", *args: P.args, **kwargs: P.kwargs
1248
- ) -> Awaitable[T]:
1248
+ ) -> Coroutine[Any, Any, T]:
1249
1249
  ...
1250
1250
 
1251
1251
  @overload
prefect/runner/runner.py CHANGED
@@ -398,10 +398,12 @@ class Runner:
398
398
  start_client_metrics_server()
399
399
 
400
400
  async with self as runner:
401
- async with self._loops_task_group as tg:
401
+ # This task group isn't included in the exit stack because we want to
402
+ # stay in this function until the runner is told to stop
403
+ async with self._loops_task_group as loops_task_group:
402
404
  for storage in self._storage_objs:
403
405
  if storage.pull_interval:
404
- tg.start_soon(
406
+ loops_task_group.start_soon(
405
407
  partial(
406
408
  critical_service_loop,
407
409
  workload=storage.pull_code,
@@ -411,8 +413,8 @@ class Runner:
411
413
  )
412
414
  )
413
415
  else:
414
- tg.start_soon(storage.pull_code)
415
- tg.start_soon(
416
+ loops_task_group.start_soon(storage.pull_code)
417
+ loops_task_group.start_soon(
416
418
  partial(
417
419
  critical_service_loop,
418
420
  workload=runner._get_and_submit_flow_runs,
@@ -421,7 +423,7 @@ class Runner:
421
423
  jitter_range=0.3,
422
424
  )
423
425
  )
424
- tg.start_soon(
426
+ loops_task_group.start_soon(
425
427
  partial(
426
428
  critical_service_loop,
427
429
  workload=runner._check_for_cancelled_flow_runs,
@@ -1264,15 +1266,15 @@ class Runner:
1264
1266
  if not hasattr(self, "_loop") or not self._loop:
1265
1267
  self._loop = asyncio.get_event_loop()
1266
1268
 
1269
+ await self._client.__aenter__()
1270
+
1267
1271
  if not hasattr(self, "_runs_task_group") or not self._runs_task_group:
1268
1272
  self._runs_task_group: anyio.abc.TaskGroup = anyio.create_task_group()
1273
+ await self._runs_task_group.__aenter__()
1269
1274
 
1270
1275
  if not hasattr(self, "_loops_task_group") or not self._loops_task_group:
1271
1276
  self._loops_task_group: anyio.abc.TaskGroup = anyio.create_task_group()
1272
1277
 
1273
- await self._client.__aenter__()
1274
- await self._runs_task_group.__aenter__()
1275
-
1276
1278
  self.started = True
1277
1279
  return self
1278
1280
 
@@ -1281,13 +1283,18 @@ class Runner:
1281
1283
  if self.pause_on_shutdown:
1282
1284
  await self._pause_schedules()
1283
1285
  self.started = False
1286
+
1284
1287
  for scope in self._scheduled_task_scopes:
1285
1288
  scope.cancel()
1289
+
1286
1290
  if self._runs_task_group:
1287
1291
  await self._runs_task_group.__aexit__(*exc_info)
1292
+
1288
1293
  if self._client:
1289
1294
  await self._client.__aexit__(*exc_info)
1295
+
1290
1296
  shutil.rmtree(str(self._tmp_dir))
1297
+ del self._runs_task_group, self._loops_task_group
1291
1298
 
1292
1299
  def __repr__(self):
1293
1300
  return f"Runner(name={self.name!r})"
prefect/settings.py CHANGED
@@ -10,6 +10,7 @@ After https://github.com/pydantic/pydantic/issues/9789 is resolved, we will be a
10
10
  for settings, at which point we will not need to use the "after" model_validator.
11
11
  """
12
12
 
13
+ import inspect
13
14
  import os
14
15
  import re
15
16
  import sys
@@ -62,7 +63,11 @@ from typing_extensions import Literal, Self
62
63
 
63
64
  from prefect.exceptions import ProfileSettingsValidationError
64
65
  from prefect.types import ClientRetryExtraCodes, LogLevel
65
- from prefect.utilities.collections import visit_collection
66
+ from prefect.utilities.collections import (
67
+ deep_merge_dicts,
68
+ set_in_dict,
69
+ visit_collection,
70
+ )
66
71
  from prefect.utilities.pydantic import handle_secret_render
67
72
 
68
73
  T = TypeVar("T")
@@ -72,10 +77,12 @@ DEFAULT_PROFILES_PATH = Path(__file__).parent.joinpath("profiles.toml")
72
77
  _SECRET_TYPES: Tuple[Type, ...] = (Secret, SecretStr)
73
78
 
74
79
 
75
- def env_var_to_attr_name(env_var: str) -> str:
80
+ def env_var_to_accessor(env_var: str) -> str:
76
81
  """
77
- Convert an environment variable name to an attribute name.
82
+ Convert an environment variable name to a settings accessor.
78
83
  """
84
+ if SETTING_VARIABLES.get(env_var) is not None:
85
+ return SETTING_VARIABLES[env_var].accessor
79
86
  return env_var.replace("PREFECT_", "").lower()
80
87
 
81
88
 
@@ -87,19 +94,21 @@ def is_test_mode() -> bool:
87
94
  class Setting:
88
95
  """Mimics the old Setting object for compatibility with existing code."""
89
96
 
90
- def __init__(self, name: str, default: Any, type_: Any):
97
+ def __init__(
98
+ self, name: str, default: Any, type_: Any, accessor: Optional[str] = None
99
+ ):
91
100
  self._name = name
92
101
  self._default = default
93
102
  self._type = type_
103
+ if accessor is None:
104
+ self.accessor = env_var_to_accessor(name)
105
+ else:
106
+ self.accessor = accessor
94
107
 
95
108
  @property
96
109
  def name(self):
97
110
  return self._name
98
111
 
99
- @property
100
- def field_name(self):
101
- return env_var_to_attr_name(self.name)
102
-
103
112
  @property
104
113
  def is_secret(self):
105
114
  if self._type in _SECRET_TYPES:
@@ -119,13 +128,19 @@ class Setting:
119
128
  else:
120
129
  return None
121
130
 
122
- current_value = getattr(get_current_settings(), self.field_name)
131
+ path = self.accessor.split(".")
132
+ current_value = get_current_settings()
133
+ for key in path:
134
+ current_value = getattr(current_value, key, None)
123
135
  if isinstance(current_value, _SECRET_TYPES):
124
136
  return current_value.get_secret_value()
125
137
  return current_value
126
138
 
127
139
  def value_from(self: Self, settings: "Settings") -> Any:
128
- current_value = getattr(settings, self.field_name)
140
+ path = self.accessor.split(".")
141
+ current_value = settings
142
+ for key in path:
143
+ current_value = getattr(current_value, key, None)
129
144
  if isinstance(current_value, _SECRET_TYPES):
130
145
  return current_value.get_secret_value()
131
146
  return current_value
@@ -157,7 +172,7 @@ def default_ui_url(settings: "Settings") -> Optional[str]:
157
172
  return value
158
173
 
159
174
  # Otherwise, infer a value from the API URL
160
- ui_url = api_url = settings.api_url
175
+ ui_url = api_url = settings.api.url
161
176
 
162
177
  if not api_url:
163
178
  return None
@@ -243,7 +258,7 @@ def warn_on_misconfigured_api_url(values):
243
258
  """
244
259
  Validator for settings warning if the API URL is misconfigured.
245
260
  """
246
- api_url = values["api_url"]
261
+ api_url = values.get("api", {}).get("url")
247
262
  if api_url is not None:
248
263
  misconfigured_mappings = {
249
264
  "app.prefect.cloud": (
@@ -381,14 +396,15 @@ class ProfileSettingsTomlLoader(PydanticBaseSettingsSource):
381
396
 
382
397
  if not active_profile or active_profile not in profiles_data:
383
398
  return {}
384
-
385
399
  return profiles_data[active_profile]
386
400
 
387
401
  def get_field_value(
388
402
  self, field: FieldInfo, field_name: str
389
403
  ) -> Tuple[Any, str, bool]:
390
404
  """Concrete implementation to get the field value from the profile settings"""
391
- value = self.profile_settings.get(f"PREFECT_{field_name.upper()}")
405
+ value = self.profile_settings.get(
406
+ f"{self.config.get('env_prefix','')}{field_name.upper()}"
407
+ )
392
408
  return value, field_name, self.field_is_complex(field)
393
409
 
394
410
  def __call__(self) -> Dict[str, Any]:
@@ -408,21 +424,7 @@ class ProfileSettingsTomlLoader(PydanticBaseSettingsSource):
408
424
 
409
425
  ###########################################################################
410
426
  # Settings
411
-
412
-
413
- class Settings(BaseSettings):
414
- """
415
- Settings for Prefect using Pydantic settings.
416
-
417
- See https://docs.pydantic.dev/latest/concepts/pydantic_settings
418
- """
419
-
420
- model_config = SettingsConfigDict(
421
- env_file=".env",
422
- env_prefix="PREFECT_",
423
- extra="ignore",
424
- )
425
-
427
+ class PrefectBaseSettings(BaseSettings):
426
428
  @classmethod
427
429
  def settings_customise_sources(
428
430
  cls,
@@ -447,6 +449,129 @@ class Settings(BaseSettings):
447
449
  ProfileSettingsTomlLoader(settings_cls),
448
450
  )
449
451
 
452
+ @classmethod
453
+ def valid_setting_names(cls) -> Set[str]:
454
+ """
455
+ A set of valid setting names, e.g. "PREFECT_API_URL" or "PREFECT_API_KEY".
456
+ """
457
+ settings_fields = set()
458
+ for field_name, field in cls.model_fields.items():
459
+ if inspect.isclass(field.annotation) and issubclass(
460
+ field.annotation, PrefectBaseSettings
461
+ ):
462
+ settings_fields.update(field.annotation.valid_setting_names())
463
+ else:
464
+ settings_fields.add(
465
+ f"{cls.model_config.get('env_prefix')}{field_name.upper()}"
466
+ )
467
+ return settings_fields
468
+
469
+ def to_environment_variables(
470
+ self,
471
+ exclude_unset: bool = False,
472
+ include_secrets: bool = True,
473
+ ) -> Dict[str, str]:
474
+ """Convert the settings object to a dictionary of environment variables."""
475
+
476
+ env: Dict[str, Any] = self.model_dump(
477
+ exclude_unset=exclude_unset,
478
+ mode="json",
479
+ context={"include_secrets": include_secrets},
480
+ )
481
+ env_variables = {}
482
+ for key in self.model_fields.keys():
483
+ if isinstance(child_settings := getattr(self, key), PrefectBaseSettings):
484
+ child_env = child_settings.to_environment_variables(
485
+ exclude_unset=exclude_unset,
486
+ include_secrets=include_secrets,
487
+ )
488
+ env_variables.update(child_env)
489
+ elif (value := env.get(key)) is not None:
490
+ env_variables[
491
+ f"{self.model_config.get('env_prefix')}{key.upper()}"
492
+ ] = str(value)
493
+ return env_variables
494
+
495
+ @model_serializer(
496
+ mode="wrap", when_used="always"
497
+ ) # TODO: reconsider `when_used` default for more control
498
+ def ser_model(
499
+ self, handler: SerializerFunctionWrapHandler, info: SerializationInfo
500
+ ) -> Any:
501
+ ctx = info.context
502
+ jsonable_self = handler(self)
503
+ if ctx and ctx.get("include_secrets") is True:
504
+ dump_kwargs = dict(
505
+ include=info.include,
506
+ exclude=info.exclude,
507
+ exclude_unset=info.exclude_unset,
508
+ )
509
+ jsonable_self.update(
510
+ {
511
+ field_name: visit_collection(
512
+ expr=getattr(self, field_name),
513
+ visit_fn=partial(handle_secret_render, context=ctx),
514
+ return_data=True,
515
+ )
516
+ for field_name in set(self.model_dump(**dump_kwargs).keys()) # type: ignore
517
+ }
518
+ )
519
+
520
+ return jsonable_self
521
+
522
+
523
+ class APISettings(PrefectBaseSettings):
524
+ """
525
+ Settings for interacting with the Prefect API
526
+ """
527
+
528
+ model_config = SettingsConfigDict(
529
+ env_prefix="PREFECT_API_", env_file=".env", extra="ignore"
530
+ )
531
+ url: Optional[str] = Field(
532
+ default=None,
533
+ description="The URL of the Prefect API. If not set, the client will attempt to infer it.",
534
+ )
535
+ key: Optional[SecretStr] = Field(
536
+ default=None,
537
+ description="The API key used for authentication with the Prefect API. Should be kept secret.",
538
+ )
539
+ tls_insecure_skip_verify: bool = Field(
540
+ default=False,
541
+ description="If `True`, disables SSL checking to allow insecure requests. This is recommended only during development, e.g. when using self-signed certificates.",
542
+ )
543
+ ssl_cert_file: Optional[str] = Field(
544
+ default=os.environ.get("SSL_CERT_FILE"),
545
+ description="This configuration settings option specifies the path to an SSL certificate file.",
546
+ )
547
+ enable_http2: bool = Field(
548
+ default=False,
549
+ description="If true, enable support for HTTP/2 for communicating with an API. If the API does not support HTTP/2, this will have no effect and connections will be made via HTTP/1.1.",
550
+ )
551
+ request_timeout: float = Field(
552
+ default=60.0,
553
+ description="The default timeout for requests to the API",
554
+ )
555
+ default_limit: int = Field(
556
+ default=200,
557
+ description="The default limit applied to queries that can return multiple objects, such as `POST /flow_runs/filter`.",
558
+ )
559
+
560
+
561
+ class Settings(PrefectBaseSettings):
562
+ """
563
+ Settings for Prefect using Pydantic settings.
564
+
565
+ See https://docs.pydantic.dev/latest/concepts/pydantic_settings
566
+ """
567
+
568
+ model_config = SettingsConfigDict(
569
+ env_file=".env",
570
+ env_prefix="PREFECT_",
571
+ env_nested_delimiter=None,
572
+ extra="ignore",
573
+ )
574
+
450
575
  ###########################################################################
451
576
  # CLI
452
577
 
@@ -504,33 +629,9 @@ class Settings(BaseSettings):
504
629
  ###########################################################################
505
630
  # API settings
506
631
 
507
- api_url: Optional[str] = Field(
508
- default=None,
509
- description="The URL of the Prefect API. If not set, the client will attempt to infer it.",
510
- )
511
- api_key: Optional[SecretStr] = Field(
512
- default=None,
513
- description="The API key used for authentication with the Prefect API. Should be kept secret.",
514
- )
515
-
516
- api_tls_insecure_skip_verify: bool = Field(
517
- default=False,
518
- description="If `True`, disables SSL checking to allow insecure requests. This is recommended only during development, e.g. when using self-signed certificates.",
519
- )
520
-
521
- api_ssl_cert_file: Optional[str] = Field(
522
- default=os.environ.get("SSL_CERT_FILE"),
523
- description="This configuration settings option specifies the path to an SSL certificate file.",
524
- )
525
-
526
- api_enable_http2: bool = Field(
527
- default=False,
528
- description="If true, enable support for HTTP/2 for communicating with an API. If the API does not support HTTP/2, this will have no effect and connections will be made via HTTP/1.1.",
529
- )
530
-
531
- api_request_timeout: float = Field(
532
- default=60.0,
533
- description="The default timeout for requests to the API",
632
+ api: APISettings = Field(
633
+ default_factory=APISettings,
634
+ description="Settings for interacting with the Prefect API",
534
635
  )
535
636
 
536
637
  api_blocks_register_on_start: bool = Field(
@@ -1429,7 +1530,7 @@ class Settings(BaseSettings):
1429
1530
 
1430
1531
  def __getattribute__(self, name: str) -> Any:
1431
1532
  if name.startswith("PREFECT_"):
1432
- field_name = env_var_to_attr_name(name)
1533
+ field_name = env_var_to_accessor(name)
1433
1534
  warnings.warn(
1434
1535
  f"Accessing `Settings().{name}` is deprecated. Use `Settings().{field_name}` instead.",
1435
1536
  DeprecationWarning,
@@ -1456,8 +1557,10 @@ class Settings(BaseSettings):
1456
1557
  self.ui_url = default_ui_url(self)
1457
1558
  self.__pydantic_fields_set__.remove("ui_url")
1458
1559
  if self.ui_api_url is None:
1459
- if self.api_url:
1460
- self.ui_api_url = self.api_url
1560
+ if self.api.url:
1561
+ self.ui_api_url = self.api.url
1562
+ if self.api.url:
1563
+ self.ui_api_url = self.api.url
1461
1564
  self.__pydantic_fields_set__.remove("ui_api_url")
1462
1565
  else:
1463
1566
  self.ui_api_url = (
@@ -1509,7 +1612,7 @@ class Settings(BaseSettings):
1509
1612
  return self
1510
1613
 
1511
1614
  @model_validator(mode="after")
1512
- def emit_warnings(self):
1615
+ def emit_warnings(self) -> Self:
1513
1616
  """More post-hoc validation of settings, including warnings for misconfigurations."""
1514
1617
  values = self.model_dump()
1515
1618
  values = max_log_size_smaller_than_batch_size(values)
@@ -1521,16 +1624,6 @@ class Settings(BaseSettings):
1521
1624
  ##########################################################################
1522
1625
  # Settings methods
1523
1626
 
1524
- @classmethod
1525
- def valid_setting_names(cls) -> Set[str]:
1526
- """
1527
- A set of valid setting names, e.g. "PREFECT_API_URL" or "PREFECT_API_KEY".
1528
- """
1529
- return set(
1530
- f"{cls.model_config.get('env_prefix')}{key.upper()}"
1531
- for key in cls.model_fields.keys()
1532
- )
1533
-
1534
1627
  def copy_with_update(
1535
1628
  self: Self,
1536
1629
  updates: Optional[Mapping[Setting, Any]] = None,
@@ -1550,14 +1643,26 @@ class Settings(BaseSettings):
1550
1643
  Returns:
1551
1644
  A new Settings object.
1552
1645
  """
1553
- restore_defaults_names = set(r.field_name for r in restore_defaults or [])
1646
+ restore_defaults_obj = {}
1647
+ for r in restore_defaults or []:
1648
+ set_in_dict(restore_defaults_obj, r.accessor, True)
1554
1649
  updates = updates or {}
1555
1650
  set_defaults = set_defaults or {}
1556
1651
 
1652
+ set_defaults_obj = {}
1653
+ for setting, value in set_defaults.items():
1654
+ set_in_dict(set_defaults_obj, setting.accessor, value)
1655
+
1656
+ updates_obj = {}
1657
+ for setting, value in updates.items():
1658
+ set_in_dict(updates_obj, setting.accessor, value)
1659
+
1557
1660
  new_settings = self.__class__(
1558
- **{setting.field_name: value for setting, value in set_defaults.items()}
1559
- | self.model_dump(exclude_unset=True, exclude=restore_defaults_names)
1560
- | {setting.field_name: value for setting, value in updates.items()}
1661
+ **deep_merge_dicts(
1662
+ set_defaults_obj,
1663
+ self.model_dump(exclude_unset=True, exclude=restore_defaults_obj),
1664
+ updates_obj,
1665
+ )
1561
1666
  )
1562
1667
  return new_settings
1563
1668
 
@@ -1569,59 +1674,6 @@ class Settings(BaseSettings):
1569
1674
  env_variables = self.to_environment_variables()
1570
1675
  return str(hash(tuple((key, value) for key, value in env_variables.items())))
1571
1676
 
1572
- def to_environment_variables(
1573
- self,
1574
- include: Optional[Iterable[Setting]] = None,
1575
- exclude: Optional[Iterable[Setting]] = None,
1576
- exclude_unset: bool = False,
1577
- include_secrets: bool = True,
1578
- ) -> Dict[str, str]:
1579
- """Convert the settings object to a dictionary of environment variables."""
1580
- included_names = {s.field_name for s in include} if include else None
1581
- excluded_names = {s.field_name for s in exclude} if exclude else None
1582
-
1583
- if exclude_unset:
1584
- if included_names is None:
1585
- included_names = set(self.model_dump(exclude_unset=True).keys())
1586
- else:
1587
- included_names.intersection_update(
1588
- {key for key in self.model_dump(exclude_unset=True)}
1589
- )
1590
-
1591
- env: Dict[str, Any] = self.model_dump(
1592
- include=included_names,
1593
- exclude=excluded_names,
1594
- mode="json",
1595
- context={"include_secrets": include_secrets},
1596
- )
1597
- return {
1598
- f"{self.model_config.get('env_prefix')}{key.upper()}": str(value)
1599
- for key, value in env.items()
1600
- if value is not None
1601
- }
1602
-
1603
- @model_serializer(
1604
- mode="wrap", when_used="always"
1605
- ) # TODO: reconsider `when_used` default for more control
1606
- def ser_model(
1607
- self, handler: SerializerFunctionWrapHandler, info: SerializationInfo
1608
- ) -> Any:
1609
- ctx = info.context
1610
- jsonable_self = handler(self)
1611
- if ctx and ctx.get("include_secrets") is True:
1612
- dump_kwargs = dict(include=info.include, exclude=info.exclude)
1613
- jsonable_self.update(
1614
- {
1615
- field_name: visit_collection(
1616
- expr=getattr(self, field_name),
1617
- visit_fn=partial(handle_secret_render, context=ctx),
1618
- return_data=True,
1619
- )
1620
- for field_name in set(self.model_dump(**dump_kwargs).keys()) # type: ignore
1621
- }
1622
- )
1623
- return jsonable_self
1624
-
1625
1677
 
1626
1678
  ############################################################################
1627
1679
  # Settings utils
@@ -1639,12 +1691,7 @@ def _cast_settings(
1639
1691
  for k, value in settings.items():
1640
1692
  try:
1641
1693
  if isinstance(k, str):
1642
- field = Settings.model_fields[env_var_to_attr_name(k)]
1643
- setting = Setting(
1644
- name=k,
1645
- default=field.default,
1646
- type_=field.annotation,
1647
- )
1694
+ setting = SETTING_VARIABLES[k]
1648
1695
  else:
1649
1696
  setting = k
1650
1697
  casted_settings[setting] = value
@@ -1739,9 +1786,16 @@ class Profile(BaseModel):
1739
1786
  errors: List[Tuple[Setting, ValidationError]] = []
1740
1787
  for setting, value in self.settings.items():
1741
1788
  try:
1742
- TypeAdapter(
1743
- Settings.model_fields[setting.field_name].annotation
1744
- ).validate_python(value)
1789
+ model_fields = Settings.model_fields
1790
+ annotation = None
1791
+ for section in setting.accessor.split("."):
1792
+ annotation = model_fields[section].annotation
1793
+ if inspect.isclass(annotation) and issubclass(
1794
+ annotation, BaseSettings
1795
+ ):
1796
+ model_fields = annotation.model_fields
1797
+
1798
+ TypeAdapter(annotation).validate_python(value)
1745
1799
  except ValidationError as e:
1746
1800
  errors.append((setting, e))
1747
1801
  if errors:
@@ -2058,26 +2112,38 @@ def update_current_profile(
2058
2112
  # Allow traditional env var access
2059
2113
 
2060
2114
 
2061
- class _SettingsDict(dict):
2062
- """allow either `field_name` or `ENV_VAR_NAME` access
2063
- ```
2064
- d = _SettingsDict(Settings)
2065
- d["api_url"] == d["PREFECT_API_URL"]
2066
- ```
2067
- """
2068
-
2069
- def __init__(self: Self, settings_cls: Type[BaseSettings]):
2070
- super().__init__()
2071
- for field_name, field in settings_cls.model_fields.items():
2115
+ def _collect_settings_fields(
2116
+ settings_cls: Type[BaseSettings], accessor_prefix: Optional[str] = None
2117
+ ) -> Dict[str, Setting]:
2118
+ settings_fields: Dict[str, Setting] = {}
2119
+ for field_name, field in settings_cls.model_fields.items():
2120
+ if inspect.isclass(field.annotation) and issubclass(
2121
+ field.annotation, BaseSettings
2122
+ ):
2123
+ accessor = (
2124
+ field_name
2125
+ if accessor_prefix is None
2126
+ else f"{accessor_prefix}.{field_name}"
2127
+ )
2128
+ settings_fields.update(_collect_settings_fields(field.annotation, accessor))
2129
+ else:
2130
+ accessor = (
2131
+ field_name
2132
+ if accessor_prefix is None
2133
+ else f"{accessor_prefix}.{field_name}"
2134
+ )
2072
2135
  setting = Setting(
2073
2136
  name=f"{settings_cls.model_config.get('env_prefix')}{field_name.upper()}",
2074
2137
  default=field.default,
2075
2138
  type_=field.annotation,
2139
+ accessor=accessor,
2076
2140
  )
2077
- self[field_name] = self[setting.name] = setting
2141
+ settings_fields[setting.name] = setting
2142
+ settings_fields[setting.accessor] = setting
2143
+ return settings_fields
2078
2144
 
2079
2145
 
2080
- SETTING_VARIABLES: dict[str, Setting] = _SettingsDict(Settings)
2146
+ SETTING_VARIABLES: dict[str, Setting] = _collect_settings_fields(Settings)
2081
2147
 
2082
2148
 
2083
2149
  def __getattr__(name: str) -> Setting:
prefect/task_worker.py CHANGED
@@ -102,7 +102,7 @@ class TaskWorker:
102
102
  "TaskWorker must be initialized within an async context."
103
103
  )
104
104
 
105
- self._runs_task_group: anyio.abc.TaskGroup = anyio.create_task_group()
105
+ self._runs_task_group: Optional[anyio.abc.TaskGroup] = None
106
106
  self._executor = ThreadPoolExecutor(max_workers=limit if limit else None)
107
107
  self._limiter = anyio.CapacityLimiter(limit) if limit else None
108
108
 
@@ -230,6 +230,9 @@ class TaskWorker:
230
230
 
231
231
  token_acquired = await self._acquire_token(task_run.id)
232
232
  if token_acquired:
233
+ assert (
234
+ self._runs_task_group is not None
235
+ ), "Task group was not initialized"
233
236
  self._runs_task_group.start_soon(
234
237
  self._safe_submit_scheduled_task_run, task_run
235
238
  )
@@ -349,7 +352,9 @@ class TaskWorker:
349
352
 
350
353
  if self._client._closed:
351
354
  self._client = get_client()
355
+ self._runs_task_group = anyio.create_task_group()
352
356
 
357
+ await self._exit_stack.__aenter__()
353
358
  await self._exit_stack.enter_async_context(self._client)
354
359
  await self._exit_stack.enter_async_context(self._runs_task_group)
355
360
  self._exit_stack.enter_context(self._executor)
@@ -513,3 +513,73 @@ def get_from_dict(dct: Dict, keys: Union[str, List[str]], default: Any = None) -
513
513
  return dct
514
514
  except (TypeError, KeyError, IndexError):
515
515
  return default
516
+
517
+
518
+ def set_in_dict(dct: Dict, keys: Union[str, List[str]], value: Any):
519
+ """
520
+ Sets a value in a nested dictionary using a sequence of keys.
521
+
522
+ This function allows to set a value in a deeply nested structure
523
+ of dictionaries and lists using either a dot-separated string or a list
524
+ of keys. If a requested key does not exist, the function will create it as
525
+ a new dictionary.
526
+
527
+ Args:
528
+ dct: The dictionary to set the value in.
529
+ keys: The sequence of keys to use for access. Can be a
530
+ dot-separated string or a list of keys.
531
+ value: The value to set in the dictionary.
532
+
533
+ Returns:
534
+ The modified dictionary with the value set at the specified key path.
535
+
536
+ Raises:
537
+ KeyError: If the key path exists and is not a dictionary.
538
+ """
539
+ if isinstance(keys, str):
540
+ keys = keys.replace("[", ".").replace("]", "").split(".")
541
+ for k in keys[:-1]:
542
+ if not isinstance(dct.get(k, {}), dict):
543
+ raise TypeError(f"Key path exists and contains a non-dict value: {keys}")
544
+ if k not in dct:
545
+ dct[k] = {}
546
+ dct = dct[k]
547
+ dct[keys[-1]] = value
548
+
549
+
550
+ def deep_merge(dct: Dict, merge: Dict):
551
+ """
552
+ Recursively merges `merge` into `dct`.
553
+
554
+ Args:
555
+ dct: The dictionary to merge into.
556
+ merge: The dictionary to merge from.
557
+
558
+ Returns:
559
+ A new dictionary with the merged contents.
560
+ """
561
+ result = dct.copy() # Start with keys and values from `dct`
562
+ for key, value in merge.items():
563
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
564
+ # If both values are dictionaries, merge them recursively
565
+ result[key] = deep_merge(result[key], value)
566
+ else:
567
+ # Otherwise, overwrite with the new value
568
+ result[key] = value
569
+ return result
570
+
571
+
572
+ def deep_merge_dicts(*dicts):
573
+ """
574
+ Recursively merges multiple dictionaries.
575
+
576
+ Args:
577
+ dicts: The dictionaries to merge.
578
+
579
+ Returns:
580
+ A new dictionary with the merged contents.
581
+ """
582
+ result = {}
583
+ for dictionary in dicts:
584
+ result = deep_merge(result, dictionary)
585
+ return result
@@ -57,7 +57,7 @@ def get_prefect_image_name(
57
57
  flavor: An optional alternative image flavor to build, like 'conda'
58
58
  """
59
59
  parsed_version = Version(prefect_version or prefect.__version__)
60
- is_prod_build = parsed_version.post is None
60
+ is_prod_build = parsed_version.local is None
61
61
  prefect_version = (
62
62
  parsed_version.base_version
63
63
  if is_prod_build
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prefect-client
3
- Version: 3.0.8
3
+ Version: 3.0.10
4
4
  Summary: Workflow orchestration and management.
5
5
  Home-page: https://www.prefect.io
6
6
  Author: Prefect Technologies, Inc.
@@ -1,17 +1,17 @@
1
1
  prefect/.prefectignore,sha256=awSprvKT0vI8a64mEOLrMxhxqcO-b0ERQeYpA2rNKVQ,390
2
2
  prefect/__init__.py,sha256=2jnhqiLx5v3iQ2JeTVp4V85uSC_3Yg3HlE05JjjQSGc,3223
3
- prefect/_version.py,sha256=KRrx9_zdQ4japuLlN9Lgz-NJqRSHpWPy5o2-HKoeKTU,496
3
+ prefect/_version.py,sha256=hvgGMUP1JDKwXHcztLhCsG85v6eHSKVrckT1E_HPJc0,497
4
4
  prefect/agent.py,sha256=BOVVY5z-vUIQ2u8LwMTXDaNys2fjOZSS5YGDwJmTQjI,230
5
5
  prefect/artifacts.py,sha256=dsxFWmdg2r9zbHM3KgKOR5YbJ29_dXUYF9kipJpbxkE,13009
6
6
  prefect/automations.py,sha256=NlQ62GPJzy-gnWQqX7c6CQJKw7p60WLGDAFcy82vtg4,5613
7
7
  prefect/cache_policies.py,sha256=PWUzyJue4h5XHVeIVolfPKhRGrx1hyWJt58AJyHbcqU,9104
8
- prefect/context.py,sha256=4CsGsfn8Kt8SHcTor5vhfcSIRm3qcQkO9vb9rfsKD3E,21571
8
+ prefect/context.py,sha256=U-IBDEQsmeZmTcNWjeeELTnYpbKKKUh0thM-S8cXRI8,21381
9
9
  prefect/engine.py,sha256=BpmDbe6miZcTl1vRkxfCPYcWSXADLigGPCagFwucMz0,1976
10
10
  prefect/exceptions.py,sha256=V_nRpS2Z93PvJMoQdXbx8zepVaFb-pWanCqVi7T1ngI,11803
11
11
  prefect/filesystems.py,sha256=CxwMmKY8LBUed_9IqE2jUqxVCWhXa1r2fjKgLbIC2Vg,17893
12
12
  prefect/flow_engine.py,sha256=p1IoMa5okV0l-0KGjDxNDsR1N74K5oP_Lb3V0z7v49U,30076
13
13
  prefect/flow_runs.py,sha256=EaXRIQTOnwnA0fO7_EjwafFRmS57K_CRy0Xsz3JDIhc,16070
14
- prefect/flows.py,sha256=lBv62fWlnembj-AsUkfuA96U8nHp8CGmnLrOaD_eWXA,89594
14
+ prefect/flows.py,sha256=ZHv9qjlSeZkap7TPFOP9nenXhBQwKYsqF2WnKIyhbhM,89604
15
15
  prefect/futures.py,sha256=_hmzkFwCGhiSBWrlfXqipN7XyA8WzfjiOhm-mtchARU,16329
16
16
  prefect/main.py,sha256=IdtnJR5-IwP8EZsfhMFKj92ylMhNyau9X_eMcTP2ZjM,2336
17
17
  prefect/plugins.py,sha256=HY7Z7OJlltqzsUiPMEL1Y_hQbHw0CeZKayWiK-k8DP4,2435
@@ -19,12 +19,12 @@ prefect/profiles.toml,sha256=kTvqDNMzjH3fsm5OEI-NKY4dMmipor5EvQXRB6rPEjY,522
19
19
  prefect/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  prefect/results.py,sha256=-V_JRaWeY2WXWhY2d_zL7KVIro660mIU6F3heNaih0o,47391
21
21
  prefect/serializers.py,sha256=Lo41EM0_qGzcfB_63390Izeo3DdK6cY6VZfxa9hpSGQ,8712
22
- prefect/settings.py,sha256=032alJnUkrWxXnHFyZD-p6cPN09x9bIoK7q7qi1VSxs,73558
22
+ prefect/settings.py,sha256=iWjtmrnvxw_oeV7Oy55p7z0DZrhAsSekmhYbnFp1UdU,76039
23
23
  prefect/states.py,sha256=2lysq6X5AvqPfE3eD3D0HYt-KpFA2OUgA0c4ZQ22A_U,24906
24
24
  prefect/task_engine.py,sha256=gjSpoLecy1gyPavNPOw40DFZonvqXIzLLqiGooqyhM0,57945
25
25
  prefect/task_runners.py,sha256=Ef8JENamKGWGyAGkuB_QwSLGWbWKRsmvemZGDkyRWCQ,15021
26
26
  prefect/task_runs.py,sha256=jkaQOkRKOHS8fgHUijteriFpjMSKv4zldn1D8tZHkUI,8777
27
- prefect/task_worker.py,sha256=a8Uw78Ms4p3ikt_la50lENmPLIa-jjbuvunvjVXvRKQ,16785
27
+ prefect/task_worker.py,sha256=VfLF0W_RAahAZM-M75vC0zxDFwcHY0V20qsQX4cDKuw,17007
28
28
  prefect/tasks.py,sha256=35eOv7VfhziiC3hL9FxB3spYtG6tpxZBLzk5KP_8Ux8,68371
29
29
  prefect/transactions.py,sha256=NTzflkehGQ5jKmuChpvsUv1-ZPBqLI7OmUeq-nZJGHQ,16558
30
30
  prefect/variables.py,sha256=023cfSj_ydwvz6lyChRKnjHFfkdoYZKK_zdTtuSxrYo,4665
@@ -149,7 +149,7 @@ prefect/records/filesystem.py,sha256=X-h7r5deiHH5IaaDk4ugOCmR5ZKnJeU2cLgp0AkMt0E
149
149
  prefect/records/memory.py,sha256=YdzQvEfb-CX0sKxAZK5TaNxVvAlyYlZse9qdoer6Xbk,6447
150
150
  prefect/records/result_store.py,sha256=3ZUFNHCCv_qBQhmIFdvlK_GMnPZcFacaI9dVdDKWdwA,2431
151
151
  prefect/runner/__init__.py,sha256=7U-vAOXFkzMfRz1q8Uv6Otsvc0OrPYLLP44srwkJ_8s,89
152
- prefect/runner/runner.py,sha256=G9OfJRRGLaerUAF7Gt1WUwGsdiFIiLLs8t9CXDCiw48,48672
152
+ prefect/runner/runner.py,sha256=e3_pJk_eIdMeGLdUYVcOl28-houpEH51dB2RHh2Gs48,48955
153
153
  prefect/runner/server.py,sha256=2o5vhrL7Zbn-HBStWhCjqqViex5Ye9GiQ1EW9RSEzdo,10500
154
154
  prefect/runner/storage.py,sha256=OsBa4nWdFxOTiAMNLFpexBdi5K3iuxidQx4YWZwditE,24734
155
155
  prefect/runner/submit.py,sha256=RuyDr-ved9wjYYarXiehY5oJVFf_HE3XKKACNWpxpPc,8131
@@ -166,11 +166,11 @@ prefect/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
166
166
  prefect/utilities/annotations.py,sha256=Ocj2s5zhnGr8uXUBnOli-OrybXVJdu4-uZvCRpKpV_Q,2820
167
167
  prefect/utilities/asyncutils.py,sha256=jWj2bMx2yLOd2QTouMOQFOtqy2DLnfefJNlujbMZZYU,20198
168
168
  prefect/utilities/callables.py,sha256=53yqDgkx7Zb_uS4v1_ltrPrvdqjwkHvqK8A0E958dFk,24859
169
- prefect/utilities/collections.py,sha256=_YVHZfT49phrXq7aDUmn4pqWwEtJQTPy2nJD0M1sz0o,17264
169
+ prefect/utilities/collections.py,sha256=eH0PnQEOw1Q1043vBsutk38g_WTKQ2Nfdlm1F4eaEZk,19435
170
170
  prefect/utilities/compat.py,sha256=mNQZDnzyKaOqy-OV-DnmH_dc7CNF5nQgW_EsA4xMr7g,906
171
171
  prefect/utilities/context.py,sha256=BThuUW94-IYgFYTeMIM9KMo8ShT3oiI7w5ajZHzU1j0,1377
172
172
  prefect/utilities/dispatch.py,sha256=EthEmyRwv-4W8z2BJclrsOQHJ_pJoZYL0t2cyYPEa-E,6098
173
- prefect/utilities/dockerutils.py,sha256=nrekaN-ZpiKMhTH586pw4LZdjig3_XQrGgW-yCfc1UA,20371
173
+ prefect/utilities/dockerutils.py,sha256=zjqeyE4gK8r0n5l3b2XK2AKviQ2F-pOd1LE2O4qfJt0,20372
174
174
  prefect/utilities/engine.py,sha256=KaGtKWNZ-EaSTTppL7zpqWWjDLpMcPTVK0Gfd4zXpRM,32087
175
175
  prefect/utilities/filesystem.py,sha256=frAyy6qOeYa7c-jVbEUGZQEe6J1yF8I_SvUepPd59gI,4415
176
176
  prefect/utilities/hashing.py,sha256=EOwZLmoIZImuSTxAvVqInabxJ-4RpEfYeg9e2EDQF8o,1752
@@ -197,8 +197,8 @@ prefect/workers/cloud.py,sha256=BOVVY5z-vUIQ2u8LwMTXDaNys2fjOZSS5YGDwJmTQjI,230
197
197
  prefect/workers/process.py,sha256=tcJ3fbiraLCfpVGpv8dOHwMSfVzeD_kyguUOvPuIz6I,19796
198
198
  prefect/workers/server.py,sha256=lgh2FfSuaNU7b6HPxSFm8JtKvAvHsZGkiOo4y4tW1Cw,2022
199
199
  prefect/workers/utilities.py,sha256=VfPfAlGtTuDj0-Kb8WlMgAuOfgXCdrGAnKMapPSBrwc,2483
200
- prefect_client-3.0.8.dist-info/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
201
- prefect_client-3.0.8.dist-info/METADATA,sha256=J7oDkuYD2cIsUOZE0aupaZLCBb3r2s6ah04ZLt2K0Ks,7332
202
- prefect_client-3.0.8.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
203
- prefect_client-3.0.8.dist-info/top_level.txt,sha256=MJZYJgFdbRc2woQCeB4vM6T33tr01TmkEhRcns6H_H4,8
204
- prefect_client-3.0.8.dist-info/RECORD,,
200
+ prefect_client-3.0.10.dist-info/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
201
+ prefect_client-3.0.10.dist-info/METADATA,sha256=tEPhy_ROPgfNv7miJMOsjFjkGMdXrRFlJsR3VlRnmLs,7333
202
+ prefect_client-3.0.10.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
203
+ prefect_client-3.0.10.dist-info/top_level.txt,sha256=MJZYJgFdbRc2woQCeB4vM6T33tr01TmkEhRcns6H_H4,8
204
+ prefect_client-3.0.10.dist-info/RECORD,,