ob-metaflow 2.11.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.
Files changed (289) hide show
  1. metaflow/R.py +10 -7
  2. metaflow/__init__.py +40 -25
  3. metaflow/_vendor/imghdr/__init__.py +186 -0
  4. metaflow/_vendor/importlib_metadata/__init__.py +1063 -0
  5. metaflow/_vendor/importlib_metadata/_adapters.py +68 -0
  6. metaflow/_vendor/importlib_metadata/_collections.py +30 -0
  7. metaflow/_vendor/importlib_metadata/_compat.py +71 -0
  8. metaflow/_vendor/importlib_metadata/_functools.py +104 -0
  9. metaflow/_vendor/importlib_metadata/_itertools.py +73 -0
  10. metaflow/_vendor/importlib_metadata/_meta.py +48 -0
  11. metaflow/_vendor/importlib_metadata/_text.py +99 -0
  12. metaflow/_vendor/importlib_metadata/py.typed +0 -0
  13. metaflow/_vendor/typeguard/__init__.py +48 -0
  14. metaflow/_vendor/typeguard/_checkers.py +1070 -0
  15. metaflow/_vendor/typeguard/_config.py +108 -0
  16. metaflow/_vendor/typeguard/_decorators.py +233 -0
  17. metaflow/_vendor/typeguard/_exceptions.py +42 -0
  18. metaflow/_vendor/typeguard/_functions.py +308 -0
  19. metaflow/_vendor/typeguard/_importhook.py +213 -0
  20. metaflow/_vendor/typeguard/_memo.py +48 -0
  21. metaflow/_vendor/typeguard/_pytest_plugin.py +127 -0
  22. metaflow/_vendor/typeguard/_suppression.py +86 -0
  23. metaflow/_vendor/typeguard/_transformer.py +1229 -0
  24. metaflow/_vendor/typeguard/_union_transformer.py +55 -0
  25. metaflow/_vendor/typeguard/_utils.py +173 -0
  26. metaflow/_vendor/typeguard/py.typed +0 -0
  27. metaflow/_vendor/typing_extensions.py +3641 -0
  28. metaflow/_vendor/v3_7/importlib_metadata/__init__.py +1063 -0
  29. metaflow/_vendor/v3_7/importlib_metadata/_adapters.py +68 -0
  30. metaflow/_vendor/v3_7/importlib_metadata/_collections.py +30 -0
  31. metaflow/_vendor/v3_7/importlib_metadata/_compat.py +71 -0
  32. metaflow/_vendor/v3_7/importlib_metadata/_functools.py +104 -0
  33. metaflow/_vendor/v3_7/importlib_metadata/_itertools.py +73 -0
  34. metaflow/_vendor/v3_7/importlib_metadata/_meta.py +48 -0
  35. metaflow/_vendor/v3_7/importlib_metadata/_text.py +99 -0
  36. metaflow/_vendor/v3_7/importlib_metadata/py.typed +0 -0
  37. metaflow/_vendor/v3_7/typeguard/__init__.py +48 -0
  38. metaflow/_vendor/v3_7/typeguard/_checkers.py +906 -0
  39. metaflow/_vendor/v3_7/typeguard/_config.py +108 -0
  40. metaflow/_vendor/v3_7/typeguard/_decorators.py +237 -0
  41. metaflow/_vendor/v3_7/typeguard/_exceptions.py +42 -0
  42. metaflow/_vendor/v3_7/typeguard/_functions.py +310 -0
  43. metaflow/_vendor/v3_7/typeguard/_importhook.py +213 -0
  44. metaflow/_vendor/v3_7/typeguard/_memo.py +48 -0
  45. metaflow/_vendor/v3_7/typeguard/_pytest_plugin.py +100 -0
  46. metaflow/_vendor/v3_7/typeguard/_suppression.py +88 -0
  47. metaflow/_vendor/v3_7/typeguard/_transformer.py +1207 -0
  48. metaflow/_vendor/v3_7/typeguard/_union_transformer.py +54 -0
  49. metaflow/_vendor/v3_7/typeguard/_utils.py +169 -0
  50. metaflow/_vendor/v3_7/typeguard/py.typed +0 -0
  51. metaflow/_vendor/v3_7/typing_extensions.py +3072 -0
  52. metaflow/_vendor/yaml/__init__.py +427 -0
  53. metaflow/_vendor/yaml/composer.py +139 -0
  54. metaflow/_vendor/yaml/constructor.py +748 -0
  55. metaflow/_vendor/yaml/cyaml.py +101 -0
  56. metaflow/_vendor/yaml/dumper.py +62 -0
  57. metaflow/_vendor/yaml/emitter.py +1137 -0
  58. metaflow/_vendor/yaml/error.py +75 -0
  59. metaflow/_vendor/yaml/events.py +86 -0
  60. metaflow/_vendor/yaml/loader.py +63 -0
  61. metaflow/_vendor/yaml/nodes.py +49 -0
  62. metaflow/_vendor/yaml/parser.py +589 -0
  63. metaflow/_vendor/yaml/reader.py +185 -0
  64. metaflow/_vendor/yaml/representer.py +389 -0
  65. metaflow/_vendor/yaml/resolver.py +227 -0
  66. metaflow/_vendor/yaml/scanner.py +1435 -0
  67. metaflow/_vendor/yaml/serializer.py +111 -0
  68. metaflow/_vendor/yaml/tokens.py +104 -0
  69. metaflow/cards.py +5 -0
  70. metaflow/cli.py +331 -785
  71. metaflow/cli_args.py +17 -0
  72. metaflow/cli_components/__init__.py +0 -0
  73. metaflow/cli_components/dump_cmd.py +96 -0
  74. metaflow/cli_components/init_cmd.py +52 -0
  75. metaflow/cli_components/run_cmds.py +546 -0
  76. metaflow/cli_components/step_cmd.py +334 -0
  77. metaflow/cli_components/utils.py +140 -0
  78. metaflow/client/__init__.py +1 -0
  79. metaflow/client/core.py +467 -73
  80. metaflow/client/filecache.py +75 -35
  81. metaflow/clone_util.py +7 -1
  82. metaflow/cmd/code/__init__.py +231 -0
  83. metaflow/cmd/develop/stub_generator.py +756 -288
  84. metaflow/cmd/develop/stubs.py +12 -28
  85. metaflow/cmd/main_cli.py +6 -4
  86. metaflow/cmd/make_wrapper.py +78 -0
  87. metaflow/datastore/__init__.py +1 -0
  88. metaflow/datastore/content_addressed_store.py +41 -10
  89. metaflow/datastore/datastore_set.py +11 -2
  90. metaflow/datastore/flow_datastore.py +156 -10
  91. metaflow/datastore/spin_datastore.py +91 -0
  92. metaflow/datastore/task_datastore.py +154 -39
  93. metaflow/debug.py +5 -0
  94. metaflow/decorators.py +404 -78
  95. metaflow/exception.py +8 -2
  96. metaflow/extension_support/__init__.py +527 -376
  97. metaflow/extension_support/_empty_file.py +2 -2
  98. metaflow/extension_support/plugins.py +49 -31
  99. metaflow/flowspec.py +482 -33
  100. metaflow/graph.py +210 -42
  101. metaflow/includefile.py +84 -40
  102. metaflow/lint.py +141 -22
  103. metaflow/meta_files.py +13 -0
  104. metaflow/{metadata → metadata_provider}/heartbeat.py +24 -8
  105. metaflow/{metadata → metadata_provider}/metadata.py +86 -1
  106. metaflow/metaflow_config.py +175 -28
  107. metaflow/metaflow_config_funcs.py +51 -3
  108. metaflow/metaflow_current.py +4 -10
  109. metaflow/metaflow_environment.py +139 -53
  110. metaflow/metaflow_git.py +115 -0
  111. metaflow/metaflow_profile.py +18 -0
  112. metaflow/metaflow_version.py +150 -66
  113. metaflow/mflog/__init__.py +4 -3
  114. metaflow/mflog/save_logs.py +2 -2
  115. metaflow/multicore_utils.py +31 -14
  116. metaflow/package/__init__.py +673 -0
  117. metaflow/packaging_sys/__init__.py +880 -0
  118. metaflow/packaging_sys/backend.py +128 -0
  119. metaflow/packaging_sys/distribution_support.py +153 -0
  120. metaflow/packaging_sys/tar_backend.py +99 -0
  121. metaflow/packaging_sys/utils.py +54 -0
  122. metaflow/packaging_sys/v1.py +527 -0
  123. metaflow/parameters.py +149 -28
  124. metaflow/plugins/__init__.py +74 -5
  125. metaflow/plugins/airflow/airflow.py +40 -25
  126. metaflow/plugins/airflow/airflow_cli.py +22 -5
  127. metaflow/plugins/airflow/airflow_decorator.py +1 -1
  128. metaflow/plugins/airflow/airflow_utils.py +5 -3
  129. metaflow/plugins/airflow/sensors/base_sensor.py +4 -4
  130. metaflow/plugins/airflow/sensors/external_task_sensor.py +2 -2
  131. metaflow/plugins/airflow/sensors/s3_sensor.py +2 -2
  132. metaflow/plugins/argo/argo_client.py +78 -33
  133. metaflow/plugins/argo/argo_events.py +6 -6
  134. metaflow/plugins/argo/argo_workflows.py +2410 -527
  135. metaflow/plugins/argo/argo_workflows_cli.py +571 -121
  136. metaflow/plugins/argo/argo_workflows_decorator.py +43 -12
  137. metaflow/plugins/argo/argo_workflows_deployer.py +106 -0
  138. metaflow/plugins/argo/argo_workflows_deployer_objects.py +453 -0
  139. metaflow/plugins/argo/capture_error.py +73 -0
  140. metaflow/plugins/argo/conditional_input_paths.py +35 -0
  141. metaflow/plugins/argo/exit_hooks.py +209 -0
  142. metaflow/plugins/argo/jobset_input_paths.py +15 -0
  143. metaflow/plugins/argo/param_val.py +19 -0
  144. metaflow/plugins/aws/aws_client.py +10 -3
  145. metaflow/plugins/aws/aws_utils.py +55 -2
  146. metaflow/plugins/aws/batch/batch.py +72 -5
  147. metaflow/plugins/aws/batch/batch_cli.py +33 -10
  148. metaflow/plugins/aws/batch/batch_client.py +4 -3
  149. metaflow/plugins/aws/batch/batch_decorator.py +102 -35
  150. metaflow/plugins/aws/secrets_manager/aws_secrets_manager_secrets_provider.py +13 -10
  151. metaflow/plugins/aws/step_functions/dynamo_db_client.py +0 -3
  152. metaflow/plugins/aws/step_functions/production_token.py +1 -1
  153. metaflow/plugins/aws/step_functions/step_functions.py +65 -8
  154. metaflow/plugins/aws/step_functions/step_functions_cli.py +101 -7
  155. metaflow/plugins/aws/step_functions/step_functions_decorator.py +1 -2
  156. metaflow/plugins/aws/step_functions/step_functions_deployer.py +97 -0
  157. metaflow/plugins/aws/step_functions/step_functions_deployer_objects.py +264 -0
  158. metaflow/plugins/azure/azure_exceptions.py +1 -1
  159. metaflow/plugins/azure/azure_secret_manager_secrets_provider.py +240 -0
  160. metaflow/plugins/azure/azure_tail.py +1 -1
  161. metaflow/plugins/azure/includefile_support.py +2 -0
  162. metaflow/plugins/cards/card_cli.py +66 -30
  163. metaflow/plugins/cards/card_creator.py +25 -1
  164. metaflow/plugins/cards/card_datastore.py +21 -49
  165. metaflow/plugins/cards/card_decorator.py +132 -8
  166. metaflow/plugins/cards/card_modules/basic.py +112 -17
  167. metaflow/plugins/cards/card_modules/bundle.css +1 -1
  168. metaflow/plugins/cards/card_modules/card.py +16 -1
  169. metaflow/plugins/cards/card_modules/chevron/renderer.py +1 -1
  170. metaflow/plugins/cards/card_modules/components.py +665 -28
  171. metaflow/plugins/cards/card_modules/convert_to_native_type.py +36 -7
  172. metaflow/plugins/cards/card_modules/json_viewer.py +232 -0
  173. metaflow/plugins/cards/card_modules/main.css +1 -0
  174. metaflow/plugins/cards/card_modules/main.js +68 -49
  175. metaflow/plugins/cards/card_modules/renderer_tools.py +1 -0
  176. metaflow/plugins/cards/card_modules/test_cards.py +26 -12
  177. metaflow/plugins/cards/card_server.py +39 -14
  178. metaflow/plugins/cards/component_serializer.py +2 -9
  179. metaflow/plugins/cards/metadata.py +22 -0
  180. metaflow/plugins/catch_decorator.py +9 -0
  181. metaflow/plugins/datastores/azure_storage.py +10 -1
  182. metaflow/plugins/datastores/gs_storage.py +6 -2
  183. metaflow/plugins/datastores/local_storage.py +12 -6
  184. metaflow/plugins/datastores/spin_storage.py +12 -0
  185. metaflow/plugins/datatools/local.py +2 -0
  186. metaflow/plugins/datatools/s3/s3.py +126 -75
  187. metaflow/plugins/datatools/s3/s3op.py +254 -121
  188. metaflow/plugins/env_escape/__init__.py +3 -3
  189. metaflow/plugins/env_escape/client_modules.py +102 -72
  190. metaflow/plugins/env_escape/server.py +7 -0
  191. metaflow/plugins/env_escape/stub.py +24 -5
  192. metaflow/plugins/events_decorator.py +343 -185
  193. metaflow/plugins/exit_hook/__init__.py +0 -0
  194. metaflow/plugins/exit_hook/exit_hook_decorator.py +46 -0
  195. metaflow/plugins/exit_hook/exit_hook_script.py +52 -0
  196. metaflow/plugins/gcp/__init__.py +1 -1
  197. metaflow/plugins/gcp/gcp_secret_manager_secrets_provider.py +11 -6
  198. metaflow/plugins/gcp/gs_tail.py +10 -6
  199. metaflow/plugins/gcp/includefile_support.py +3 -0
  200. metaflow/plugins/kubernetes/kube_utils.py +108 -0
  201. metaflow/plugins/kubernetes/kubernetes.py +411 -130
  202. metaflow/plugins/kubernetes/kubernetes_cli.py +168 -36
  203. metaflow/plugins/kubernetes/kubernetes_client.py +104 -2
  204. metaflow/plugins/kubernetes/kubernetes_decorator.py +246 -88
  205. metaflow/plugins/kubernetes/kubernetes_job.py +253 -581
  206. metaflow/plugins/kubernetes/kubernetes_jobsets.py +1071 -0
  207. metaflow/plugins/kubernetes/spot_metadata_cli.py +69 -0
  208. metaflow/plugins/kubernetes/spot_monitor_sidecar.py +109 -0
  209. metaflow/plugins/logs_cli.py +359 -0
  210. metaflow/plugins/{metadata → metadata_providers}/local.py +144 -84
  211. metaflow/plugins/{metadata → metadata_providers}/service.py +103 -26
  212. metaflow/plugins/metadata_providers/spin.py +16 -0
  213. metaflow/plugins/package_cli.py +36 -24
  214. metaflow/plugins/parallel_decorator.py +128 -11
  215. metaflow/plugins/parsers.py +16 -0
  216. metaflow/plugins/project_decorator.py +51 -5
  217. metaflow/plugins/pypi/bootstrap.py +357 -105
  218. metaflow/plugins/pypi/conda_decorator.py +82 -81
  219. metaflow/plugins/pypi/conda_environment.py +187 -52
  220. metaflow/plugins/pypi/micromamba.py +157 -47
  221. metaflow/plugins/pypi/parsers.py +268 -0
  222. metaflow/plugins/pypi/pip.py +88 -13
  223. metaflow/plugins/pypi/pypi_decorator.py +37 -1
  224. metaflow/plugins/pypi/utils.py +48 -2
  225. metaflow/plugins/resources_decorator.py +2 -2
  226. metaflow/plugins/secrets/__init__.py +3 -0
  227. metaflow/plugins/secrets/secrets_decorator.py +26 -181
  228. metaflow/plugins/secrets/secrets_func.py +49 -0
  229. metaflow/plugins/secrets/secrets_spec.py +101 -0
  230. metaflow/plugins/secrets/utils.py +74 -0
  231. metaflow/plugins/tag_cli.py +4 -7
  232. metaflow/plugins/test_unbounded_foreach_decorator.py +41 -6
  233. metaflow/plugins/timeout_decorator.py +3 -3
  234. metaflow/plugins/uv/__init__.py +0 -0
  235. metaflow/plugins/uv/bootstrap.py +128 -0
  236. metaflow/plugins/uv/uv_environment.py +72 -0
  237. metaflow/procpoll.py +1 -1
  238. metaflow/pylint_wrapper.py +5 -1
  239. metaflow/runner/__init__.py +0 -0
  240. metaflow/runner/click_api.py +717 -0
  241. metaflow/runner/deployer.py +470 -0
  242. metaflow/runner/deployer_impl.py +201 -0
  243. metaflow/runner/metaflow_runner.py +714 -0
  244. metaflow/runner/nbdeploy.py +132 -0
  245. metaflow/runner/nbrun.py +225 -0
  246. metaflow/runner/subprocess_manager.py +650 -0
  247. metaflow/runner/utils.py +335 -0
  248. metaflow/runtime.py +1078 -260
  249. metaflow/sidecar/sidecar_worker.py +1 -1
  250. metaflow/system/__init__.py +5 -0
  251. metaflow/system/system_logger.py +85 -0
  252. metaflow/system/system_monitor.py +108 -0
  253. metaflow/system/system_utils.py +19 -0
  254. metaflow/task.py +521 -225
  255. metaflow/tracing/__init__.py +7 -7
  256. metaflow/tracing/span_exporter.py +31 -38
  257. metaflow/tracing/tracing_modules.py +38 -43
  258. metaflow/tuple_util.py +27 -0
  259. metaflow/user_configs/__init__.py +0 -0
  260. metaflow/user_configs/config_options.py +563 -0
  261. metaflow/user_configs/config_parameters.py +598 -0
  262. metaflow/user_decorators/__init__.py +0 -0
  263. metaflow/user_decorators/common.py +144 -0
  264. metaflow/user_decorators/mutable_flow.py +512 -0
  265. metaflow/user_decorators/mutable_step.py +424 -0
  266. metaflow/user_decorators/user_flow_decorator.py +264 -0
  267. metaflow/user_decorators/user_step_decorator.py +749 -0
  268. metaflow/util.py +243 -27
  269. metaflow/vendor.py +23 -7
  270. metaflow/version.py +1 -1
  271. ob_metaflow-2.19.7.1rc0.data/data/share/metaflow/devtools/Makefile +355 -0
  272. ob_metaflow-2.19.7.1rc0.data/data/share/metaflow/devtools/Tiltfile +726 -0
  273. ob_metaflow-2.19.7.1rc0.data/data/share/metaflow/devtools/pick_services.sh +105 -0
  274. ob_metaflow-2.19.7.1rc0.dist-info/METADATA +87 -0
  275. ob_metaflow-2.19.7.1rc0.dist-info/RECORD +445 -0
  276. {ob_metaflow-2.11.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/WHEEL +1 -1
  277. {ob_metaflow-2.11.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/entry_points.txt +1 -0
  278. metaflow/_vendor/v3_5/__init__.py +0 -1
  279. metaflow/_vendor/v3_5/importlib_metadata/__init__.py +0 -644
  280. metaflow/_vendor/v3_5/importlib_metadata/_compat.py +0 -152
  281. metaflow/package.py +0 -188
  282. ob_metaflow-2.11.13.1.dist-info/METADATA +0 -85
  283. ob_metaflow-2.11.13.1.dist-info/RECORD +0 -308
  284. /metaflow/_vendor/{v3_5/zipp.py → zipp.py} +0 -0
  285. /metaflow/{metadata → metadata_provider}/__init__.py +0 -0
  286. /metaflow/{metadata → metadata_provider}/util.py +0 -0
  287. /metaflow/plugins/{metadata → metadata_providers}/__init__.py +0 -0
  288. {ob_metaflow-2.11.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info/licenses}/LICENSE +0 -0
  289. {ob_metaflow-2.11.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,18 @@
1
+ import functools
1
2
  import json
2
3
  import os
4
+ import re
5
+ import shutil
3
6
  import subprocess
4
7
  import tempfile
8
+ import time
5
9
 
10
+ from metaflow.debug import debug
6
11
  from metaflow.exception import MetaflowException
7
12
  from metaflow.util import which
8
13
 
9
- from .utils import conda_platform
14
+ from .utils import MICROMAMBA_MIRROR_URL, MICROMAMBA_URL, conda_platform
15
+ from threading import Lock
10
16
 
11
17
 
12
18
  class MicromambaException(MetaflowException):
@@ -19,8 +25,13 @@ class MicromambaException(MetaflowException):
19
25
  super(MicromambaException, self).__init__(msg)
20
26
 
21
27
 
28
+ GLIBC_VERSION = os.environ.get("CONDA_OVERRIDE_GLIBC", "2.38")
29
+
30
+ _double_equal_match = re.compile("==(?=[<=>!~])")
31
+
32
+
22
33
  class Micromamba(object):
23
- def __init__(self):
34
+ def __init__(self, logger=None, force_rebuild=False):
24
35
  # micromamba is a tiny version of the mamba package manager and comes with
25
36
  # metaflow specific performance enhancements.
26
37
 
@@ -29,26 +40,53 @@ class Micromamba(object):
29
40
  _home = os.environ.get("METAFLOW_TOKEN_HOME")
30
41
  else:
31
42
  _home = os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
32
- _path_to_hidden_micromamba = os.path.join(
43
+ self._path_to_hidden_micromamba = os.path.join(
33
44
  os.path.expanduser(_home),
34
45
  "micromamba",
35
46
  )
36
- self.bin = (
47
+
48
+ if logger:
49
+ self.logger = logger
50
+ else:
51
+ self.logger = lambda *args, **kwargs: None # No-op logger if not provided
52
+
53
+ self._bin = (
37
54
  which(os.environ.get("METAFLOW_PATH_TO_MICROMAMBA") or "micromamba")
38
55
  or which("./micromamba") # to support remote execution
39
56
  or which("./bin/micromamba")
40
- or which(os.path.join(_path_to_hidden_micromamba, "bin/micromamba"))
57
+ or which(os.path.join(self._path_to_hidden_micromamba, "bin/micromamba"))
41
58
  )
42
- if self.bin is None:
59
+
60
+ # We keep a mutex as environments are resolved in parallel,
61
+ # which causes a race condition in case micromamba needs to be installed first.
62
+ self.install_mutex = Lock()
63
+
64
+ self.force_rebuild = force_rebuild
65
+
66
+ @property
67
+ def bin(self) -> str:
68
+ "Defer installing Micromamba until when the binary path is actually requested"
69
+ if self._bin is not None:
70
+ return self._bin
71
+ with self.install_mutex:
72
+ # another check as micromamba might have been installed when the mutex is released.
73
+ if self._bin is not None:
74
+ return self._bin
75
+
43
76
  # Install Micromamba on the fly.
44
77
  # TODO: Make this optional at some point.
45
- _install_micromamba(_path_to_hidden_micromamba)
46
- self.bin = which(os.path.join(_path_to_hidden_micromamba, "bin/micromamba"))
78
+ debug.conda_exec("No Micromamba binary found. Installing micromamba")
79
+ _install_micromamba(self._path_to_hidden_micromamba)
80
+ self._bin = which(
81
+ os.path.join(self._path_to_hidden_micromamba, "bin/micromamba")
82
+ )
83
+
84
+ if self._bin is None:
85
+ msg = "No installation for *Micromamba* found.\n"
86
+ msg += "Visit https://mamba.readthedocs.io/en/latest/micromamba-installation.html for installation instructions."
87
+ raise MetaflowException(msg)
47
88
 
48
- if self.bin is None:
49
- msg = "No installation for *Micromamba* found.\n"
50
- msg += "Visit https://mamba.readthedocs.io/en/latest/micromamba-installation.html for installation instructions."
51
- raise MetaflowException(msg)
89
+ return self._bin
52
90
 
53
91
  def solve(self, id_, packages, python, platform):
54
92
  # Performance enhancements
@@ -65,11 +103,15 @@ class Micromamba(object):
65
103
  # environment
66
104
  # 4. Multiple solves can progress at the same time while relying on the same
67
105
  # index
106
+ debug.conda_exec("Solving packages for conda environment %s" % id_)
68
107
  with tempfile.TemporaryDirectory() as tmp_dir:
69
108
  env = {
70
109
  "MAMBA_ADD_PIP_AS_PYTHON_DEPENDENCY": "true",
71
110
  "CONDA_SUBDIR": platform,
72
111
  # "CONDA_UNSATISFIABLE_HINTS_CHECK_DEPTH": "0" # https://github.com/conda/conda/issues/9862
112
+ # Add a default glibc version for linux-64 environments (ignored for other platforms)
113
+ # TODO: Make the version configurable
114
+ "CONDA_OVERRIDE_GLIBC": GLIBC_VERSION,
73
115
  }
74
116
  cmd = [
75
117
  "create",
@@ -78,6 +120,7 @@ class Micromamba(object):
78
120
  "--dry-run",
79
121
  "--no-extra-safety-checks",
80
122
  "--repodata-ttl=86400",
123
+ "--safety-checks=disabled",
81
124
  "--retry-clean-cache",
82
125
  "--prefix=%s/prefix" % tmp_dir,
83
126
  ]
@@ -86,15 +129,17 @@ class Micromamba(object):
86
129
  cmd.append("--channel=%s" % channel)
87
130
 
88
131
  for package, version in packages.items():
89
- cmd.append("%s==%s" % (package, version))
132
+ version_string = "%s==%s" % (package, version)
133
+ cmd.append(_double_equal_match.sub("", version_string))
90
134
  if python:
91
135
  cmd.append("python==%s" % python)
92
136
  # TODO: Ensure a human readable message is returned when the environment
93
137
  # can't be resolved for any and all reasons.
94
- return [
138
+ solved_packages = [
95
139
  {k: v for k, v in item.items() if k in ["url"]}
96
140
  for item in self._call(cmd, env)["actions"]["LINK"]
97
141
  ]
142
+ return solved_packages
98
143
 
99
144
  def download(self, id_, packages, python, platform):
100
145
  # Unfortunately all the packages need to be catalogued in package cache
@@ -103,8 +148,6 @@ class Micromamba(object):
103
148
  # Micromamba is painfully slow in determining if many packages are infact
104
149
  # already cached. As a perf heuristic, we check if the environment already
105
150
  # exists to short circuit package downloads.
106
- if self.path_to_environment(id_, platform):
107
- return
108
151
 
109
152
  prefix = "{env_dirs}/{keyword}/{platform}/{id}".format(
110
153
  env_dirs=self.info()["envs_dirs"][0],
@@ -112,14 +155,25 @@ class Micromamba(object):
112
155
  keyword="metaflow", # indicates metaflow generated environment
113
156
  id=id_,
114
157
  )
158
+ # If we are forcing a rebuild of the environment, we make sure to remove existing files beforehand.
159
+ # This is to ensure that no irrelevant packages get bundled relative to the resolved environment.
160
+ # NOTE: download always happens before create, so we want to do the cleanup here instead.
161
+ if self.force_rebuild:
162
+ shutil.rmtree(self.path_to_environment(id_, platform), ignore_errors=True)
115
163
 
116
- # Another forced perf heuristic to skip cross-platform downloads.
164
+ # cheap check
117
165
  if os.path.exists(f"{prefix}/fake.done"):
118
166
  return
119
167
 
168
+ # somewhat expensive check
169
+ if self.path_to_environment(id_, platform):
170
+ return
171
+
172
+ debug.conda_exec("Downloading packages for conda environment %s" % id_)
120
173
  with tempfile.TemporaryDirectory() as tmp_dir:
121
174
  env = {
122
175
  "CONDA_SUBDIR": platform,
176
+ "CONDA_OVERRIDE_GLIBC": GLIBC_VERSION,
123
177
  }
124
178
  cmd = [
125
179
  "create",
@@ -148,6 +202,7 @@ class Micromamba(object):
148
202
  if platform != self.platform() or self.path_to_environment(id_, platform):
149
203
  return
150
204
 
205
+ debug.conda_exec("Creating local Conda environment %s" % id_)
151
206
  prefix = "{env_dirs}/{keyword}/{platform}/{id}".format(
152
207
  env_dirs=self.info()["envs_dirs"][0],
153
208
  platform=platform,
@@ -159,6 +214,7 @@ class Micromamba(object):
159
214
  # use hardlinks when possible, otherwise copy files
160
215
  # disabled for now since it adds to environment creation latencies
161
216
  "CONDA_ALLOW_SOFTLINKS": "0",
217
+ "CONDA_OVERRIDE_GLIBC": GLIBC_VERSION,
162
218
  }
163
219
  cmd = [
164
220
  "create",
@@ -174,6 +230,7 @@ class Micromamba(object):
174
230
  cmd.append("{url}".format(**package))
175
231
  self._call(cmd, env)
176
232
 
233
+ @functools.lru_cache(maxsize=None)
177
234
  def info(self):
178
235
  return self._call(["config", "list", "-a"])
179
236
 
@@ -198,18 +255,24 @@ class Micromamba(object):
198
255
  }
199
256
  directories = self.info()["pkgs_dirs"]
200
257
  # search all package caches for packages
201
- metadata = {
202
- url: os.path.join(d, file)
258
+
259
+ file_to_path = {}
260
+ for d in directories:
261
+ if os.path.isdir(d):
262
+ try:
263
+ with os.scandir(d) as entries:
264
+ for entry in entries:
265
+ if entry.is_file():
266
+ # Prefer the first occurrence if the file exists in multiple directories
267
+ file_to_path.setdefault(entry.name, entry.path)
268
+ except OSError:
269
+ continue
270
+ ret = {
271
+ # set package tarball local paths to None if package tarballs are missing
272
+ url: file_to_path.get(file)
203
273
  for url, file in packages_to_filenames.items()
204
- for d in directories
205
- if os.path.isdir(d)
206
- and file in os.listdir(d)
207
- and os.path.isfile(os.path.join(d, file))
208
274
  }
209
- # set package tarball local paths to None if package tarballs are missing
210
- for url in packages_to_filenames:
211
- metadata.setdefault(url, None)
212
- return metadata
275
+ return ret
213
276
 
214
277
  def interpreter(self, id_):
215
278
  return os.path.join(self.path_to_environment(id_), "bin/python")
@@ -253,7 +316,40 @@ class Micromamba(object):
253
316
  try:
254
317
  output = json.loads(e.output)
255
318
  err = []
319
+ v_pkgs = ["__cuda", "__glibc"]
256
320
  for error in output.get("solver_problems", []):
321
+ # raise a specific error message for virtual package related errors
322
+ match = next((p for p in v_pkgs if p in error), None)
323
+ if match is not None:
324
+ vpkg_name = match[2:]
325
+ # try to strip version from error msg which are of the format:
326
+ # nothing provides <__vpkg> >=2.17,<3.0.a0 needed by <pkg_name>
327
+ try:
328
+ vpkg_version = error[
329
+ len("nothing provides %s " % match) : error.index(
330
+ " needed by"
331
+ )
332
+ ]
333
+ except ValueError:
334
+ vpkg_version = None
335
+ raise MicromambaException(
336
+ "{msg}\n\n"
337
+ "*Please set the environment variable CONDA_OVERRIDE_{var} to a specific version{version} of {name}.*\n\n"
338
+ "Here is an example of supplying environment variables through the command line\n"
339
+ "CONDA_OVERRIDE_{var}=<{name}-version> python flow.py <args>".format(
340
+ msg=msg.format(
341
+ cmd=" ".join(e.cmd),
342
+ code=e.returncode,
343
+ output=e.output.decode(),
344
+ stderr=error,
345
+ ),
346
+ var=vpkg_name.upper(),
347
+ version=(
348
+ "" if not vpkg_version else f" ({vpkg_version})"
349
+ ),
350
+ name=vpkg_name,
351
+ )
352
+ )
257
353
  err.append(error)
258
354
  raise MicromambaException(
259
355
  msg.format(
@@ -263,7 +359,7 @@ class Micromamba(object):
263
359
  stderr="\n".join(err),
264
360
  )
265
361
  )
266
- except (TypeError, ValueError) as ve:
362
+ except (TypeError, ValueError):
267
363
  pass
268
364
  raise MicromambaException(
269
365
  msg.format(
@@ -279,23 +375,37 @@ def _install_micromamba(installation_location):
279
375
  # Unfortunately no 32bit binaries are available for micromamba, which ideally
280
376
  # shouldn't be much of a problem in today's world.
281
377
  platform = conda_platform()
282
- try:
283
- subprocess.Popen(f"mkdir -p {installation_location}", shell=True).wait()
284
- # https://mamba.readthedocs.io/en/latest/micromamba-installation.html#manual-installation
285
- # requires bzip2
286
- result = subprocess.Popen(
287
- f"curl -Ls https://micro.mamba.pm/api/micromamba/{platform}/latest | tar -xvj -C {installation_location} bin/micromamba",
288
- shell=True,
289
- stderr=subprocess.PIPE,
290
- stdout=subprocess.PIPE,
291
- )
292
- _, err = result.communicate()
293
- if result.returncode != 0:
294
- raise MicromambaException(
295
- f"Micromamba installation '{result.args}' failed:\n{err.decode()}"
296
- )
378
+ url = MICROMAMBA_URL.format(platform=platform, version="1.5.7")
379
+ mirror_url = MICROMAMBA_MIRROR_URL.format(platform=platform, version="1.5.7")
380
+ os.makedirs(installation_location, exist_ok=True)
297
381
 
298
- except subprocess.CalledProcessError as e:
299
- raise MicromambaException(
300
- "Micromamba installation failed:\n{}".format(e.stderr.decode())
301
- )
382
+ def _download_and_extract(url):
383
+ max_retries = 3
384
+ for attempt in range(max_retries):
385
+ try:
386
+ # https://mamba.readthedocs.io/en/latest/micromamba-installation.html#manual-installation
387
+ # requires bzip2
388
+ result = subprocess.Popen(
389
+ f"curl -Ls {url} | tar -xvj -C {installation_location} bin/micromamba",
390
+ shell=True,
391
+ stderr=subprocess.PIPE,
392
+ stdout=subprocess.PIPE,
393
+ )
394
+ _, err = result.communicate()
395
+ if result.returncode != 0:
396
+ raise MicromambaException(
397
+ f"Micromamba installation '{result.args}' failed:\n{err.decode()}"
398
+ )
399
+ except subprocess.CalledProcessError as e:
400
+ if attempt == max_retries - 1:
401
+ raise MicromambaException(
402
+ "Micromamba installation failed:\n{}".format(e.stderr.decode())
403
+ )
404
+ time.sleep(2**attempt)
405
+
406
+ try:
407
+ # prioritize downloading from mirror
408
+ _download_and_extract(mirror_url)
409
+ except Exception:
410
+ # download from official source as a fallback
411
+ _download_and_extract(url)
@@ -0,0 +1,268 @@
1
+ # this file can be overridden by extensions as is (e.g. metaflow-nflx-extensions)
2
+ from metaflow.exception import MetaflowException
3
+
4
+
5
+ class ParserValueError(MetaflowException):
6
+ headline = "Value error"
7
+
8
+
9
+ def requirements_txt_parser(content: str):
10
+ """
11
+ Parse non-comment lines from a requirements.txt file as strictly valid
12
+ PEP 508 requirements.
13
+
14
+ Recognizes direct references (e.g. "my_lib @ git+https://..."), extras
15
+ (e.g. "requests[security]"), and version specifiers (e.g. "==2.0"). If
16
+ the package name is "python", its specifier is stored in the "python"
17
+ key instead of "packages".
18
+
19
+ Parameters
20
+ ----------
21
+ content : str
22
+ Contents of a requirements.txt file.
23
+
24
+ Returns
25
+ -------
26
+ dict
27
+ A dictionary with two keys:
28
+ - "packages": dict(str -> str)
29
+ Mapping from package name (plus optional extras/references) to a
30
+ version specifier string.
31
+ - "python": str or None
32
+ The Python version constraints if present, otherwise None.
33
+
34
+ Raises
35
+ ------
36
+ ParserValueError
37
+ If a requirement line is invalid PEP 508 or if environment markers are
38
+ detected, or if multiple Python constraints are specified.
39
+ """
40
+ import re
41
+ from metaflow._vendor.packaging.requirements import Requirement, InvalidRequirement
42
+
43
+ parsed = {"packages": {}, "python": None}
44
+
45
+ inline_comment_pattern = re.compile(r"\s+#.*$")
46
+ for line in content.splitlines():
47
+ line = line.strip()
48
+
49
+ # support Rye lockfiles by skipping lines not compliant with requirements
50
+ if line == "-e file:.":
51
+ continue
52
+
53
+ if not line or line.startswith("#"):
54
+ continue
55
+
56
+ line = inline_comment_pattern.sub("", line).strip()
57
+ if not line:
58
+ continue
59
+
60
+ try:
61
+ req = Requirement(line)
62
+ except InvalidRequirement:
63
+ raise ParserValueError(f"Not a valid PEP 508 requirement: '{line}'")
64
+
65
+ if req.marker is not None:
66
+ raise ParserValueError(
67
+ "Environment markers (e.g. 'platform_system==\"Linux\"') "
68
+ f"are not supported for line: '{line}'"
69
+ )
70
+
71
+ dep_key = req.name
72
+ if req.extras:
73
+ dep_key += f"[{','.join(req.extras)}]"
74
+ if req.url:
75
+ dep_key += f"@{req.url}"
76
+
77
+ dep_spec = str(req.specifier).lstrip(" =")
78
+
79
+ if req.name.lower() == "python":
80
+ if parsed["python"] is not None and dep_spec:
81
+ raise ParserValueError(
82
+ f"Multiple Python version specs not allowed: '{line}'"
83
+ )
84
+ parsed["python"] = dep_spec or None
85
+ else:
86
+ parsed["packages"][dep_key] = dep_spec
87
+
88
+ return parsed
89
+
90
+
91
+ def pyproject_toml_parser(content: str):
92
+ """
93
+ Parse a pyproject.toml file per PEP 621.
94
+
95
+ Reads the 'requires-python' and 'dependencies' fields from the "[project]" section.
96
+ Each dependency line must be a valid PEP 508 requirement. If the package name is
97
+ "python", its specifier is stored in the "python" key instead of "packages".
98
+
99
+ Parameters
100
+ ----------
101
+ content : str
102
+ Contents of a pyproject.toml file.
103
+
104
+ Returns
105
+ -------
106
+ dict
107
+ A dictionary with two keys:
108
+ - "packages": dict(str -> str)
109
+ Mapping from package name (plus optional extras/references) to a
110
+ version specifier string.
111
+ - "python": str or None
112
+ The Python version constraints if present, otherwise None.
113
+
114
+ Raises
115
+ ------
116
+ RuntimeError
117
+ If no TOML library (tomllib in Python 3.11+ or tomli in earlier versions) is found.
118
+ ParserValueError
119
+ If a dependency is not valid PEP 508, if environment markers are used, or if
120
+ multiple Python constraints are specified.
121
+ """
122
+ try:
123
+ import tomllib as toml # Python 3.11+
124
+ except ImportError:
125
+ try:
126
+ import tomli as toml # Python < 3.11 (requires "tomli" package)
127
+ except ImportError:
128
+ raise RuntimeError(
129
+ "Could not import a TOML library. For Python <3.11, please install 'tomli'."
130
+ )
131
+ from metaflow._vendor.packaging.requirements import Requirement, InvalidRequirement
132
+
133
+ data = toml.loads(content)
134
+
135
+ project = data.get("project", {})
136
+ requirements = project.get("dependencies", [])
137
+ requires_python = project.get("requires-python")
138
+
139
+ parsed = {"packages": {}, "python": None}
140
+
141
+ if requires_python is not None:
142
+ # If present, store verbatim; note that PEP 621 does not necessarily
143
+ # require "python" to be a dependency in the usual sense.
144
+ # Example: "requires-python" = ">=3.7,<4"
145
+ parsed["python"] = requires_python.lstrip("=").strip()
146
+
147
+ for dep_line in requirements:
148
+ dep_line_stripped = dep_line.strip()
149
+ try:
150
+ req = Requirement(dep_line_stripped)
151
+ except InvalidRequirement:
152
+ raise ParserValueError(
153
+ f"Not a valid PEP 508 requirement: '{dep_line_stripped}'"
154
+ )
155
+
156
+ if req.marker is not None:
157
+ raise ParserValueError(
158
+ f"Environment markers not supported for line: '{dep_line_stripped}'"
159
+ )
160
+
161
+ dep_key = req.name
162
+ if req.extras:
163
+ dep_key += f"[{','.join(req.extras)}]"
164
+ if req.url:
165
+ dep_key += f"@{req.url}"
166
+
167
+ dep_spec = str(req.specifier).lstrip("=")
168
+
169
+ if req.name.lower() == "python":
170
+ if parsed["python"] is not None and dep_spec:
171
+ raise ParserValueError(
172
+ f"Multiple Python version specs not allowed: '{dep_line_stripped}'"
173
+ )
174
+ parsed["python"] = dep_spec or None
175
+ else:
176
+ parsed["packages"][dep_key] = dep_spec
177
+
178
+ return parsed
179
+
180
+
181
+ def conda_environment_yml_parser(content: str):
182
+ """
183
+ Parse a minimal environment.yml file under strict assumptions.
184
+
185
+ The file must contain a 'dependencies:' line, after which each dependency line
186
+ appears with a '- ' prefix. Python can appear as 'python=3.9', etc.; other
187
+ packages as 'numpy=1.21.2' or simply 'numpy'. Non-compliant lines raise ParserValueError.
188
+
189
+ Parameters
190
+ ----------
191
+ content : str
192
+ Contents of a environment.yml file.
193
+
194
+ Returns
195
+ -------
196
+ dict
197
+ A dictionary with keys:
198
+ {
199
+ "packages": dict(str -> str),
200
+ "python": str or None
201
+ }
202
+
203
+ Raises
204
+ ------
205
+ ParserValueError
206
+ If the file has malformed lines or unsupported sections.
207
+ """
208
+ import re
209
+
210
+ packages = {}
211
+ python_version = None
212
+
213
+ inside_dependencies = False
214
+
215
+ # Basic pattern for lines like "numpy=1.21.2"
216
+ # Group 1: package name
217
+ # Group 2: optional operator + version (could be "=1.21.2", "==1.21.2", etc.)
218
+ line_regex = re.compile(r"^([A-Za-z0-9_\-\.]+)(\s*[=<>!~].+\s*)?$")
219
+ inline_comment_pattern = re.compile(r"\s+#.*$")
220
+
221
+ for line in content.splitlines():
222
+ line = line.strip()
223
+ if not line or line.startswith("#"):
224
+ continue
225
+
226
+ line = inline_comment_pattern.sub("", line).strip()
227
+ if not line:
228
+ continue
229
+
230
+ if line.lower().startswith("dependencies:"):
231
+ inside_dependencies = True
232
+ continue
233
+
234
+ if inside_dependencies and not line.startswith("-"):
235
+ inside_dependencies = False
236
+ continue
237
+
238
+ if not inside_dependencies:
239
+ continue
240
+
241
+ dep_line = line.lstrip("-").strip()
242
+ if dep_line.endswith(":"):
243
+ raise ParserValueError(
244
+ f"Unsupported subsection '{dep_line}' in environment.yml."
245
+ )
246
+
247
+ match = line_regex.match(dep_line)
248
+ if not match:
249
+ raise ParserValueError(
250
+ f"Line '{dep_line}' is not a valid conda package specifier."
251
+ )
252
+
253
+ pkg_name, pkg_version_part = match.groups()
254
+ version_spec = pkg_version_part.strip() if pkg_version_part else ""
255
+
256
+ if version_spec.startswith("="):
257
+ version_spec = version_spec.lstrip("=").strip()
258
+
259
+ if pkg_name.lower() == "python":
260
+ if python_version is not None and version_spec:
261
+ raise ParserValueError(
262
+ f"Multiple Python version specs detected: '{dep_line}'"
263
+ )
264
+ python_version = version_spec
265
+ else:
266
+ packages[pkg_name] = version_spec
267
+
268
+ return {"packages": packages, "python": python_version}