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.
- metaflow/R.py +10 -7
- metaflow/__init__.py +40 -25
- metaflow/_vendor/imghdr/__init__.py +186 -0
- metaflow/_vendor/importlib_metadata/__init__.py +1063 -0
- metaflow/_vendor/importlib_metadata/_adapters.py +68 -0
- metaflow/_vendor/importlib_metadata/_collections.py +30 -0
- metaflow/_vendor/importlib_metadata/_compat.py +71 -0
- metaflow/_vendor/importlib_metadata/_functools.py +104 -0
- metaflow/_vendor/importlib_metadata/_itertools.py +73 -0
- metaflow/_vendor/importlib_metadata/_meta.py +48 -0
- metaflow/_vendor/importlib_metadata/_text.py +99 -0
- metaflow/_vendor/importlib_metadata/py.typed +0 -0
- metaflow/_vendor/typeguard/__init__.py +48 -0
- metaflow/_vendor/typeguard/_checkers.py +1070 -0
- metaflow/_vendor/typeguard/_config.py +108 -0
- metaflow/_vendor/typeguard/_decorators.py +233 -0
- metaflow/_vendor/typeguard/_exceptions.py +42 -0
- metaflow/_vendor/typeguard/_functions.py +308 -0
- metaflow/_vendor/typeguard/_importhook.py +213 -0
- metaflow/_vendor/typeguard/_memo.py +48 -0
- metaflow/_vendor/typeguard/_pytest_plugin.py +127 -0
- metaflow/_vendor/typeguard/_suppression.py +86 -0
- metaflow/_vendor/typeguard/_transformer.py +1229 -0
- metaflow/_vendor/typeguard/_union_transformer.py +55 -0
- metaflow/_vendor/typeguard/_utils.py +173 -0
- metaflow/_vendor/typeguard/py.typed +0 -0
- metaflow/_vendor/typing_extensions.py +3641 -0
- metaflow/_vendor/v3_7/importlib_metadata/__init__.py +1063 -0
- metaflow/_vendor/v3_7/importlib_metadata/_adapters.py +68 -0
- metaflow/_vendor/v3_7/importlib_metadata/_collections.py +30 -0
- metaflow/_vendor/v3_7/importlib_metadata/_compat.py +71 -0
- metaflow/_vendor/v3_7/importlib_metadata/_functools.py +104 -0
- metaflow/_vendor/v3_7/importlib_metadata/_itertools.py +73 -0
- metaflow/_vendor/v3_7/importlib_metadata/_meta.py +48 -0
- metaflow/_vendor/v3_7/importlib_metadata/_text.py +99 -0
- metaflow/_vendor/v3_7/importlib_metadata/py.typed +0 -0
- metaflow/_vendor/v3_7/typeguard/__init__.py +48 -0
- metaflow/_vendor/v3_7/typeguard/_checkers.py +906 -0
- metaflow/_vendor/v3_7/typeguard/_config.py +108 -0
- metaflow/_vendor/v3_7/typeguard/_decorators.py +237 -0
- metaflow/_vendor/v3_7/typeguard/_exceptions.py +42 -0
- metaflow/_vendor/v3_7/typeguard/_functions.py +310 -0
- metaflow/_vendor/v3_7/typeguard/_importhook.py +213 -0
- metaflow/_vendor/v3_7/typeguard/_memo.py +48 -0
- metaflow/_vendor/v3_7/typeguard/_pytest_plugin.py +100 -0
- metaflow/_vendor/v3_7/typeguard/_suppression.py +88 -0
- metaflow/_vendor/v3_7/typeguard/_transformer.py +1207 -0
- metaflow/_vendor/v3_7/typeguard/_union_transformer.py +54 -0
- metaflow/_vendor/v3_7/typeguard/_utils.py +169 -0
- metaflow/_vendor/v3_7/typeguard/py.typed +0 -0
- metaflow/_vendor/v3_7/typing_extensions.py +3072 -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 +5 -0
- metaflow/cli.py +331 -785
- metaflow/cli_args.py +17 -0
- metaflow/cli_components/__init__.py +0 -0
- metaflow/cli_components/dump_cmd.py +96 -0
- metaflow/cli_components/init_cmd.py +52 -0
- metaflow/cli_components/run_cmds.py +546 -0
- metaflow/cli_components/step_cmd.py +334 -0
- metaflow/cli_components/utils.py +140 -0
- metaflow/client/__init__.py +1 -0
- metaflow/client/core.py +467 -73
- metaflow/client/filecache.py +75 -35
- metaflow/clone_util.py +7 -1
- metaflow/cmd/code/__init__.py +231 -0
- metaflow/cmd/develop/stub_generator.py +756 -288
- metaflow/cmd/develop/stubs.py +12 -28
- metaflow/cmd/main_cli.py +6 -4
- metaflow/cmd/make_wrapper.py +78 -0
- metaflow/datastore/__init__.py +1 -0
- metaflow/datastore/content_addressed_store.py +41 -10
- metaflow/datastore/datastore_set.py +11 -2
- metaflow/datastore/flow_datastore.py +156 -10
- metaflow/datastore/spin_datastore.py +91 -0
- metaflow/datastore/task_datastore.py +154 -39
- metaflow/debug.py +5 -0
- metaflow/decorators.py +404 -78
- metaflow/exception.py +8 -2
- metaflow/extension_support/__init__.py +527 -376
- metaflow/extension_support/_empty_file.py +2 -2
- metaflow/extension_support/plugins.py +49 -31
- metaflow/flowspec.py +482 -33
- metaflow/graph.py +210 -42
- metaflow/includefile.py +84 -40
- metaflow/lint.py +141 -22
- metaflow/meta_files.py +13 -0
- metaflow/{metadata → metadata_provider}/heartbeat.py +24 -8
- metaflow/{metadata → metadata_provider}/metadata.py +86 -1
- metaflow/metaflow_config.py +175 -28
- metaflow/metaflow_config_funcs.py +51 -3
- metaflow/metaflow_current.py +4 -10
- metaflow/metaflow_environment.py +139 -53
- metaflow/metaflow_git.py +115 -0
- metaflow/metaflow_profile.py +18 -0
- metaflow/metaflow_version.py +150 -66
- metaflow/mflog/__init__.py +4 -3
- metaflow/mflog/save_logs.py +2 -2
- metaflow/multicore_utils.py +31 -14
- 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 +149 -28
- metaflow/plugins/__init__.py +74 -5
- metaflow/plugins/airflow/airflow.py +40 -25
- metaflow/plugins/airflow/airflow_cli.py +22 -5
- metaflow/plugins/airflow/airflow_decorator.py +1 -1
- metaflow/plugins/airflow/airflow_utils.py +5 -3
- metaflow/plugins/airflow/sensors/base_sensor.py +4 -4
- metaflow/plugins/airflow/sensors/external_task_sensor.py +2 -2
- metaflow/plugins/airflow/sensors/s3_sensor.py +2 -2
- metaflow/plugins/argo/argo_client.py +78 -33
- metaflow/plugins/argo/argo_events.py +6 -6
- metaflow/plugins/argo/argo_workflows.py +2410 -527
- metaflow/plugins/argo/argo_workflows_cli.py +571 -121
- metaflow/plugins/argo/argo_workflows_decorator.py +43 -12
- metaflow/plugins/argo/argo_workflows_deployer.py +106 -0
- metaflow/plugins/argo/argo_workflows_deployer_objects.py +453 -0
- metaflow/plugins/argo/capture_error.py +73 -0
- metaflow/plugins/argo/conditional_input_paths.py +35 -0
- metaflow/plugins/argo/exit_hooks.py +209 -0
- metaflow/plugins/argo/jobset_input_paths.py +15 -0
- metaflow/plugins/argo/param_val.py +19 -0
- metaflow/plugins/aws/aws_client.py +10 -3
- metaflow/plugins/aws/aws_utils.py +55 -2
- metaflow/plugins/aws/batch/batch.py +72 -5
- metaflow/plugins/aws/batch/batch_cli.py +33 -10
- metaflow/plugins/aws/batch/batch_client.py +4 -3
- metaflow/plugins/aws/batch/batch_decorator.py +102 -35
- metaflow/plugins/aws/secrets_manager/aws_secrets_manager_secrets_provider.py +13 -10
- metaflow/plugins/aws/step_functions/dynamo_db_client.py +0 -3
- metaflow/plugins/aws/step_functions/production_token.py +1 -1
- metaflow/plugins/aws/step_functions/step_functions.py +65 -8
- metaflow/plugins/aws/step_functions/step_functions_cli.py +101 -7
- metaflow/plugins/aws/step_functions/step_functions_decorator.py +1 -2
- metaflow/plugins/aws/step_functions/step_functions_deployer.py +97 -0
- metaflow/plugins/aws/step_functions/step_functions_deployer_objects.py +264 -0
- metaflow/plugins/azure/azure_exceptions.py +1 -1
- metaflow/plugins/azure/azure_secret_manager_secrets_provider.py +240 -0
- metaflow/plugins/azure/azure_tail.py +1 -1
- metaflow/plugins/azure/includefile_support.py +2 -0
- metaflow/plugins/cards/card_cli.py +66 -30
- metaflow/plugins/cards/card_creator.py +25 -1
- metaflow/plugins/cards/card_datastore.py +21 -49
- metaflow/plugins/cards/card_decorator.py +132 -8
- metaflow/plugins/cards/card_modules/basic.py +112 -17
- metaflow/plugins/cards/card_modules/bundle.css +1 -1
- metaflow/plugins/cards/card_modules/card.py +16 -1
- metaflow/plugins/cards/card_modules/chevron/renderer.py +1 -1
- metaflow/plugins/cards/card_modules/components.py +665 -28
- metaflow/plugins/cards/card_modules/convert_to_native_type.py +36 -7
- 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 +68 -49
- metaflow/plugins/cards/card_modules/renderer_tools.py +1 -0
- metaflow/plugins/cards/card_modules/test_cards.py +26 -12
- metaflow/plugins/cards/card_server.py +39 -14
- metaflow/plugins/cards/component_serializer.py +2 -9
- metaflow/plugins/cards/metadata.py +22 -0
- metaflow/plugins/catch_decorator.py +9 -0
- metaflow/plugins/datastores/azure_storage.py +10 -1
- metaflow/plugins/datastores/gs_storage.py +6 -2
- metaflow/plugins/datastores/local_storage.py +12 -6
- metaflow/plugins/datastores/spin_storage.py +12 -0
- metaflow/plugins/datatools/local.py +2 -0
- metaflow/plugins/datatools/s3/s3.py +126 -75
- metaflow/plugins/datatools/s3/s3op.py +254 -121
- metaflow/plugins/env_escape/__init__.py +3 -3
- metaflow/plugins/env_escape/client_modules.py +102 -72
- metaflow/plugins/env_escape/server.py +7 -0
- metaflow/plugins/env_escape/stub.py +24 -5
- metaflow/plugins/events_decorator.py +343 -185
- 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/gcp/__init__.py +1 -1
- metaflow/plugins/gcp/gcp_secret_manager_secrets_provider.py +11 -6
- metaflow/plugins/gcp/gs_tail.py +10 -6
- metaflow/plugins/gcp/includefile_support.py +3 -0
- metaflow/plugins/kubernetes/kube_utils.py +108 -0
- metaflow/plugins/kubernetes/kubernetes.py +411 -130
- metaflow/plugins/kubernetes/kubernetes_cli.py +168 -36
- metaflow/plugins/kubernetes/kubernetes_client.py +104 -2
- metaflow/plugins/kubernetes/kubernetes_decorator.py +246 -88
- metaflow/plugins/kubernetes/kubernetes_job.py +253 -581
- metaflow/plugins/kubernetes/kubernetes_jobsets.py +1071 -0
- metaflow/plugins/kubernetes/spot_metadata_cli.py +69 -0
- metaflow/plugins/kubernetes/spot_monitor_sidecar.py +109 -0
- metaflow/plugins/logs_cli.py +359 -0
- metaflow/plugins/{metadata → metadata_providers}/local.py +144 -84
- metaflow/plugins/{metadata → metadata_providers}/service.py +103 -26
- metaflow/plugins/metadata_providers/spin.py +16 -0
- metaflow/plugins/package_cli.py +36 -24
- metaflow/plugins/parallel_decorator.py +128 -11
- metaflow/plugins/parsers.py +16 -0
- metaflow/plugins/project_decorator.py +51 -5
- metaflow/plugins/pypi/bootstrap.py +357 -105
- metaflow/plugins/pypi/conda_decorator.py +82 -81
- metaflow/plugins/pypi/conda_environment.py +187 -52
- metaflow/plugins/pypi/micromamba.py +157 -47
- metaflow/plugins/pypi/parsers.py +268 -0
- metaflow/plugins/pypi/pip.py +88 -13
- metaflow/plugins/pypi/pypi_decorator.py +37 -1
- metaflow/plugins/pypi/utils.py +48 -2
- metaflow/plugins/resources_decorator.py +2 -2
- metaflow/plugins/secrets/__init__.py +3 -0
- metaflow/plugins/secrets/secrets_decorator.py +26 -181
- 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/tag_cli.py +4 -7
- metaflow/plugins/test_unbounded_foreach_decorator.py +41 -6
- metaflow/plugins/timeout_decorator.py +3 -3
- metaflow/plugins/uv/__init__.py +0 -0
- metaflow/plugins/uv/bootstrap.py +128 -0
- metaflow/plugins/uv/uv_environment.py +72 -0
- metaflow/procpoll.py +1 -1
- metaflow/pylint_wrapper.py +5 -1
- metaflow/runner/__init__.py +0 -0
- metaflow/runner/click_api.py +717 -0
- metaflow/runner/deployer.py +470 -0
- metaflow/runner/deployer_impl.py +201 -0
- metaflow/runner/metaflow_runner.py +714 -0
- metaflow/runner/nbdeploy.py +132 -0
- metaflow/runner/nbrun.py +225 -0
- metaflow/runner/subprocess_manager.py +650 -0
- metaflow/runner/utils.py +335 -0
- metaflow/runtime.py +1078 -260
- metaflow/sidecar/sidecar_worker.py +1 -1
- metaflow/system/__init__.py +5 -0
- metaflow/system/system_logger.py +85 -0
- metaflow/system/system_monitor.py +108 -0
- metaflow/system/system_utils.py +19 -0
- metaflow/task.py +521 -225
- metaflow/tracing/__init__.py +7 -7
- metaflow/tracing/span_exporter.py +31 -38
- metaflow/tracing/tracing_modules.py +38 -43
- metaflow/tuple_util.py +27 -0
- metaflow/user_configs/__init__.py +0 -0
- metaflow/user_configs/config_options.py +563 -0
- metaflow/user_configs/config_parameters.py +598 -0
- 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 +243 -27
- metaflow/vendor.py +23 -7
- metaflow/version.py +1 -1
- ob_metaflow-2.19.7.1rc0.data/data/share/metaflow/devtools/Makefile +355 -0
- ob_metaflow-2.19.7.1rc0.data/data/share/metaflow/devtools/Tiltfile +726 -0
- ob_metaflow-2.19.7.1rc0.data/data/share/metaflow/devtools/pick_services.sh +105 -0
- ob_metaflow-2.19.7.1rc0.dist-info/METADATA +87 -0
- ob_metaflow-2.19.7.1rc0.dist-info/RECORD +445 -0
- {ob_metaflow-2.11.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/WHEEL +1 -1
- {ob_metaflow-2.11.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/entry_points.txt +1 -0
- 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/package.py +0 -188
- ob_metaflow-2.11.13.1.dist-info/METADATA +0 -85
- ob_metaflow-2.11.13.1.dist-info/RECORD +0 -308
- /metaflow/_vendor/{v3_5/zipp.py → zipp.py} +0 -0
- /metaflow/{metadata → metadata_provider}/__init__.py +0 -0
- /metaflow/{metadata → metadata_provider}/util.py +0 -0
- /metaflow/plugins/{metadata → metadata_providers}/__init__.py +0 -0
- {ob_metaflow-2.11.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info/licenses}/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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}
|