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 +3 -3
- prefect/context.py +13 -21
- prefect/flows.py +1 -1
- prefect/runner/runner.py +15 -8
- prefect/settings.py +215 -149
- prefect/task_worker.py +6 -1
- prefect/utilities/collections.py +70 -0
- prefect/utilities/dockerutils.py +1 -1
- {prefect_client-3.0.8.dist-info → prefect_client-3.0.10.dist-info}/METADATA +1 -1
- {prefect_client-3.0.8.dist-info → prefect_client-3.0.10.dist-info}/RECORD +13 -13
- {prefect_client-3.0.8.dist-info → prefect_client-3.0.10.dist-info}/LICENSE +0 -0
- {prefect_client-3.0.8.dist-info → prefect_client-3.0.10.dist-info}/WHEEL +0 -0
- {prefect_client-3.0.8.dist-info → prefect_client-3.0.10.dist-info}/top_level.txt +0 -0
prefect/_version.py
CHANGED
@@ -8,11 +8,11 @@ import json
|
|
8
8
|
|
9
9
|
version_json = '''
|
10
10
|
{
|
11
|
-
"date": "2024-10-
|
11
|
+
"date": "2024-10-15T13:31:59-0500",
|
12
12
|
"dirty": true,
|
13
13
|
"error": null,
|
14
|
-
"full-revisionid": "
|
15
|
-
"version": "3.0.
|
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
|
-
|
571
|
-
with TagsContext(current_tags=
|
572
|
-
yield
|
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
|
-
|
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
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
|
-
|
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
|
-
|
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
|
-
|
415
|
-
|
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
|
-
|
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
|
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
|
80
|
+
def env_var_to_accessor(env_var: str) -> str:
|
76
81
|
"""
|
77
|
-
Convert an environment variable name to
|
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__(
|
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
|
-
|
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
|
-
|
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.
|
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
|
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(
|
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
|
-
|
508
|
-
|
509
|
-
description="
|
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 =
|
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.
|
1460
|
-
self.ui_api_url = self.
|
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
|
-
|
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
|
-
**
|
1559
|
-
|
1560
|
-
|
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
|
-
|
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
|
-
|
1743
|
-
|
1744
|
-
|
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
|
-
|
2062
|
-
|
2063
|
-
|
2064
|
-
|
2065
|
-
|
2066
|
-
|
2067
|
-
|
2068
|
-
|
2069
|
-
|
2070
|
-
|
2071
|
-
|
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
|
-
|
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] =
|
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 =
|
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)
|
prefect/utilities/collections.py
CHANGED
@@ -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
|
prefect/utilities/dockerutils.py
CHANGED
@@ -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.
|
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,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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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.
|
201
|
-
prefect_client-3.0.
|
202
|
-
prefect_client-3.0.
|
203
|
-
prefect_client-3.0.
|
204
|
-
prefect_client-3.0.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|