ob-metaflow 2.15.13.1__py2.py3-none-any.whl → 2.19.7.1rc0__py2.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.
- metaflow/__init__.py +10 -3
- metaflow/_vendor/imghdr/__init__.py +186 -0
- metaflow/_vendor/yaml/__init__.py +427 -0
- metaflow/_vendor/yaml/composer.py +139 -0
- metaflow/_vendor/yaml/constructor.py +748 -0
- metaflow/_vendor/yaml/cyaml.py +101 -0
- metaflow/_vendor/yaml/dumper.py +62 -0
- metaflow/_vendor/yaml/emitter.py +1137 -0
- metaflow/_vendor/yaml/error.py +75 -0
- metaflow/_vendor/yaml/events.py +86 -0
- metaflow/_vendor/yaml/loader.py +63 -0
- metaflow/_vendor/yaml/nodes.py +49 -0
- metaflow/_vendor/yaml/parser.py +589 -0
- metaflow/_vendor/yaml/reader.py +185 -0
- metaflow/_vendor/yaml/representer.py +389 -0
- metaflow/_vendor/yaml/resolver.py +227 -0
- metaflow/_vendor/yaml/scanner.py +1435 -0
- metaflow/_vendor/yaml/serializer.py +111 -0
- metaflow/_vendor/yaml/tokens.py +104 -0
- metaflow/cards.py +4 -0
- metaflow/cli.py +125 -21
- metaflow/cli_components/init_cmd.py +1 -0
- metaflow/cli_components/run_cmds.py +204 -40
- metaflow/cli_components/step_cmd.py +160 -4
- metaflow/client/__init__.py +1 -0
- metaflow/client/core.py +198 -130
- metaflow/client/filecache.py +59 -32
- metaflow/cmd/code/__init__.py +2 -1
- metaflow/cmd/develop/stub_generator.py +49 -18
- metaflow/cmd/develop/stubs.py +9 -27
- metaflow/cmd/make_wrapper.py +30 -0
- metaflow/datastore/__init__.py +1 -0
- metaflow/datastore/content_addressed_store.py +40 -9
- metaflow/datastore/datastore_set.py +10 -1
- metaflow/datastore/flow_datastore.py +124 -4
- metaflow/datastore/spin_datastore.py +91 -0
- metaflow/datastore/task_datastore.py +92 -6
- metaflow/debug.py +5 -0
- metaflow/decorators.py +331 -82
- metaflow/extension_support/__init__.py +414 -356
- metaflow/extension_support/_empty_file.py +2 -2
- metaflow/flowspec.py +322 -82
- metaflow/graph.py +178 -15
- metaflow/includefile.py +25 -3
- metaflow/lint.py +94 -3
- metaflow/meta_files.py +13 -0
- metaflow/metadata_provider/metadata.py +13 -2
- metaflow/metaflow_config.py +66 -4
- metaflow/metaflow_environment.py +91 -25
- metaflow/metaflow_profile.py +18 -0
- metaflow/metaflow_version.py +16 -1
- metaflow/package/__init__.py +673 -0
- metaflow/packaging_sys/__init__.py +880 -0
- metaflow/packaging_sys/backend.py +128 -0
- metaflow/packaging_sys/distribution_support.py +153 -0
- metaflow/packaging_sys/tar_backend.py +99 -0
- metaflow/packaging_sys/utils.py +54 -0
- metaflow/packaging_sys/v1.py +527 -0
- metaflow/parameters.py +6 -2
- metaflow/plugins/__init__.py +6 -0
- metaflow/plugins/airflow/airflow.py +11 -1
- metaflow/plugins/airflow/airflow_cli.py +16 -5
- metaflow/plugins/argo/argo_client.py +42 -20
- metaflow/plugins/argo/argo_events.py +6 -6
- metaflow/plugins/argo/argo_workflows.py +1023 -344
- metaflow/plugins/argo/argo_workflows_cli.py +396 -94
- metaflow/plugins/argo/argo_workflows_decorator.py +9 -0
- metaflow/plugins/argo/argo_workflows_deployer_objects.py +75 -49
- metaflow/plugins/argo/capture_error.py +5 -2
- metaflow/plugins/argo/conditional_input_paths.py +35 -0
- metaflow/plugins/argo/exit_hooks.py +209 -0
- metaflow/plugins/argo/param_val.py +19 -0
- metaflow/plugins/aws/aws_client.py +6 -0
- metaflow/plugins/aws/aws_utils.py +33 -1
- metaflow/plugins/aws/batch/batch.py +72 -5
- metaflow/plugins/aws/batch/batch_cli.py +24 -3
- metaflow/plugins/aws/batch/batch_decorator.py +57 -6
- metaflow/plugins/aws/step_functions/step_functions.py +28 -3
- metaflow/plugins/aws/step_functions/step_functions_cli.py +49 -4
- metaflow/plugins/aws/step_functions/step_functions_deployer.py +3 -0
- metaflow/plugins/aws/step_functions/step_functions_deployer_objects.py +30 -0
- metaflow/plugins/cards/card_cli.py +20 -1
- metaflow/plugins/cards/card_creator.py +24 -1
- metaflow/plugins/cards/card_datastore.py +21 -49
- metaflow/plugins/cards/card_decorator.py +58 -6
- metaflow/plugins/cards/card_modules/basic.py +38 -9
- metaflow/plugins/cards/card_modules/bundle.css +1 -1
- metaflow/plugins/cards/card_modules/chevron/renderer.py +1 -1
- metaflow/plugins/cards/card_modules/components.py +592 -3
- metaflow/plugins/cards/card_modules/convert_to_native_type.py +34 -5
- metaflow/plugins/cards/card_modules/json_viewer.py +232 -0
- metaflow/plugins/cards/card_modules/main.css +1 -0
- metaflow/plugins/cards/card_modules/main.js +56 -41
- metaflow/plugins/cards/card_modules/test_cards.py +22 -6
- metaflow/plugins/cards/component_serializer.py +1 -8
- metaflow/plugins/cards/metadata.py +22 -0
- metaflow/plugins/catch_decorator.py +9 -0
- metaflow/plugins/datastores/local_storage.py +12 -6
- metaflow/plugins/datastores/spin_storage.py +12 -0
- metaflow/plugins/datatools/s3/s3.py +49 -17
- metaflow/plugins/datatools/s3/s3op.py +113 -66
- metaflow/plugins/env_escape/client_modules.py +102 -72
- metaflow/plugins/events_decorator.py +127 -121
- metaflow/plugins/exit_hook/__init__.py +0 -0
- metaflow/plugins/exit_hook/exit_hook_decorator.py +46 -0
- metaflow/plugins/exit_hook/exit_hook_script.py +52 -0
- metaflow/plugins/kubernetes/kubernetes.py +12 -1
- metaflow/plugins/kubernetes/kubernetes_cli.py +11 -0
- metaflow/plugins/kubernetes/kubernetes_decorator.py +25 -6
- metaflow/plugins/kubernetes/kubernetes_job.py +12 -4
- metaflow/plugins/kubernetes/kubernetes_jobsets.py +31 -30
- metaflow/plugins/metadata_providers/local.py +76 -82
- metaflow/plugins/metadata_providers/service.py +13 -9
- metaflow/plugins/metadata_providers/spin.py +16 -0
- metaflow/plugins/package_cli.py +36 -24
- metaflow/plugins/parallel_decorator.py +11 -2
- metaflow/plugins/parsers.py +16 -0
- metaflow/plugins/pypi/bootstrap.py +7 -1
- metaflow/plugins/pypi/conda_decorator.py +41 -82
- metaflow/plugins/pypi/conda_environment.py +14 -6
- metaflow/plugins/pypi/micromamba.py +9 -1
- metaflow/plugins/pypi/pip.py +41 -5
- metaflow/plugins/pypi/pypi_decorator.py +4 -4
- metaflow/plugins/pypi/utils.py +22 -0
- metaflow/plugins/secrets/__init__.py +3 -0
- metaflow/plugins/secrets/secrets_decorator.py +14 -178
- metaflow/plugins/secrets/secrets_func.py +49 -0
- metaflow/plugins/secrets/secrets_spec.py +101 -0
- metaflow/plugins/secrets/utils.py +74 -0
- metaflow/plugins/test_unbounded_foreach_decorator.py +2 -2
- metaflow/plugins/timeout_decorator.py +0 -1
- metaflow/plugins/uv/bootstrap.py +29 -1
- metaflow/plugins/uv/uv_environment.py +5 -3
- metaflow/pylint_wrapper.py +5 -1
- metaflow/runner/click_api.py +79 -26
- metaflow/runner/deployer.py +208 -6
- metaflow/runner/deployer_impl.py +32 -12
- metaflow/runner/metaflow_runner.py +266 -33
- metaflow/runner/subprocess_manager.py +21 -1
- metaflow/runner/utils.py +27 -16
- metaflow/runtime.py +660 -66
- metaflow/task.py +255 -26
- metaflow/user_configs/config_options.py +33 -21
- metaflow/user_configs/config_parameters.py +220 -58
- metaflow/user_decorators/__init__.py +0 -0
- metaflow/user_decorators/common.py +144 -0
- metaflow/user_decorators/mutable_flow.py +512 -0
- metaflow/user_decorators/mutable_step.py +424 -0
- metaflow/user_decorators/user_flow_decorator.py +264 -0
- metaflow/user_decorators/user_step_decorator.py +749 -0
- metaflow/util.py +197 -7
- metaflow/vendor.py +23 -7
- metaflow/version.py +1 -1
- {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/Makefile +13 -2
- {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/Tiltfile +107 -7
- {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/pick_services.sh +1 -0
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/METADATA +2 -3
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/RECORD +162 -121
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/WHEEL +1 -1
- metaflow/_vendor/v3_5/__init__.py +0 -1
- metaflow/_vendor/v3_5/importlib_metadata/__init__.py +0 -644
- metaflow/_vendor/v3_5/importlib_metadata/_compat.py +0 -152
- metaflow/_vendor/v3_5/zipp.py +0 -329
- metaflow/info_file.py +0 -25
- metaflow/package.py +0 -203
- metaflow/user_configs/config_decorators.py +0 -568
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/entry_points.txt +0 -0
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/licenses/LICENSE +0 -0
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/top_level.txt +0 -0
|
@@ -1,183 +1,17 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import re
|
|
3
2
|
|
|
4
3
|
from metaflow.exception import MetaflowException
|
|
5
4
|
from metaflow.decorators import StepDecorator
|
|
6
5
|
from metaflow.metaflow_config import DEFAULT_SECRETS_ROLE
|
|
6
|
+
from metaflow.plugins.secrets.secrets_spec import SecretSpec
|
|
7
|
+
from metaflow.plugins.secrets.utils import (
|
|
8
|
+
get_secrets_backend_provider,
|
|
9
|
+
validate_env_vars,
|
|
10
|
+
validate_env_vars_across_secrets,
|
|
11
|
+
validate_env_vars_vs_existing_env,
|
|
12
|
+
)
|
|
7
13
|
from metaflow.unbounded_foreach import UBF_TASK
|
|
8
14
|
|
|
9
|
-
from typing import Any, Dict, List, Union
|
|
10
|
-
|
|
11
|
-
DISALLOWED_SECRETS_ENV_VAR_PREFIXES = ["METAFLOW_"]
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def get_default_secrets_backend_type():
|
|
15
|
-
from metaflow.metaflow_config import DEFAULT_SECRETS_BACKEND_TYPE
|
|
16
|
-
|
|
17
|
-
if DEFAULT_SECRETS_BACKEND_TYPE is None:
|
|
18
|
-
raise MetaflowException(
|
|
19
|
-
"No default secrets backend type configured, but needed by @secrets. "
|
|
20
|
-
"Set METAFLOW_DEFAULT_SECRETS_BACKEND_TYPE."
|
|
21
|
-
)
|
|
22
|
-
return DEFAULT_SECRETS_BACKEND_TYPE
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class SecretSpec:
|
|
26
|
-
def __init__(self, secrets_backend_type, secret_id, options={}, role=None):
|
|
27
|
-
self._secrets_backend_type = secrets_backend_type
|
|
28
|
-
self._secret_id = secret_id
|
|
29
|
-
self._options = options
|
|
30
|
-
self._role = role
|
|
31
|
-
|
|
32
|
-
@property
|
|
33
|
-
def secrets_backend_type(self):
|
|
34
|
-
return self._secrets_backend_type
|
|
35
|
-
|
|
36
|
-
@property
|
|
37
|
-
def secret_id(self):
|
|
38
|
-
return self._secret_id
|
|
39
|
-
|
|
40
|
-
@property
|
|
41
|
-
def options(self):
|
|
42
|
-
return self._options
|
|
43
|
-
|
|
44
|
-
@property
|
|
45
|
-
def role(self):
|
|
46
|
-
return self._role
|
|
47
|
-
|
|
48
|
-
def to_json(self):
|
|
49
|
-
"""Mainly used for testing... not the same as the input dict in secret_spec_from_dict()!"""
|
|
50
|
-
return {
|
|
51
|
-
"secrets_backend_type": self.secrets_backend_type,
|
|
52
|
-
"secret_id": self.secret_id,
|
|
53
|
-
"options": self.options,
|
|
54
|
-
"role": self.role,
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
def __str__(self):
|
|
58
|
-
return "%s (%s)" % (self._secret_id, self._secrets_backend_type)
|
|
59
|
-
|
|
60
|
-
@staticmethod
|
|
61
|
-
def secret_spec_from_str(secret_spec_str, role):
|
|
62
|
-
# "." may be used in secret_id one day (provider specific). HOWEVER, it provides the best UX for
|
|
63
|
-
# non-conflicting cases (i.e. for secret ids that don't contain "."). This is true for all AWS
|
|
64
|
-
# Secrets Manager secrets.
|
|
65
|
-
#
|
|
66
|
-
# So we skew heavily optimize for best upfront UX for the present (1/2023).
|
|
67
|
-
#
|
|
68
|
-
# If/when a certain secret backend supports "." secret names, we can figure out a solution at that time.
|
|
69
|
-
# At a minimum, dictionary style secret spec may be used with no code changes (see secret_spec_from_dict()).
|
|
70
|
-
# Other options could be:
|
|
71
|
-
# - accept and document that "." secret_ids don't work in Metaflow (across all possible providers)
|
|
72
|
-
# - add a Metaflow config variable that specifies the separator (default ".")
|
|
73
|
-
# - smarter spec parsing, that errors on secrets that look ambiguous. "aws-secrets-manager.XYZ" could mean:
|
|
74
|
-
# + secret_id "XYZ" in aws-secrets-manager backend, OR
|
|
75
|
-
# + secret_id "aws-secrets-manager.XYZ" default backend (if it is defined).
|
|
76
|
-
# + in this case, user can simply set "azure-key-vault.aws-secrets-manager.XYZ" instead!
|
|
77
|
-
parts = secret_spec_str.split(".", maxsplit=1)
|
|
78
|
-
if len(parts) == 1:
|
|
79
|
-
secrets_backend_type = get_default_secrets_backend_type()
|
|
80
|
-
secret_id = parts[0]
|
|
81
|
-
else:
|
|
82
|
-
secrets_backend_type = parts[0]
|
|
83
|
-
secret_id = parts[1]
|
|
84
|
-
return SecretSpec(
|
|
85
|
-
secrets_backend_type, secret_id=secret_id, options={}, role=role
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
@staticmethod
|
|
89
|
-
def secret_spec_from_dict(secret_spec_dict, role):
|
|
90
|
-
if "type" not in secret_spec_dict:
|
|
91
|
-
secrets_backend_type = get_default_secrets_backend_type()
|
|
92
|
-
else:
|
|
93
|
-
secrets_backend_type = secret_spec_dict["type"]
|
|
94
|
-
if not isinstance(secrets_backend_type, str):
|
|
95
|
-
raise MetaflowException(
|
|
96
|
-
"Bad @secrets specification - 'type' must be a string - found %s"
|
|
97
|
-
% type(secrets_backend_type)
|
|
98
|
-
)
|
|
99
|
-
secret_id = secret_spec_dict.get("id")
|
|
100
|
-
if not isinstance(secret_id, str):
|
|
101
|
-
raise MetaflowException(
|
|
102
|
-
"Bad @secrets specification - 'id' must be a string - found %s"
|
|
103
|
-
% type(secret_id)
|
|
104
|
-
)
|
|
105
|
-
options = secret_spec_dict.get("options", {})
|
|
106
|
-
if not isinstance(options, dict):
|
|
107
|
-
raise MetaflowException(
|
|
108
|
-
"Bad @secrets specification - 'option' must be a dict - found %s"
|
|
109
|
-
% type(options)
|
|
110
|
-
)
|
|
111
|
-
role_for_source = secret_spec_dict.get("role", None)
|
|
112
|
-
if role_for_source is not None:
|
|
113
|
-
if not isinstance(role_for_source, str):
|
|
114
|
-
raise MetaflowException(
|
|
115
|
-
"Bad @secrets specification - 'role' must be a str - found %s"
|
|
116
|
-
% type(role_for_source)
|
|
117
|
-
)
|
|
118
|
-
role = role_for_source
|
|
119
|
-
return SecretSpec(
|
|
120
|
-
secrets_backend_type, secret_id=secret_id, options=options, role=role
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def validate_env_vars_across_secrets(all_secrets_env_vars):
|
|
125
|
-
vars_injected_by = {}
|
|
126
|
-
for secret_spec, env_vars in all_secrets_env_vars:
|
|
127
|
-
for k in env_vars:
|
|
128
|
-
if k in vars_injected_by:
|
|
129
|
-
raise MetaflowException(
|
|
130
|
-
"Secret '%s' will inject '%s' as env var, and it is also added by '%s'"
|
|
131
|
-
% (secret_spec, k, vars_injected_by[k])
|
|
132
|
-
)
|
|
133
|
-
vars_injected_by[k] = secret_spec
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def validate_env_vars_vs_existing_env(all_secrets_env_vars):
|
|
137
|
-
for secret_spec, env_vars in all_secrets_env_vars:
|
|
138
|
-
for k in env_vars:
|
|
139
|
-
if k in os.environ:
|
|
140
|
-
raise MetaflowException(
|
|
141
|
-
"Secret '%s' will inject '%s' as env var, but it already exists in env"
|
|
142
|
-
% (secret_spec, k)
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def validate_env_vars(env_vars):
|
|
147
|
-
for k, v in env_vars.items():
|
|
148
|
-
if not isinstance(k, str):
|
|
149
|
-
raise MetaflowException("Found non string key %s (%s)" % (str(k), type(k)))
|
|
150
|
-
if not isinstance(v, str):
|
|
151
|
-
raise MetaflowException(
|
|
152
|
-
"Found non string value %s (%s)" % (str(v), type(v))
|
|
153
|
-
)
|
|
154
|
-
if not re.fullmatch("[a-zA-Z_][a-zA-Z0-9_]*", k):
|
|
155
|
-
raise MetaflowException("Found invalid env var name '%s'." % k)
|
|
156
|
-
for disallowed_prefix in DISALLOWED_SECRETS_ENV_VAR_PREFIXES:
|
|
157
|
-
if k.startswith(disallowed_prefix):
|
|
158
|
-
raise MetaflowException(
|
|
159
|
-
"Found disallowed env var name '%s' (starts with '%s')."
|
|
160
|
-
% (k, disallowed_prefix)
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def get_secrets_backend_provider(secrets_backend_type):
|
|
165
|
-
from metaflow.plugins import SECRETS_PROVIDERS
|
|
166
|
-
|
|
167
|
-
try:
|
|
168
|
-
provider_cls = [
|
|
169
|
-
pc for pc in SECRETS_PROVIDERS if pc.TYPE == secrets_backend_type
|
|
170
|
-
][0]
|
|
171
|
-
return provider_cls()
|
|
172
|
-
except IndexError:
|
|
173
|
-
raise MetaflowException(
|
|
174
|
-
"Unknown secrets backend type %s (available types: %s)"
|
|
175
|
-
% (
|
|
176
|
-
secrets_backend_type,
|
|
177
|
-
", ".join(pc.TYPE for pc in SECRETS_PROVIDERS if pc.TYPE != "inline"),
|
|
178
|
-
)
|
|
179
|
-
)
|
|
180
|
-
|
|
181
15
|
|
|
182
16
|
class SecretsDecorator(StepDecorator):
|
|
183
17
|
"""
|
|
@@ -188,13 +22,14 @@ class SecretsDecorator(StepDecorator):
|
|
|
188
22
|
----------
|
|
189
23
|
sources : List[Union[str, Dict[str, Any]]], default: []
|
|
190
24
|
List of secret specs, defining how the secrets are to be retrieved
|
|
25
|
+
role : str, optional, default: None
|
|
26
|
+
Role to use for fetching secrets
|
|
27
|
+
allow_override : bool, optional, default: False
|
|
28
|
+
Toggle whether secrets can replace existing environment variables.
|
|
191
29
|
"""
|
|
192
30
|
|
|
193
31
|
name = "secrets"
|
|
194
|
-
defaults = {
|
|
195
|
-
"sources": [],
|
|
196
|
-
"role": None,
|
|
197
|
-
}
|
|
32
|
+
defaults = {"sources": [], "role": None, "allow_override": False}
|
|
198
33
|
|
|
199
34
|
def task_pre_step(
|
|
200
35
|
self,
|
|
@@ -276,7 +111,8 @@ class SecretsDecorator(StepDecorator):
|
|
|
276
111
|
all_secrets_env_vars.append((secret_spec, env_vars_for_secret))
|
|
277
112
|
|
|
278
113
|
validate_env_vars_across_secrets(all_secrets_env_vars)
|
|
279
|
-
|
|
114
|
+
if not self.attributes["allow_override"]:
|
|
115
|
+
validate_env_vars_vs_existing_env(all_secrets_env_vars)
|
|
280
116
|
|
|
281
117
|
# By this point
|
|
282
118
|
# all_secrets_env_vars contains a list of dictionaries... env maps.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional, Union
|
|
2
|
+
|
|
3
|
+
from metaflow.metaflow_config import DEFAULT_SECRETS_ROLE
|
|
4
|
+
from metaflow.exception import MetaflowException
|
|
5
|
+
from metaflow.plugins.secrets.secrets_spec import SecretSpec
|
|
6
|
+
from metaflow.plugins.secrets.utils import get_secrets_backend_provider
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_secret(
|
|
10
|
+
source: Union[str, Dict[str, Any]], role: Optional[str] = None
|
|
11
|
+
) -> Dict[str, str]:
|
|
12
|
+
"""
|
|
13
|
+
Get secret from source
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
source : Union[str, Dict[str, Any]]
|
|
18
|
+
Secret spec, defining how the secret is to be retrieved
|
|
19
|
+
role : str, optional
|
|
20
|
+
Role to use for fetching secrets
|
|
21
|
+
"""
|
|
22
|
+
if role is None:
|
|
23
|
+
role = DEFAULT_SECRETS_ROLE
|
|
24
|
+
|
|
25
|
+
secret_spec = None
|
|
26
|
+
|
|
27
|
+
if isinstance(source, str):
|
|
28
|
+
secret_spec = SecretSpec.secret_spec_from_str(source, role=role)
|
|
29
|
+
elif isinstance(source, dict):
|
|
30
|
+
secret_spec = SecretSpec.secret_spec_from_dict(source, role=role)
|
|
31
|
+
else:
|
|
32
|
+
raise MetaflowException(
|
|
33
|
+
"get_secrets sources items must be either a string or a dict"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
secrets_backend_provider = get_secrets_backend_provider(
|
|
37
|
+
secret_spec.secrets_backend_type
|
|
38
|
+
)
|
|
39
|
+
try:
|
|
40
|
+
dict_for_secret = secrets_backend_provider.get_secret_as_dict(
|
|
41
|
+
secret_spec.secret_id,
|
|
42
|
+
options=secret_spec.options,
|
|
43
|
+
role=secret_spec.role,
|
|
44
|
+
)
|
|
45
|
+
return dict_for_secret
|
|
46
|
+
except Exception as e:
|
|
47
|
+
raise MetaflowException(
|
|
48
|
+
"Failed to retrieve secret '%s': %s" % (secret_spec.secret_id, e)
|
|
49
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from metaflow.exception import MetaflowException
|
|
2
|
+
from metaflow.plugins.secrets.utils import get_default_secrets_backend_type
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SecretSpec:
|
|
6
|
+
def __init__(self, secrets_backend_type, secret_id, options={}, role=None):
|
|
7
|
+
self._secrets_backend_type = secrets_backend_type
|
|
8
|
+
self._secret_id = secret_id
|
|
9
|
+
self._options = options
|
|
10
|
+
self._role = role
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def secrets_backend_type(self):
|
|
14
|
+
return self._secrets_backend_type
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def secret_id(self):
|
|
18
|
+
return self._secret_id
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def options(self):
|
|
22
|
+
return self._options
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def role(self):
|
|
26
|
+
return self._role
|
|
27
|
+
|
|
28
|
+
def to_json(self):
|
|
29
|
+
"""Mainly used for testing... not the same as the input dict in secret_spec_from_dict()!"""
|
|
30
|
+
return {
|
|
31
|
+
"secrets_backend_type": self.secrets_backend_type,
|
|
32
|
+
"secret_id": self.secret_id,
|
|
33
|
+
"options": self.options,
|
|
34
|
+
"role": self.role,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def __str__(self):
|
|
38
|
+
return "%s (%s)" % (self._secret_id, self._secrets_backend_type)
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def secret_spec_from_str(secret_spec_str, role):
|
|
42
|
+
# "." may be used in secret_id one day (provider specific). HOWEVER, it provides the best UX for
|
|
43
|
+
# non-conflicting cases (i.e. for secret ids that don't contain "."). This is true for all AWS
|
|
44
|
+
# Secrets Manager secrets.
|
|
45
|
+
#
|
|
46
|
+
# So we skew heavily optimize for best upfront UX for the present (1/2023).
|
|
47
|
+
#
|
|
48
|
+
# If/when a certain secret backend supports "." secret names, we can figure out a solution at that time.
|
|
49
|
+
# At a minimum, dictionary style secret spec may be used with no code changes (see secret_spec_from_dict()).
|
|
50
|
+
# Other options could be:
|
|
51
|
+
# - accept and document that "." secret_ids don't work in Metaflow (across all possible providers)
|
|
52
|
+
# - add a Metaflow config variable that specifies the separator (default ".")
|
|
53
|
+
# - smarter spec parsing, that errors on secrets that look ambiguous. "aws-secrets-manager.XYZ" could mean:
|
|
54
|
+
# + secret_id "XYZ" in aws-secrets-manager backend, OR
|
|
55
|
+
# + secret_id "aws-secrets-manager.XYZ" default backend (if it is defined).
|
|
56
|
+
# + in this case, user can simply set "azure-key-vault.aws-secrets-manager.XYZ" instead!
|
|
57
|
+
parts = secret_spec_str.split(".", maxsplit=1)
|
|
58
|
+
if len(parts) == 1:
|
|
59
|
+
secrets_backend_type = get_default_secrets_backend_type()
|
|
60
|
+
secret_id = parts[0]
|
|
61
|
+
else:
|
|
62
|
+
secrets_backend_type = parts[0]
|
|
63
|
+
secret_id = parts[1]
|
|
64
|
+
return SecretSpec(
|
|
65
|
+
secrets_backend_type, secret_id=secret_id, options={}, role=role
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def secret_spec_from_dict(secret_spec_dict, role):
|
|
70
|
+
if "type" not in secret_spec_dict:
|
|
71
|
+
secrets_backend_type = get_default_secrets_backend_type()
|
|
72
|
+
else:
|
|
73
|
+
secrets_backend_type = secret_spec_dict["type"]
|
|
74
|
+
if not isinstance(secrets_backend_type, str):
|
|
75
|
+
raise MetaflowException(
|
|
76
|
+
"Bad @secrets specification - 'type' must be a string - found %s"
|
|
77
|
+
% type(secrets_backend_type)
|
|
78
|
+
)
|
|
79
|
+
secret_id = secret_spec_dict.get("id")
|
|
80
|
+
if not isinstance(secret_id, str):
|
|
81
|
+
raise MetaflowException(
|
|
82
|
+
"Bad @secrets specification - 'id' must be a string - found %s"
|
|
83
|
+
% type(secret_id)
|
|
84
|
+
)
|
|
85
|
+
options = secret_spec_dict.get("options", {})
|
|
86
|
+
if not isinstance(options, dict):
|
|
87
|
+
raise MetaflowException(
|
|
88
|
+
"Bad @secrets specification - 'option' must be a dict - found %s"
|
|
89
|
+
% type(options)
|
|
90
|
+
)
|
|
91
|
+
role_for_source = secret_spec_dict.get("role", None)
|
|
92
|
+
if role_for_source is not None:
|
|
93
|
+
if not isinstance(role_for_source, str):
|
|
94
|
+
raise MetaflowException(
|
|
95
|
+
"Bad @secrets specification - 'role' must be a str - found %s"
|
|
96
|
+
% type(role_for_source)
|
|
97
|
+
)
|
|
98
|
+
role = role_for_source
|
|
99
|
+
return SecretSpec(
|
|
100
|
+
secrets_backend_type, secret_id=secret_id, options=options, role=role
|
|
101
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from metaflow.exception import MetaflowException
|
|
4
|
+
|
|
5
|
+
DISALLOWED_SECRETS_ENV_VAR_PREFIXES = ["METAFLOW_"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_default_secrets_backend_type():
|
|
9
|
+
from metaflow.metaflow_config import DEFAULT_SECRETS_BACKEND_TYPE
|
|
10
|
+
|
|
11
|
+
if DEFAULT_SECRETS_BACKEND_TYPE is None:
|
|
12
|
+
raise MetaflowException(
|
|
13
|
+
"No default secrets backend type configured, but needed by @secrets. "
|
|
14
|
+
"Set METAFLOW_DEFAULT_SECRETS_BACKEND_TYPE."
|
|
15
|
+
)
|
|
16
|
+
return DEFAULT_SECRETS_BACKEND_TYPE
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_env_vars_across_secrets(all_secrets_env_vars):
|
|
20
|
+
vars_injected_by = {}
|
|
21
|
+
for secret_spec, env_vars in all_secrets_env_vars:
|
|
22
|
+
for k in env_vars:
|
|
23
|
+
if k in vars_injected_by:
|
|
24
|
+
raise MetaflowException(
|
|
25
|
+
"Secret '%s' will inject '%s' as env var, and it is also added by '%s'"
|
|
26
|
+
% (secret_spec, k, vars_injected_by[k])
|
|
27
|
+
)
|
|
28
|
+
vars_injected_by[k] = secret_spec
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_env_vars_vs_existing_env(all_secrets_env_vars):
|
|
32
|
+
for secret_spec, env_vars in all_secrets_env_vars:
|
|
33
|
+
for k in env_vars:
|
|
34
|
+
if k in os.environ:
|
|
35
|
+
raise MetaflowException(
|
|
36
|
+
"Secret '%s' will inject '%s' as env var, but it already exists in env"
|
|
37
|
+
% (secret_spec, k)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_env_vars(env_vars):
|
|
42
|
+
for k, v in env_vars.items():
|
|
43
|
+
if not isinstance(k, str):
|
|
44
|
+
raise MetaflowException("Found non string key %s (%s)" % (str(k), type(k)))
|
|
45
|
+
if not isinstance(v, str):
|
|
46
|
+
raise MetaflowException(
|
|
47
|
+
"Found non string value %s (%s)" % (str(v), type(v))
|
|
48
|
+
)
|
|
49
|
+
if not re.fullmatch("[a-zA-Z_][a-zA-Z0-9_]*", k):
|
|
50
|
+
raise MetaflowException("Found invalid env var name '%s'." % k)
|
|
51
|
+
for disallowed_prefix in DISALLOWED_SECRETS_ENV_VAR_PREFIXES:
|
|
52
|
+
if k.startswith(disallowed_prefix):
|
|
53
|
+
raise MetaflowException(
|
|
54
|
+
"Found disallowed env var name '%s' (starts with '%s')."
|
|
55
|
+
% (k, disallowed_prefix)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_secrets_backend_provider(secrets_backend_type):
|
|
60
|
+
from metaflow.plugins import SECRETS_PROVIDERS
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
provider_cls = [
|
|
64
|
+
pc for pc in SECRETS_PROVIDERS if pc.TYPE == secrets_backend_type
|
|
65
|
+
][0]
|
|
66
|
+
return provider_cls()
|
|
67
|
+
except IndexError:
|
|
68
|
+
raise MetaflowException(
|
|
69
|
+
"Unknown secrets backend type %s (available types: %s)"
|
|
70
|
+
% (
|
|
71
|
+
secrets_backend_type,
|
|
72
|
+
", ".join(pc.TYPE for pc in SECRETS_PROVIDERS if pc.TYPE != "inline"),
|
|
73
|
+
)
|
|
74
|
+
)
|
|
@@ -56,9 +56,9 @@ class InternalTestUnboundedForeachDecorator(StepDecorator):
|
|
|
56
56
|
name = "unbounded_test_foreach_internal"
|
|
57
57
|
results_dict = {}
|
|
58
58
|
|
|
59
|
-
def __init__(self, attributes=None, statically_defined=False):
|
|
59
|
+
def __init__(self, attributes=None, statically_defined=False, inserted_by=None):
|
|
60
60
|
super(InternalTestUnboundedForeachDecorator, self).__init__(
|
|
61
|
-
attributes, statically_defined
|
|
61
|
+
attributes, statically_defined, inserted_by
|
|
62
62
|
)
|
|
63
63
|
|
|
64
64
|
def step_init(
|
|
@@ -38,7 +38,6 @@ class TimeoutDecorator(StepDecorator):
|
|
|
38
38
|
defaults = {"seconds": 0, "minutes": 0, "hours": 0}
|
|
39
39
|
|
|
40
40
|
def init(self):
|
|
41
|
-
super().init()
|
|
42
41
|
# Initialize secs in __init__ so other decorators could safely use this
|
|
43
42
|
# value without worrying about decorator order.
|
|
44
43
|
# Convert values in attributes to type:int since they can be type:str
|
metaflow/plugins/uv/bootstrap.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import shutil
|
|
2
3
|
import subprocess
|
|
3
4
|
import sys
|
|
4
5
|
import time
|
|
5
6
|
|
|
6
7
|
from metaflow.util import which
|
|
8
|
+
from metaflow.meta_files import read_info_file
|
|
7
9
|
from metaflow.metaflow_config import get_pinned_conda_libs
|
|
10
|
+
from metaflow.packaging_sys import MetaflowCodeContent, ContentType
|
|
8
11
|
from urllib.request import Request, urlopen
|
|
9
12
|
from urllib.error import URLError
|
|
10
13
|
|
|
@@ -78,11 +81,36 @@ if __name__ == "__main__":
|
|
|
78
81
|
# return only dependency names instead of pinned versions
|
|
79
82
|
return pinned.keys()
|
|
80
83
|
|
|
84
|
+
def skip_metaflow_dependencies():
|
|
85
|
+
skip_pkgs = ["metaflow", "ob-metaflow"]
|
|
86
|
+
info = read_info_file()
|
|
87
|
+
if info is not None:
|
|
88
|
+
try:
|
|
89
|
+
skip_pkgs.extend([ext_name for ext_name in info["ext_info"][0].keys()])
|
|
90
|
+
except Exception:
|
|
91
|
+
print(
|
|
92
|
+
"Failed to read INFO. Metaflow-related packages might get installed during runtime."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return skip_pkgs
|
|
96
|
+
|
|
81
97
|
def sync_uv_project(datastore_type):
|
|
98
|
+
# Move the files to the current directory so uv can find them.
|
|
99
|
+
for filename in ["uv.lock", "pyproject.toml"]:
|
|
100
|
+
path_to_file = MetaflowCodeContent.get_filename(
|
|
101
|
+
filename, ContentType.OTHER_CONTENT
|
|
102
|
+
)
|
|
103
|
+
if path_to_file is None:
|
|
104
|
+
raise RuntimeError(f"Could not find {filename} in the package.")
|
|
105
|
+
shutil.move(path_to_file, os.path.join(os.getcwd(), filename))
|
|
106
|
+
|
|
82
107
|
print("Syncing uv project...")
|
|
83
108
|
dependencies = " ".join(get_dependencies(datastore_type))
|
|
109
|
+
skip_pkgs = " ".join(
|
|
110
|
+
[f"--no-install-package {dep}" for dep in skip_metaflow_dependencies()]
|
|
111
|
+
)
|
|
84
112
|
cmd = f"""set -e;
|
|
85
|
-
uv sync --frozen
|
|
113
|
+
uv sync --frozen {skip_pkgs};
|
|
86
114
|
uv pip install {dependencies} --strict
|
|
87
115
|
"""
|
|
88
116
|
run_cmd(cmd)
|
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
|
|
3
3
|
from metaflow.exception import MetaflowException
|
|
4
4
|
from metaflow.metaflow_environment import MetaflowEnvironment
|
|
5
|
+
from metaflow.packaging_sys import ContentType
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class UVException(MetaflowException):
|
|
@@ -12,6 +13,7 @@ class UVEnvironment(MetaflowEnvironment):
|
|
|
12
13
|
TYPE = "uv"
|
|
13
14
|
|
|
14
15
|
def __init__(self, flow):
|
|
16
|
+
super().__init__(flow)
|
|
15
17
|
self.flow = flow
|
|
16
18
|
|
|
17
19
|
def validate_environment(self, logger, datastore_type):
|
|
@@ -22,7 +24,7 @@ class UVEnvironment(MetaflowEnvironment):
|
|
|
22
24
|
self.logger("Bootstrapping uv...")
|
|
23
25
|
|
|
24
26
|
def executable(self, step_name, default=None):
|
|
25
|
-
return "uv run python"
|
|
27
|
+
return "uv run --no-sync python"
|
|
26
28
|
|
|
27
29
|
def add_to_package(self):
|
|
28
30
|
# NOTE: We treat uv.lock and pyproject.toml as regular project assets and ship these along user code as part of the code package
|
|
@@ -43,8 +45,8 @@ class UVEnvironment(MetaflowEnvironment):
|
|
|
43
45
|
pyproject_path = _find("pyproject.toml")
|
|
44
46
|
uv_lock_path = _find("uv.lock")
|
|
45
47
|
files = [
|
|
46
|
-
(uv_lock_path, "uv.lock"),
|
|
47
|
-
(pyproject_path, "pyproject.toml"),
|
|
48
|
+
(uv_lock_path, "uv.lock", ContentType.OTHER_CONTENT),
|
|
49
|
+
(pyproject_path, "pyproject.toml", ContentType.OTHER_CONTENT),
|
|
48
50
|
]
|
|
49
51
|
return files
|
|
50
52
|
|
metaflow/pylint_wrapper.py
CHANGED
|
@@ -28,7 +28,11 @@ class PyLint(object):
|
|
|
28
28
|
return self._run is not None
|
|
29
29
|
|
|
30
30
|
def run(self, logger=None, warnings=False, pylint_config=[]):
|
|
31
|
-
args = [
|
|
31
|
+
args = [
|
|
32
|
+
self._fname,
|
|
33
|
+
"--signature-mutators",
|
|
34
|
+
"metaflow.user_decorators.user_step_decorator.user_step_decorator",
|
|
35
|
+
]
|
|
32
36
|
if not warnings:
|
|
33
37
|
args.append("--errors-only")
|
|
34
38
|
if pylint_config:
|