mlcflow 1.2.5__tar.gz → 1.2.6__tar.gz
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.
- {mlcflow-1.2.5 → mlcflow-1.2.6}/PKG-INFO +1 -1
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/repo_action.py +80 -15
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlcflow.egg-info/PKG-INFO +1 -1
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlcflow.egg-info/SOURCES.txt +1 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/pyproject.toml +1 -1
- mlcflow-1.2.6/tests/test_mlcflow_unix_installer.py +83 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/LICENSE.md +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/README.md +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/__init__.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/__main__.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/action.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/action_factory.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/cache_action.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/cfg_action.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/error_codes.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/experiment_action.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/index.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/item.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/logger.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/main.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/meta_schema.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/repo.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/script_action.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlc/utils.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlcflow.egg-info/dependency_links.txt +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlcflow.egg-info/entry_points.txt +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlcflow.egg-info/requires.txt +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/mlcflow.egg-info/top_level.txt +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/setup.cfg +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/tests/test_action_invalid_meta_entries.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/tests/test_cache_mark_tmp.py +0 -0
- {mlcflow-1.2.5 → mlcflow-1.2.6}/tests/test_script_action_apptainer.py +0 -0
|
@@ -2,6 +2,7 @@ from .action import Action
|
|
|
2
2
|
import os
|
|
3
3
|
import subprocess
|
|
4
4
|
import re
|
|
5
|
+
import shlex
|
|
5
6
|
import yaml
|
|
6
7
|
import json
|
|
7
8
|
import shutil
|
|
@@ -322,8 +323,21 @@ class RepoAction(Action):
|
|
|
322
323
|
return {"return": 0, "value": os.path.basename(
|
|
323
324
|
url).replace(".git", "")}
|
|
324
325
|
|
|
326
|
+
def _is_shallow_repo(self, repo_path):
|
|
327
|
+
"""Return True if the git repository at *repo_path* is a shallow clone."""
|
|
328
|
+
try:
|
|
329
|
+
result = subprocess.run(
|
|
330
|
+
['git', '-C', repo_path, 'rev-parse', '--is-shallow-repository'],
|
|
331
|
+
capture_output=True,
|
|
332
|
+
text=True,
|
|
333
|
+
)
|
|
334
|
+
return result.returncode == 0 and result.stdout.strip() == 'true'
|
|
335
|
+
except (subprocess.SubprocessError, FileNotFoundError, OSError):
|
|
336
|
+
return False
|
|
337
|
+
|
|
325
338
|
def pull_repo(self, repo_url, branch=None, checkout=None, tag=None,
|
|
326
|
-
pat=None, ssh=None, ignore_on_conflict=False, repo_path=None, force=False
|
|
339
|
+
pat=None, ssh=None, ignore_on_conflict=False, repo_path=None, force=False,
|
|
340
|
+
shallow=False, depth=None, extra_git_args=None):
|
|
327
341
|
|
|
328
342
|
# Determine the checkout path from environment or default
|
|
329
343
|
repo_base_path = self.repos_path # either the value will be from 'MLC_REPOS'
|
|
@@ -358,20 +372,41 @@ class RepoAction(Action):
|
|
|
358
372
|
repo_path = os.path.join(repo_base_path, repo_download_name)
|
|
359
373
|
|
|
360
374
|
try:
|
|
375
|
+
# Compute depth argument: --shallow implies depth=1; explicit
|
|
376
|
+
# --depth=N takes precedence
|
|
377
|
+
clone_depth = None
|
|
378
|
+
if depth is not None:
|
|
379
|
+
try:
|
|
380
|
+
clone_depth = int(depth)
|
|
381
|
+
except (TypeError, ValueError):
|
|
382
|
+
return {
|
|
383
|
+
"return": 1, "error": f"Invalid value for --depth: {depth!r}. Must be a positive integer."}
|
|
384
|
+
if clone_depth < 1:
|
|
385
|
+
return {
|
|
386
|
+
"return": 1, "error": f"Invalid value for --depth: {clone_depth}. Must be a positive integer."}
|
|
387
|
+
elif shallow:
|
|
388
|
+
clone_depth = 1
|
|
389
|
+
|
|
390
|
+
# Parse extra_git_args into a list
|
|
391
|
+
extra_args = []
|
|
392
|
+
if extra_git_args:
|
|
393
|
+
if isinstance(extra_git_args, list):
|
|
394
|
+
extra_args = extra_git_args
|
|
395
|
+
else:
|
|
396
|
+
extra_args = shlex.split(str(extra_git_args))
|
|
397
|
+
|
|
361
398
|
# If the directory doesn't exist, clone it
|
|
362
399
|
if not os.path.exists(repo_path):
|
|
363
400
|
logger.info(f"Cloning repository {repo_url} to {repo_path}...")
|
|
364
401
|
|
|
365
|
-
# Build clone command
|
|
366
|
-
clone_command = ['git', 'clone'
|
|
402
|
+
# Build clone command
|
|
403
|
+
clone_command = ['git', 'clone']
|
|
367
404
|
if branch:
|
|
368
|
-
clone_command
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
repo_url,
|
|
374
|
-
repo_path]
|
|
405
|
+
clone_command += ['--branch', branch]
|
|
406
|
+
if clone_depth is not None:
|
|
407
|
+
clone_command += ['--depth', str(clone_depth)]
|
|
408
|
+
clone_command += extra_args
|
|
409
|
+
clone_command += [repo_url, repo_path]
|
|
375
410
|
|
|
376
411
|
subprocess.run(clone_command, check=True)
|
|
377
412
|
|
|
@@ -433,8 +468,17 @@ class RepoAction(Action):
|
|
|
433
468
|
logger.info(
|
|
434
469
|
"Pulling latest changes...")
|
|
435
470
|
try:
|
|
471
|
+
pull_command = ['git', '-C', repo_path, 'pull']
|
|
472
|
+
if clone_depth is not None:
|
|
473
|
+
if self._is_shallow_repo(repo_path):
|
|
474
|
+
pull_command += ['--depth', str(clone_depth)]
|
|
475
|
+
else:
|
|
476
|
+
logger.warning(
|
|
477
|
+
f"--depth/--shallow ignored for pull on non-shallow repo {repo_path}. "
|
|
478
|
+
"Re-clone with --shallow to create a shallow copy."
|
|
479
|
+
)
|
|
436
480
|
subprocess.run(
|
|
437
|
-
|
|
481
|
+
pull_command,
|
|
438
482
|
capture_output=True,
|
|
439
483
|
text=True,
|
|
440
484
|
check=True)
|
|
@@ -492,8 +536,16 @@ class RepoAction(Action):
|
|
|
492
536
|
else:
|
|
493
537
|
logger.info(
|
|
494
538
|
"No local changes detected. Pulling latest changes...")
|
|
495
|
-
|
|
496
|
-
|
|
539
|
+
pull_command = ['git', '-C', repo_path, 'pull']
|
|
540
|
+
if clone_depth is not None:
|
|
541
|
+
if self._is_shallow_repo(repo_path):
|
|
542
|
+
pull_command += ['--depth', str(clone_depth)]
|
|
543
|
+
else:
|
|
544
|
+
logger.warning(
|
|
545
|
+
f"--depth/--shallow ignored for pull on non-shallow repo {repo_path}. "
|
|
546
|
+
"Re-clone with --shallow to create a shallow copy."
|
|
547
|
+
)
|
|
548
|
+
subprocess.run(pull_command, check=True)
|
|
497
549
|
logger.info("Repository successfully pulled.")
|
|
498
550
|
|
|
499
551
|
if tag:
|
|
@@ -562,6 +614,9 @@ class RepoAction(Action):
|
|
|
562
614
|
- `--tag <release_tag>`: Checks out a particular release tag.
|
|
563
615
|
- `--pat <access_token>` or `--ssh`: Clones a private repository using a personal access token or SSH.
|
|
564
616
|
- `--force`: For existing repositories with local tracked changes, stashes changes before pull and reapplies them after pull.
|
|
617
|
+
- `--shallow`: Perform a shallow clone with `--depth=1` (fastest for a fresh copy without history). For existing repos, only applied if the repo is already shallow; otherwise ignored with a warning.
|
|
618
|
+
- `--depth=N`: Perform a shallow clone/pull with the specified history depth (e.g. `--depth=5`). For existing repos, `--depth` is only applied when the repository is already a shallow clone; passing `--depth` to a full-history clone would corrupt it and is therefore ignored with a warning.
|
|
619
|
+
- `--extra_git_args=<args>`: Pass additional arguments to the `git clone` command (e.g. `--extra_git_args="--filter=blob:none"`). Only applies when cloning a new repository; not used for pull on existing repos. Accepts only trusted input — arguments are passed directly to git without further validation.
|
|
565
620
|
|
|
566
621
|
Example Output:
|
|
567
622
|
|
|
@@ -592,7 +647,11 @@ class RepoAction(Action):
|
|
|
592
647
|
repo_object.path, os.W_OK):
|
|
593
648
|
repo_folder_name = os.path.basename(repo_object.path)
|
|
594
649
|
res = self.pull_repo(
|
|
595
|
-
repo_folder_name, repo_path=repo_object.path, force=run_args.get(
|
|
650
|
+
repo_folder_name, repo_path=repo_object.path, force=run_args.get(
|
|
651
|
+
'force'),
|
|
652
|
+
shallow=run_args.get('shallow', False),
|
|
653
|
+
depth=run_args.get('depth'),
|
|
654
|
+
extra_git_args=run_args.get('extra_git_args'))
|
|
596
655
|
if res['return'] > 0:
|
|
597
656
|
return res
|
|
598
657
|
else:
|
|
@@ -604,6 +663,9 @@ class RepoAction(Action):
|
|
|
604
663
|
ssh = run_args.get('ssh')
|
|
605
664
|
force = run_args.get('force')
|
|
606
665
|
ignore_on_conflict = run_args.get('ignore_on_conflict')
|
|
666
|
+
shallow = run_args.get('shallow', False)
|
|
667
|
+
depth = run_args.get('depth')
|
|
668
|
+
extra_git_args = run_args.get('extra_git_args')
|
|
607
669
|
|
|
608
670
|
if sum(bool(var) for var in [branch, checkout, tag]) > 1:
|
|
609
671
|
return {
|
|
@@ -617,7 +679,10 @@ class RepoAction(Action):
|
|
|
617
679
|
pat,
|
|
618
680
|
ssh,
|
|
619
681
|
ignore_on_conflict=ignore_on_conflict,
|
|
620
|
-
force=force
|
|
682
|
+
force=force,
|
|
683
|
+
shallow=shallow,
|
|
684
|
+
depth=depth,
|
|
685
|
+
extra_git_args=extra_git_args)
|
|
621
686
|
if res['return'] > 0:
|
|
622
687
|
return res
|
|
623
688
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import platform
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
8
|
+
INSTALLER_SCRIPT = REPO_ROOT / "docs" / "install" / "mlcflow_unix_installer.sh"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _resolve_venv_dir(tmp_path, setup_snippet):
|
|
12
|
+
default_dir = tmp_path / "mlcflow"
|
|
13
|
+
marker = "__RESULT__"
|
|
14
|
+
command = f"""
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
source "{INSTALLER_SCRIPT}"
|
|
17
|
+
DEFAULT_VENV_DIR="{default_dir}"
|
|
18
|
+
VENV_DIR="$DEFAULT_VENV_DIR"
|
|
19
|
+
PY_MAJOR_MINOR="$(python3 -c 'import sys; print("{{}}.{{}}".format(*sys.version_info[:2]))')"
|
|
20
|
+
PY_ARCH="$(python3 -c 'import platform; print(platform.machine())')"
|
|
21
|
+
{setup_snippet}
|
|
22
|
+
resolve_default_venv_dir
|
|
23
|
+
echo "{marker}$VENV_DIR"
|
|
24
|
+
"""
|
|
25
|
+
result = subprocess.run(
|
|
26
|
+
["bash", "-lc", command],
|
|
27
|
+
text=True,
|
|
28
|
+
capture_output=True,
|
|
29
|
+
check=True,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
for line in reversed(result.stdout.splitlines()):
|
|
33
|
+
if line.startswith(marker):
|
|
34
|
+
return line[len(marker):]
|
|
35
|
+
|
|
36
|
+
raise AssertionError(
|
|
37
|
+
f"Could not parse resolved venv path from output:\n{result.stdout}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_resolve_default_venv_dir_prefers_compatible_default(tmp_path):
|
|
41
|
+
setup_snippet = """
|
|
42
|
+
python3 -m venv "$DEFAULT_VENV_DIR"
|
|
43
|
+
"""
|
|
44
|
+
resolved = _resolve_venv_dir(tmp_path, setup_snippet)
|
|
45
|
+
assert resolved == str(tmp_path / "mlcflow")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_resolve_default_venv_dir_uses_suffix_for_incompatible_default(
|
|
49
|
+
tmp_path):
|
|
50
|
+
setup_snippet = """
|
|
51
|
+
mkdir -p "$DEFAULT_VENV_DIR"
|
|
52
|
+
"""
|
|
53
|
+
resolved = _resolve_venv_dir(tmp_path, setup_snippet)
|
|
54
|
+
assert resolved.startswith(str(tmp_path / "mlcflow_"))
|
|
55
|
+
assert "_py" in resolved
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_resolve_default_venv_dir_removes_stale_shared_env_when_default_incompatible(
|
|
59
|
+
tmp_path):
|
|
60
|
+
"""When the default venv is incompatible and the suffixed venv already exists
|
|
61
|
+
but is itself stale/broken, the stale suffixed dir must be removed so that
|
|
62
|
+
setup_venv can create a fresh one rather than silently reusing a broken env."""
|
|
63
|
+
setup_snippet = """
|
|
64
|
+
mkdir -p "$DEFAULT_VENV_DIR"
|
|
65
|
+
SHARED_VENV_DIR="${DEFAULT_VENV_DIR}_${PY_ARCH}_py${PY_MAJOR_MINOR}"
|
|
66
|
+
mkdir -p "$SHARED_VENV_DIR"
|
|
67
|
+
"""
|
|
68
|
+
resolved = _resolve_venv_dir(tmp_path, setup_snippet)
|
|
69
|
+
expected_suffix = f"{tmp_path / 'mlcflow'}_{platform.machine()}_py{sys.version_info[0]}.{sys.version_info[1]}"
|
|
70
|
+
assert resolved == expected_suffix
|
|
71
|
+
# The stale shared dir should have been removed so setup_venv creates it
|
|
72
|
+
# fresh
|
|
73
|
+
assert not Path(expected_suffix).exists()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_resolve_default_venv_dir_reuses_compatible_suffixed_env(tmp_path):
|
|
77
|
+
setup_snippet = """
|
|
78
|
+
SHARED_VENV_DIR="${DEFAULT_VENV_DIR}_${PY_ARCH}_py${PY_MAJOR_MINOR}"
|
|
79
|
+
python3 -m venv "$SHARED_VENV_DIR"
|
|
80
|
+
"""
|
|
81
|
+
resolved = _resolve_venv_dir(tmp_path, setup_snippet)
|
|
82
|
+
expected = f"{tmp_path / 'mlcflow'}_{platform.machine()}_py{sys.version_info[0]}.{sys.version_info[1]}"
|
|
83
|
+
assert resolved == expected
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|