metaflow 2.15.13__py2.py3-none-any.whl → 2.15.15__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.
Files changed (39) hide show
  1. metaflow/__init__.py +2 -2
  2. metaflow/_vendor/click/core.py +4 -3
  3. metaflow/cmd/develop/stubs.py +9 -27
  4. metaflow/datastore/task_datastore.py +3 -3
  5. metaflow/decorators.py +3 -3
  6. metaflow/extension_support/__init__.py +25 -42
  7. metaflow/metaflow_config.py +9 -1
  8. metaflow/parameters.py +2 -2
  9. metaflow/plugins/argo/argo_workflows_cli.py +4 -4
  10. metaflow/plugins/cards/card_modules/chevron/renderer.py +1 -1
  11. metaflow/plugins/cards/card_modules/test_cards.py +6 -6
  12. metaflow/plugins/cards/component_serializer.py +1 -8
  13. metaflow/plugins/package_cli.py +12 -2
  14. metaflow/plugins/pypi/bootstrap.py +2 -2
  15. metaflow/plugins/uv/bootstrap.py +18 -1
  16. metaflow/plugins/uv/uv_environment.py +1 -1
  17. metaflow/runner/click_api.py +16 -9
  18. metaflow/runner/deployer_impl.py +17 -5
  19. metaflow/runner/metaflow_runner.py +40 -13
  20. metaflow/runner/subprocess_manager.py +1 -1
  21. metaflow/runner/utils.py +8 -0
  22. metaflow/user_configs/config_options.py +6 -6
  23. metaflow/user_configs/config_parameters.py +211 -45
  24. metaflow/util.py +2 -5
  25. metaflow/vendor.py +0 -1
  26. metaflow/version.py +1 -1
  27. {metaflow-2.15.13.dist-info → metaflow-2.15.15.dist-info}/METADATA +2 -2
  28. {metaflow-2.15.13.dist-info → metaflow-2.15.15.dist-info}/RECORD +35 -39
  29. {metaflow-2.15.13.dist-info → metaflow-2.15.15.dist-info}/WHEEL +1 -1
  30. metaflow/_vendor/v3_5/__init__.py +0 -1
  31. metaflow/_vendor/v3_5/importlib_metadata/__init__.py +0 -644
  32. metaflow/_vendor/v3_5/importlib_metadata/_compat.py +0 -152
  33. metaflow/_vendor/v3_5/zipp.py +0 -329
  34. {metaflow-2.15.13.data → metaflow-2.15.15.data}/data/share/metaflow/devtools/Makefile +0 -0
  35. {metaflow-2.15.13.data → metaflow-2.15.15.data}/data/share/metaflow/devtools/Tiltfile +0 -0
  36. {metaflow-2.15.13.data → metaflow-2.15.15.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
  37. {metaflow-2.15.13.dist-info → metaflow-2.15.15.dist-info}/entry_points.txt +0 -0
  38. {metaflow-2.15.13.dist-info → metaflow-2.15.15.dist-info}/licenses/LICENSE +0 -0
  39. {metaflow-2.15.13.dist-info → metaflow-2.15.15.dist-info}/top_level.txt +0 -0
@@ -7,12 +7,15 @@ from typing import Dict, Iterator, Optional, Tuple
7
7
 
8
8
  from metaflow import Run
9
9
 
10
+ from metaflow.metaflow_config import CLICK_API_PROCESS_CONFIG
11
+
10
12
  from metaflow.plugins import get_runner_cli
11
13
 
12
14
  from .utils import (
13
15
  temporary_fifo,
14
16
  handle_timeout,
15
17
  async_handle_timeout,
18
+ with_dir,
16
19
  )
17
20
  from .subprocess_manager import CommandManager, SubprocessManager
18
21
 
@@ -299,7 +302,7 @@ class Runner(metaclass=RunnerMeta):
299
302
  if profile:
300
303
  self.env_vars["METAFLOW_PROFILE"] = profile
301
304
 
302
- self.cwd = cwd
305
+ self.cwd = cwd or os.getcwd()
303
306
  self.file_read_timeout = file_read_timeout
304
307
  self.spm = SubprocessManager()
305
308
  self.top_level_kwargs = kwargs
@@ -359,9 +362,15 @@ class Runner(metaclass=RunnerMeta):
359
362
  ExecutingRun containing the results of the run.
360
363
  """
361
364
  with temporary_fifo() as (attribute_file_path, attribute_file_fd):
362
- command = self.api(**self.top_level_kwargs).run(
363
- runner_attribute_file=attribute_file_path, **kwargs
364
- )
365
+ if CLICK_API_PROCESS_CONFIG:
366
+ with with_dir(self.cwd):
367
+ command = self.api(**self.top_level_kwargs).run(
368
+ runner_attribute_file=attribute_file_path, **kwargs
369
+ )
370
+ else:
371
+ command = self.api(**self.top_level_kwargs).run(
372
+ runner_attribute_file=attribute_file_path, **kwargs
373
+ )
365
374
 
366
375
  pid = self.spm.run_command(
367
376
  [sys.executable, *command],
@@ -390,9 +399,15 @@ class Runner(metaclass=RunnerMeta):
390
399
  ExecutingRun containing the results of the resumed run.
391
400
  """
392
401
  with temporary_fifo() as (attribute_file_path, attribute_file_fd):
393
- command = self.api(**self.top_level_kwargs).resume(
394
- runner_attribute_file=attribute_file_path, **kwargs
395
- )
402
+ if CLICK_API_PROCESS_CONFIG:
403
+ with with_dir(self.cwd):
404
+ command = self.api(**self.top_level_kwargs).resume(
405
+ runner_attribute_file=attribute_file_path, **kwargs
406
+ )
407
+ else:
408
+ command = self.api(**self.top_level_kwargs).resume(
409
+ runner_attribute_file=attribute_file_path, **kwargs
410
+ )
396
411
 
397
412
  pid = self.spm.run_command(
398
413
  [sys.executable, *command],
@@ -423,9 +438,15 @@ class Runner(metaclass=RunnerMeta):
423
438
  ExecutingRun representing the run that was started.
424
439
  """
425
440
  with temporary_fifo() as (attribute_file_path, attribute_file_fd):
426
- command = self.api(**self.top_level_kwargs).run(
427
- runner_attribute_file=attribute_file_path, **kwargs
428
- )
441
+ if CLICK_API_PROCESS_CONFIG:
442
+ with with_dir(self.cwd):
443
+ command = self.api(**self.top_level_kwargs).run(
444
+ runner_attribute_file=attribute_file_path, **kwargs
445
+ )
446
+ else:
447
+ command = self.api(**self.top_level_kwargs).run(
448
+ runner_attribute_file=attribute_file_path, **kwargs
449
+ )
429
450
 
430
451
  pid = await self.spm.async_run_command(
431
452
  [sys.executable, *command],
@@ -455,9 +476,15 @@ class Runner(metaclass=RunnerMeta):
455
476
  ExecutingRun representing the resumed run that was started.
456
477
  """
457
478
  with temporary_fifo() as (attribute_file_path, attribute_file_fd):
458
- command = self.api(**self.top_level_kwargs).resume(
459
- runner_attribute_file=attribute_file_path, **kwargs
460
- )
479
+ if CLICK_API_PROCESS_CONFIG:
480
+ with with_dir(self.cwd):
481
+ command = self.api(**self.top_level_kwargs).resume(
482
+ runner_attribute_file=attribute_file_path, **kwargs
483
+ )
484
+ else:
485
+ command = self.api(**self.top_level_kwargs).resume(
486
+ runner_attribute_file=attribute_file_path, **kwargs
487
+ )
461
488
 
462
489
  pid = await self.spm.async_run_command(
463
490
  [sys.executable, *command],
@@ -237,7 +237,7 @@ class CommandManager(object):
237
237
  self.command = command
238
238
 
239
239
  self.env = env if env is not None else os.environ.copy()
240
- self.cwd = cwd if cwd is not None else os.getcwd()
240
+ self.cwd = cwd or os.getcwd()
241
241
 
242
242
  self.process = None
243
243
  self.stdout_thread = None
metaflow/runner/utils.py CHANGED
@@ -322,3 +322,11 @@ def get_lower_level_group(
322
322
  raise ValueError(f"Sub-command '{sub_command}' not found in API '{api.name}'")
323
323
 
324
324
  return sub_command_obj(**sub_command_kwargs)
325
+
326
+
327
+ @contextmanager
328
+ def with_dir(new_dir):
329
+ current_dir = os.getcwd()
330
+ os.chdir(new_dir)
331
+ yield new_dir
332
+ os.chdir(current_dir)
@@ -221,13 +221,13 @@ class ConfigInput:
221
221
  if param_name == "config_value":
222
222
  self._value_values = {
223
223
  k.lower(): v
224
- for k, v in param_value
224
+ for k, v in param_value.items()
225
225
  if v is not None and not v.startswith(_CONVERTED_DEFAULT)
226
226
  }
227
227
  else:
228
228
  self._path_values = {
229
229
  k.lower(): v
230
- for k, v in param_value
230
+ for k, v in param_value.items()
231
231
  if v is not None and not v.startswith(_CONVERTED_DEFAULT)
232
232
  }
233
233
  if do_return:
@@ -329,12 +329,12 @@ class ConfigInput:
329
329
  to_return[name] = None
330
330
  flow_cls._flow_state[_FlowState.CONFIGS][name] = None
331
331
  continue
332
- if val.startswith(_CONVERTED_DEFAULT_NO_FILE):
333
- no_default_file.append(name)
334
- continue
335
332
  if val.startswith(_CONVERTED_NO_FILE):
336
333
  no_file.append(name)
337
334
  continue
335
+ if val.startswith(_CONVERTED_DEFAULT_NO_FILE):
336
+ no_default_file.append(name)
337
+ continue
338
338
 
339
339
  val = val[len(_CONVERT_PREFIX) :] # Remove the _CONVERT_PREFIX
340
340
  if val.startswith(_DEFAULT_PREFIX): # Remove the _DEFAULT_PREFIX if needed
@@ -398,7 +398,7 @@ class ConfigInput:
398
398
  return self.process_configs(
399
399
  ctx.obj.flow.name,
400
400
  param.name,
401
- value,
401
+ dict(value),
402
402
  ctx.params["quiet"],
403
403
  ctx.params["datastore"],
404
404
  click_obj=ctx.obj,
@@ -1,9 +1,22 @@
1
- import collections.abc
1
+ import inspect
2
2
  import json
3
+ import collections.abc
4
+ import copy
3
5
  import os
4
6
  import re
5
7
 
6
- from typing import Any, Callable, Dict, List, Optional, Tuple, TYPE_CHECKING, Union
8
+ from typing import (
9
+ Any,
10
+ Callable,
11
+ Dict,
12
+ Iterable,
13
+ Iterator,
14
+ List,
15
+ Optional,
16
+ Tuple,
17
+ TYPE_CHECKING,
18
+ Union,
19
+ )
7
20
 
8
21
 
9
22
  from ..exception import MetaflowException
@@ -54,7 +67,7 @@ def dump_config_values(flow: "FlowSpec"):
54
67
  return {}
55
68
 
56
69
 
57
- class ConfigValue(collections.abc.Mapping):
70
+ class ConfigValue(collections.abc.Mapping, dict):
58
71
  """
59
72
  ConfigValue is a thin wrapper around an arbitrarily nested dictionary-like
60
73
  configuration object. It allows you to access elements of this nested structure
@@ -69,8 +82,67 @@ class ConfigValue(collections.abc.Mapping):
69
82
  # Thin wrapper to allow configuration values to be accessed using a "." notation
70
83
  # as well as a [] notation.
71
84
 
72
- def __init__(self, data: Dict[str, Any]):
73
- self._data = data
85
+ # We inherit from dict to allow the isinstanceof check to work easily and also
86
+ # to provide a simple json dumps functionality.
87
+
88
+ def __init__(self, data: Union["ConfigValue", Dict[str, Any]]):
89
+ self._data = {k: self._construct(v) for k, v in data.items()}
90
+
91
+ # Enable json dumps
92
+ dict.__init__(self, self._data)
93
+
94
+ @classmethod
95
+ def fromkeys(cls, iterable: Iterable, value: Any = None) -> "ConfigValue":
96
+ """
97
+ Creates a new ConfigValue object from the given iterable and value.
98
+
99
+ Parameters
100
+ ----------
101
+ iterable : Iterable
102
+ Iterable to create the ConfigValue from.
103
+ value : Any, optional
104
+ Value to set for each key in the iterable.
105
+
106
+ Returns
107
+ -------
108
+ ConfigValue
109
+ A new ConfigValue object.
110
+ """
111
+ return cls(dict.fromkeys(iterable, value))
112
+
113
+ def to_dict(self) -> Dict[Any, Any]:
114
+ """
115
+ Returns a dictionary representation of this configuration object.
116
+
117
+ Returns
118
+ -------
119
+ Dict[Any, Any]
120
+ Dictionary equivalent of this configuration object.
121
+ """
122
+ return self._to_dict(self._data)
123
+
124
+ def copy(self) -> "ConfigValue":
125
+ return self.__copy__()
126
+
127
+ def clear(self) -> None:
128
+ # Prevent configuration modification
129
+ raise TypeError("ConfigValue is immutable")
130
+
131
+ def update(self, *args, **kwargs) -> None:
132
+ # Prevent configuration modification
133
+ raise TypeError("ConfigValue is immutable")
134
+
135
+ def setdefault(self, key: Any, default: Any = None) -> Any:
136
+ # Prevent configuration modification
137
+ raise TypeError("ConfigValue is immutable")
138
+
139
+ def pop(self, key: Any, default: Any = None) -> Any:
140
+ # Prevent configuration modification
141
+ raise TypeError("ConfigValue is immutable")
142
+
143
+ def popitem(self) -> Tuple[Any, Any]:
144
+ # Prevent configuration modification
145
+ raise TypeError("ConfigValue is immutable")
74
146
 
75
147
  def __getattr__(self, key: str) -> Any:
76
148
  """
@@ -115,33 +187,93 @@ class ConfigValue(collections.abc.Mapping):
115
187
  Any
116
188
  Element of the configuration
117
189
  """
118
- value = self._data[key]
119
- if isinstance(value, dict):
120
- value = ConfigValue(value)
121
- return value
190
+ return self._data[key]
122
191
 
123
- def __len__(self):
192
+ def __setitem__(self, key: Any, value: Any) -> None:
193
+ # Prevent configuration modification
194
+ raise TypeError("ConfigValue is immutable")
195
+
196
+ def __delattr__(self, key) -> None:
197
+ # Prevent configuration modification
198
+ raise TypeError("ConfigValue is immutable")
199
+
200
+ def __delitem__(self, key: Any) -> None:
201
+ # Prevent configuration modification
202
+ raise TypeError("ConfigValue is immutable")
203
+
204
+ def __len__(self) -> int:
124
205
  return len(self._data)
125
206
 
126
- def __iter__(self):
207
+ def __iter__(self) -> Iterator:
127
208
  return iter(self._data)
128
209
 
129
- def __repr__(self):
210
+ def __eq__(self, other: Any) -> bool:
211
+ if isinstance(other, ConfigValue):
212
+ return self._data == other._data
213
+ if isinstance(other, dict):
214
+ return self._data == other
215
+ return False
216
+
217
+ def __ne__(self, other: Any) -> bool:
218
+ return not self.__eq__(other)
219
+
220
+ def __copy__(self) -> "ConfigValue":
221
+ cls = self.__class__
222
+ result = cls.__new__(cls)
223
+ result.__dict__.update({k: copy.copy(v) for k, v in self.__dict__.items()})
224
+ return result
225
+
226
+ def __repr__(self) -> str:
130
227
  return repr(self._data)
131
228
 
132
- def __str__(self):
133
- return json.dumps(self._data)
229
+ def __str__(self) -> str:
230
+ return str(self._data)
134
231
 
135
- def to_dict(self) -> Dict[Any, Any]:
232
+ def __dir__(self) -> Iterable[str]:
233
+ return dir(type(self)) + [k for k in self._data.keys() if ID_PATTERN.match(k)]
234
+
235
+ def __contains__(self, key: Any) -> bool:
236
+ try:
237
+ self[key]
238
+ except KeyError:
239
+ return False
240
+ return True
241
+
242
+ def keys(self):
136
243
  """
137
- Returns a dictionary representation of this configuration object.
244
+ Returns the keys of this configuration object.
138
245
 
139
246
  Returns
140
247
  -------
141
- Dict[Any, Any]
142
- Dictionary equivalent of this configuration object.
248
+ Any
249
+ Keys of this configuration object.
143
250
  """
144
- return dict(self._data)
251
+ return self._data.keys()
252
+
253
+ @classmethod
254
+ def _construct(cls, obj: Any) -> Any:
255
+ # Internal method to construct a ConfigValue so that all mappings internally
256
+ # are also converted to ConfigValue
257
+ if isinstance(obj, ConfigValue):
258
+ v = obj
259
+ elif isinstance(obj, collections.abc.Mapping):
260
+ v = ConfigValue({k: cls._construct(v) for k, v in obj.items()})
261
+ elif isinstance(obj, (list, tuple, set)):
262
+ v = type(obj)([cls._construct(x) for x in obj])
263
+ else:
264
+ v = obj
265
+ return v
266
+
267
+ @classmethod
268
+ def _to_dict(cls, obj: Any) -> Any:
269
+ # Internal method to convert all nested mappings to dicts
270
+ if isinstance(obj, collections.abc.Mapping):
271
+ v = {k: cls._to_dict(v) for k, v in obj.items()}
272
+ elif isinstance(obj, (list, tuple, set)):
273
+ v = type(obj)([cls._to_dict(x) for x in obj])
274
+ else:
275
+ v = obj
276
+ return v
145
277
 
146
278
 
147
279
  class DelayEvaluator(collections.abc.Mapping):
@@ -157,8 +289,9 @@ class DelayEvaluator(collections.abc.Mapping):
157
289
  of _unpacked_delayed_*
158
290
  """
159
291
 
160
- def __init__(self, ex: str):
292
+ def __init__(self, ex: str, saved_globals: Optional[Dict[str, Any]] = None):
161
293
  self._config_expr = ex
294
+ self._globals = saved_globals
162
295
  if ID_PATTERN.match(self._config_expr):
163
296
  # This is a variable only so allow things like config_expr("config").var
164
297
  self._is_var_only = True
@@ -166,6 +299,21 @@ class DelayEvaluator(collections.abc.Mapping):
166
299
  else:
167
300
  self._is_var_only = False
168
301
  self._access = None
302
+ self._cached_expr = None
303
+
304
+ def __copy__(self):
305
+ c = DelayEvaluator(self._config_expr)
306
+ c._access = self._access.copy() if self._access is not None else None
307
+ # Globals are not copied -- always kept as a reference
308
+ return c
309
+
310
+ def __deepcopy__(self, memo):
311
+ c = DelayEvaluator(self._config_expr)
312
+ c._access = (
313
+ copy.deepcopy(self._access, memo) if self._access is not None else None
314
+ )
315
+ # Globals are not copied -- always kept as a reference
316
+ return c
169
317
 
170
318
  def __iter__(self):
171
319
  yield "%s%d" % (UNPACK_KEY, id(self))
@@ -175,8 +323,15 @@ class DelayEvaluator(collections.abc.Mapping):
175
323
  return self
176
324
  if self._access is None:
177
325
  raise KeyError(key)
178
- self._access.append(key)
179
- return self
326
+
327
+ # Make a copy so that we can support something like
328
+ # foo = delay_evaluator["blah"]
329
+ # bar = delay_evaluator["baz"]
330
+ # and don't end up with a access list that contains both "blah" and "baz"
331
+ c = self.__copy__()
332
+ c._access.append(key)
333
+ c._cached_expr = None
334
+ return c
180
335
 
181
336
  def __len__(self):
182
337
  return 1
@@ -184,8 +339,10 @@ class DelayEvaluator(collections.abc.Mapping):
184
339
  def __getattr__(self, name):
185
340
  if self._access is None:
186
341
  raise AttributeError(name)
187
- self._access.append(name)
188
- return self
342
+ c = self.__copy__()
343
+ c._access.append(name)
344
+ c._cached_expr = None
345
+ return c
189
346
 
190
347
  def __call__(self, ctx=None, deploy_time=False):
191
348
  from ..flowspec import _FlowState # Prevent circular import
@@ -199,7 +356,9 @@ class DelayEvaluator(collections.abc.Mapping):
199
356
  "Config object can only be used directly in the FlowSpec defining them "
200
357
  "(or their flow decorators)."
201
358
  )
202
- if self._access is not None:
359
+ if self._cached_expr is not None:
360
+ to_eval_expr = self._cached_expr
361
+ elif self._access is not None:
203
362
  # Build the final expression by adding all the fields in access as . fields
204
363
  access_list = [self._config_expr]
205
364
  for a in self._access:
@@ -212,27 +371,24 @@ class DelayEvaluator(collections.abc.Mapping):
212
371
  raise MetaflowException(
213
372
  "Field '%s' of type '%s' is not supported" % (str(a), type(a))
214
373
  )
215
- self._config_expr = ".".join(access_list)
374
+ to_eval_expr = self._cached_expr = ".".join(access_list)
375
+ else:
376
+ to_eval_expr = self._cached_expr = self._config_expr
216
377
  # Evaluate the expression setting the config values as local variables
217
378
  try:
218
379
  return eval(
219
- self._config_expr,
220
- globals(),
380
+ to_eval_expr,
381
+ self._globals or globals(),
221
382
  {
222
383
  k: ConfigValue(v)
223
384
  for k, v in flow_cls._flow_state.get(_FlowState.CONFIGS, {}).items()
224
385
  },
225
386
  )
226
387
  except NameError as e:
227
- potential_config_name = self._config_expr.split(".")[0]
228
- if potential_config_name not in flow_cls._flow_state.get(
229
- _FlowState.CONFIGS, {}
230
- ):
231
- raise MetaflowException(
232
- "Config '%s' not found in the flow (maybe not required and not "
233
- "provided?)" % potential_config_name
234
- ) from e
235
- raise
388
+ raise MetaflowException(
389
+ "Config expression '%s' could not be evaluated: %s"
390
+ % (to_eval_expr, str(e))
391
+ ) from e
236
392
 
237
393
 
238
394
  def config_expr(expr: str) -> DelayEvaluator:
@@ -262,7 +418,10 @@ def config_expr(expr: str) -> DelayEvaluator:
262
418
  expr : str
263
419
  Expression using the config values.
264
420
  """
265
- return DelayEvaluator(expr)
421
+ # Get globals where the expression is defined so that the user can use
422
+ # something like `config_expr("my_func()")` in the expression.
423
+ parent_globals = inspect.currentframe().f_back.f_globals
424
+ return DelayEvaluator(expr, saved_globals=parent_globals)
266
425
 
267
426
 
268
427
  class Config(Parameter, collections.abc.Mapping):
@@ -385,23 +544,30 @@ class Config(Parameter, collections.abc.Mapping):
385
544
  return DelayEvaluator(self.name.lower())[key]
386
545
 
387
546
 
388
- def resolve_delayed_evaluator(v: Any, ignore_errors: bool = False) -> Any:
547
+ def resolve_delayed_evaluator(
548
+ v: Any, ignore_errors: bool = False, to_dict: bool = False
549
+ ) -> Any:
389
550
  # NOTE: We don't ignore errors in downstream calls because we want to have either
390
551
  # all or nothing for the top-level call by the user.
391
552
  try:
392
553
  if isinstance(v, DelayEvaluator):
393
- return v()
554
+ to_return = v()
555
+ if to_dict and isinstance(to_return, ConfigValue):
556
+ to_return = to_return.to_dict()
557
+ return to_return
394
558
  if isinstance(v, dict):
395
559
  return {
396
- resolve_delayed_evaluator(k): resolve_delayed_evaluator(v)
560
+ resolve_delayed_evaluator(
561
+ k, to_dict=to_dict
562
+ ): resolve_delayed_evaluator(v, to_dict=to_dict)
397
563
  for k, v in v.items()
398
564
  }
399
565
  if isinstance(v, list):
400
- return [resolve_delayed_evaluator(x) for x in v]
566
+ return [resolve_delayed_evaluator(x, to_dict=to_dict) for x in v]
401
567
  if isinstance(v, tuple):
402
- return tuple(resolve_delayed_evaluator(x) for x in v)
568
+ return tuple(resolve_delayed_evaluator(x, to_dict=to_dict) for x in v)
403
569
  if isinstance(v, set):
404
- return {resolve_delayed_evaluator(x) for x in v}
570
+ return {resolve_delayed_evaluator(x, to_dict=to_dict) for x in v}
405
571
  return v
406
572
  except Exception as e:
407
573
  if ignore_errors:
@@ -426,7 +592,7 @@ def unpack_delayed_evaluator(
426
592
  else:
427
593
  # k.startswith(UNPACK_KEY)
428
594
  try:
429
- new_vals = resolve_delayed_evaluator(v)
595
+ new_vals = resolve_delayed_evaluator(v, to_dict=True)
430
596
  new_keys.extend(new_vals.keys())
431
597
  result.update(new_vals)
432
598
  except Exception as e:
metaflow/util.py CHANGED
@@ -418,7 +418,7 @@ def to_pascalcase(obj):
418
418
  if isinstance(obj, dict):
419
419
  res = obj.__class__()
420
420
  for k in obj:
421
- res[re.sub("([a-zA-Z])", lambda x: x.groups()[0].upper(), k, 1)] = (
421
+ res[re.sub("([a-zA-Z])", lambda x: x.groups()[0].upper(), k, count=1)] = (
422
422
  to_pascalcase(obj[k])
423
423
  )
424
424
  elif isinstance(obj, (list, set, tuple)):
@@ -467,7 +467,4 @@ def to_pod(value):
467
467
  return str(value)
468
468
 
469
469
 
470
- if sys.version_info[:2] > (3, 5):
471
- from metaflow._vendor.packaging.version import parse as version_parse
472
- else:
473
- from distutils.version import LooseVersion as version_parse
470
+ from metaflow._vendor.packaging.version import parse as version_parse
metaflow/vendor.py CHANGED
@@ -11,7 +11,6 @@ WHITELIST = {
11
11
  "README.txt",
12
12
  "__init__.py",
13
13
  "vendor_any.txt",
14
- "vendor_v3_5.txt",
15
14
  "vendor_v3_6.txt",
16
15
  "vendor_v3_7.txt",
17
16
  "pip.LICENSE",
metaflow/version.py CHANGED
@@ -1 +1 @@
1
- metaflow_version = "2.15.13"
1
+ metaflow_version = "2.15.15"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metaflow
3
- Version: 2.15.13
3
+ Version: 2.15.15
4
4
  Summary: Metaflow: More AI and ML, Less Engineering
5
5
  Author: Metaflow Developers
6
6
  Author-email: help@metaflow.org
@@ -26,7 +26,7 @@ License-File: LICENSE
26
26
  Requires-Dist: requests
27
27
  Requires-Dist: boto3
28
28
  Provides-Extra: stubs
29
- Requires-Dist: metaflow-stubs==2.15.13; extra == "stubs"
29
+ Requires-Dist: metaflow-stubs==2.15.15; extra == "stubs"
30
30
  Dynamic: author
31
31
  Dynamic: author-email
32
32
  Dynamic: classifier