groundhog-hpc 0.5.6__py3-none-any.whl → 0.7.0__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.
- groundhog_hpc/__init__.py +4 -0
- groundhog_hpc/app/add.py +74 -0
- groundhog_hpc/app/init.py +54 -10
- groundhog_hpc/app/main.py +5 -1
- groundhog_hpc/app/remove.py +91 -16
- groundhog_hpc/app/run.py +70 -2
- groundhog_hpc/compute.py +16 -1
- groundhog_hpc/configuration/defaults.py +1 -0
- groundhog_hpc/configuration/endpoints.py +38 -171
- groundhog_hpc/configuration/models.py +26 -0
- groundhog_hpc/configuration/pep723.py +278 -2
- groundhog_hpc/configuration/resolver.py +36 -8
- groundhog_hpc/console.py +1 -1
- groundhog_hpc/decorators.py +35 -16
- groundhog_hpc/function.py +53 -19
- groundhog_hpc/future.py +48 -10
- groundhog_hpc/harness.py +15 -19
- groundhog_hpc/logging.py +51 -0
- groundhog_hpc/serialization.py +22 -2
- groundhog_hpc/templates/init_script.py.jinja +4 -5
- groundhog_hpc/templates/shell_command.sh.jinja +15 -1
- groundhog_hpc/templating.py +17 -0
- {groundhog_hpc-0.5.6.dist-info → groundhog_hpc-0.7.0.dist-info}/METADATA +12 -6
- groundhog_hpc-0.7.0.dist-info/RECORD +34 -0
- groundhog_hpc-0.5.6.dist-info/RECORD +0 -33
- {groundhog_hpc-0.5.6.dist-info → groundhog_hpc-0.7.0.dist-info}/WHEEL +0 -0
- {groundhog_hpc-0.5.6.dist-info → groundhog_hpc-0.7.0.dist-info}/entry_points.txt +0 -0
- {groundhog_hpc-0.5.6.dist-info → groundhog_hpc-0.7.0.dist-info}/licenses/LICENSE +0 -0
groundhog_hpc/__init__.py
CHANGED
|
@@ -32,6 +32,7 @@ import os
|
|
|
32
32
|
|
|
33
33
|
from groundhog_hpc.decorators import function, harness, method
|
|
34
34
|
from groundhog_hpc.import_hook import install_import_hook
|
|
35
|
+
from groundhog_hpc.logging import setup_logging
|
|
35
36
|
from groundhog_hpc.utils import mark_import_safe
|
|
36
37
|
|
|
37
38
|
try:
|
|
@@ -41,5 +42,8 @@ except importlib.metadata.PackageNotFoundError:
|
|
|
41
42
|
|
|
42
43
|
__all__ = ["function", "harness", "method", "mark_import_safe", "__version__"]
|
|
43
44
|
|
|
45
|
+
# Configure logging on import
|
|
46
|
+
setup_logging()
|
|
47
|
+
|
|
44
48
|
if not os.environ.get("GROUNDHOG_NO_IMPORT_HOOK"):
|
|
45
49
|
install_import_hook()
|
groundhog_hpc/app/add.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Add command for managing PEP 723 script dependencies."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import subprocess
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
@@ -11,9 +12,23 @@ from groundhog_hpc.app.utils import (
|
|
|
11
12
|
normalize_python_version_with_uv,
|
|
12
13
|
update_requires_python,
|
|
13
14
|
)
|
|
15
|
+
from groundhog_hpc.configuration.endpoints import (
|
|
16
|
+
KNOWN_ENDPOINTS,
|
|
17
|
+
get_endpoint_schema_comments,
|
|
18
|
+
parse_endpoint_spec,
|
|
19
|
+
)
|
|
20
|
+
from groundhog_hpc.configuration.pep723 import add_endpoint_to_script
|
|
21
|
+
from groundhog_hpc.logging import setup_logging
|
|
14
22
|
|
|
15
23
|
console = Console()
|
|
16
24
|
|
|
25
|
+
KNOWN_ENDPOINT_ALIASES = []
|
|
26
|
+
for name in KNOWN_ENDPOINTS.keys():
|
|
27
|
+
KNOWN_ENDPOINT_ALIASES += [name]
|
|
28
|
+
KNOWN_ENDPOINT_ALIASES += [
|
|
29
|
+
f"{name}.{variant}" for variant in KNOWN_ENDPOINTS[name]["variants"].keys()
|
|
30
|
+
]
|
|
31
|
+
|
|
17
32
|
|
|
18
33
|
def add(
|
|
19
34
|
script: Path = typer.Argument(..., help="Path to the script to modify"),
|
|
@@ -24,8 +39,27 @@ def add(
|
|
|
24
39
|
python: str | None = typer.Option(
|
|
25
40
|
None, "--python", "-p", help="Python version specifier"
|
|
26
41
|
),
|
|
42
|
+
endpoints: list[str] = typer.Option(
|
|
43
|
+
[],
|
|
44
|
+
"--endpoint",
|
|
45
|
+
"-e",
|
|
46
|
+
help=(
|
|
47
|
+
"Add endpoint configuration (e.g., anvil, anvil.gpu, name:uuid). "
|
|
48
|
+
f"Known endpoints: {', '.join(KNOWN_ENDPOINT_ALIASES)}. Can specify multiple."
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
log_level: str = typer.Option(
|
|
52
|
+
None,
|
|
53
|
+
"--log-level",
|
|
54
|
+
help="Set logging level (DEBUG, INFO, WARNING, ERROR)\n\n[env: GROUNDHOG_LOG_LEVEL=]",
|
|
55
|
+
),
|
|
27
56
|
) -> None:
|
|
28
57
|
"""Add dependencies or update Python version in a script's PEP 723 metadata."""
|
|
58
|
+
if log_level:
|
|
59
|
+
os.environ["GROUNDHOG_LOG_LEVEL"] = log_level.upper()
|
|
60
|
+
# Reconfigure logging with the new level
|
|
61
|
+
setup_logging()
|
|
62
|
+
|
|
29
63
|
if not script.exists():
|
|
30
64
|
console.print(f"[red]Error: Script '{script}' not found[/red]")
|
|
31
65
|
raise typer.Exit(1)
|
|
@@ -60,3 +94,43 @@ def add(
|
|
|
60
94
|
except subprocess.CalledProcessError as e:
|
|
61
95
|
console.print(f"[red]{e.stderr.strip()}[/red]")
|
|
62
96
|
raise typer.Exit(1)
|
|
97
|
+
|
|
98
|
+
# handle --endpoint flags
|
|
99
|
+
if endpoints:
|
|
100
|
+
content = script.read_text()
|
|
101
|
+
added_any = False
|
|
102
|
+
|
|
103
|
+
for endpoint_spec_str in endpoints:
|
|
104
|
+
try:
|
|
105
|
+
spec = parse_endpoint_spec(endpoint_spec_str)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
108
|
+
raise typer.Exit(1)
|
|
109
|
+
|
|
110
|
+
# Build config dict from the spec
|
|
111
|
+
endpoint_config = {"endpoint": spec.uuid, **spec.base_defaults}
|
|
112
|
+
variant_config = spec.variant_defaults if spec.variant else None
|
|
113
|
+
|
|
114
|
+
# Fetch schema comments if UUID is valid (not a TODO placeholder)
|
|
115
|
+
schema_comments = None
|
|
116
|
+
if not spec.uuid.startswith("TODO"):
|
|
117
|
+
schema_comments = get_endpoint_schema_comments(spec.uuid)
|
|
118
|
+
|
|
119
|
+
content, skip_msg = add_endpoint_to_script(
|
|
120
|
+
content,
|
|
121
|
+
endpoint_name=spec.name,
|
|
122
|
+
endpoint_config=endpoint_config,
|
|
123
|
+
variant_name=spec.variant,
|
|
124
|
+
variant_config=variant_config,
|
|
125
|
+
schema_comments=schema_comments,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if skip_msg:
|
|
129
|
+
console.print(f"[yellow]{skip_msg}[/yellow]")
|
|
130
|
+
else:
|
|
131
|
+
added_any = True
|
|
132
|
+
|
|
133
|
+
script.write_text(content)
|
|
134
|
+
|
|
135
|
+
if added_any:
|
|
136
|
+
console.print(f"[green]Added endpoint configuration to {script}[/green]")
|
groundhog_hpc/app/init.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Init command for creating new Groundhog scripts."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import subprocess
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Optional
|
|
@@ -11,9 +12,15 @@ from rich.console import Console
|
|
|
11
12
|
from groundhog_hpc.app.utils import normalize_python_version_with_uv
|
|
12
13
|
from groundhog_hpc.configuration.endpoints import (
|
|
13
14
|
KNOWN_ENDPOINTS,
|
|
14
|
-
|
|
15
|
+
get_endpoint_schema_comments,
|
|
16
|
+
parse_endpoint_spec,
|
|
15
17
|
)
|
|
16
|
-
from groundhog_hpc.configuration.pep723 import
|
|
18
|
+
from groundhog_hpc.configuration.pep723 import (
|
|
19
|
+
Pep723Metadata,
|
|
20
|
+
add_endpoint_to_script,
|
|
21
|
+
remove_endpoint_from_script,
|
|
22
|
+
)
|
|
23
|
+
from groundhog_hpc.logging import setup_logging
|
|
17
24
|
|
|
18
25
|
console = Console()
|
|
19
26
|
|
|
@@ -46,8 +53,18 @@ def init(
|
|
|
46
53
|
"Can specify multiple."
|
|
47
54
|
),
|
|
48
55
|
),
|
|
56
|
+
log_level: str = typer.Option(
|
|
57
|
+
None,
|
|
58
|
+
"--log-level",
|
|
59
|
+
help="Set logging level (DEBUG, INFO, WARNING, ERROR)\n\n[env: GROUNDHOG_LOG_LEVEL=]",
|
|
60
|
+
),
|
|
49
61
|
) -> None:
|
|
50
62
|
"""Create a new groundhog script with PEP 723 metadata and example code."""
|
|
63
|
+
if log_level:
|
|
64
|
+
os.environ["GROUNDHOG_LOG_LEVEL"] = log_level.upper()
|
|
65
|
+
# Reconfigure logging with the new level
|
|
66
|
+
setup_logging()
|
|
67
|
+
|
|
51
68
|
if Path(filename).exists():
|
|
52
69
|
console.print(f"[red]Error: {filename} already exists[/red]")
|
|
53
70
|
raise typer.Exit(1)
|
|
@@ -67,32 +84,59 @@ def init(
|
|
|
67
84
|
assert default_meta.tool and default_meta.tool.uv
|
|
68
85
|
exclude_newer = default_meta.tool.uv.exclude_newer
|
|
69
86
|
|
|
70
|
-
#
|
|
71
|
-
|
|
87
|
+
# Parse endpoint specs if provided
|
|
88
|
+
endpoint_specs = []
|
|
72
89
|
if endpoints:
|
|
73
90
|
try:
|
|
74
|
-
|
|
75
|
-
for endpoint in endpoint_blocks:
|
|
76
|
-
console.print(f"[green]✓[/green] Fetched schema for {endpoint.name}")
|
|
91
|
+
endpoint_specs = [parse_endpoint_spec(spec) for spec in endpoints]
|
|
77
92
|
except Exception as e:
|
|
78
93
|
console.print(f"[red]Error: {e}[/red]")
|
|
79
94
|
raise typer.Exit(1)
|
|
80
95
|
|
|
96
|
+
# Determine endpoint name for decorator (first endpoint or placeholder)
|
|
97
|
+
first_endpoint_name = endpoint_specs[0].name if endpoint_specs else "my_endpoint"
|
|
98
|
+
|
|
99
|
+
# Render template (always includes my_endpoint placeholder)
|
|
81
100
|
env = Environment(loader=PackageLoader("groundhog_hpc", "templates"))
|
|
82
101
|
template = env.get_template("init_script.py.jinja")
|
|
83
102
|
content = template.render(
|
|
84
103
|
filename=filename,
|
|
85
104
|
python=python,
|
|
86
105
|
exclude_newer=exclude_newer,
|
|
87
|
-
|
|
106
|
+
endpoint_name=first_endpoint_name,
|
|
88
107
|
)
|
|
108
|
+
|
|
109
|
+
# If endpoints provided, replace placeholder with real endpoints
|
|
110
|
+
if endpoint_specs:
|
|
111
|
+
# Remove placeholder
|
|
112
|
+
content = remove_endpoint_from_script(content, "my_endpoint")
|
|
113
|
+
|
|
114
|
+
# Add each requested endpoint
|
|
115
|
+
for spec in endpoint_specs:
|
|
116
|
+
endpoint_config = {"endpoint": spec.uuid, **spec.base_defaults}
|
|
117
|
+
variant_config = spec.variant_defaults if spec.variant else None
|
|
118
|
+
|
|
119
|
+
# Fetch schema comments if UUID is valid (not a TODO placeholder)
|
|
120
|
+
schema_comments = None
|
|
121
|
+
if not spec.uuid.startswith("TODO"):
|
|
122
|
+
schema_comments = get_endpoint_schema_comments(spec.uuid)
|
|
123
|
+
|
|
124
|
+
content, _ = add_endpoint_to_script(
|
|
125
|
+
content,
|
|
126
|
+
endpoint_name=spec.name,
|
|
127
|
+
endpoint_config=endpoint_config,
|
|
128
|
+
variant_name=spec.variant,
|
|
129
|
+
variant_config=variant_config,
|
|
130
|
+
schema_comments=schema_comments,
|
|
131
|
+
)
|
|
132
|
+
|
|
89
133
|
Path(filename).write_text(content)
|
|
90
134
|
|
|
91
135
|
console.print(f"[green]✓[/green] Created {filename}")
|
|
92
|
-
if
|
|
136
|
+
if endpoint_specs:
|
|
93
137
|
console.print("\nNext steps:")
|
|
94
138
|
console.print(
|
|
95
|
-
f" 1. Update fields in the \\[tool.hog.{
|
|
139
|
+
f" 1. Update fields in the \\[tool.hog.{endpoint_specs[0].name}] block"
|
|
96
140
|
)
|
|
97
141
|
console.print(f" 2. Run with: [bold]hog run {filename} main[/bold]")
|
|
98
142
|
else:
|
groundhog_hpc/app/main.py
CHANGED
|
@@ -16,7 +16,11 @@ from groundhog_hpc.app.run import run
|
|
|
16
16
|
|
|
17
17
|
app = typer.Typer(pretty_exceptions_show_locals=False)
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
# Enable extra args for run command to capture harness arguments after --
|
|
20
|
+
app.command(
|
|
21
|
+
no_args_is_help=True,
|
|
22
|
+
context_settings={"allow_extra_args": True, "allow_interspersed_args": False},
|
|
23
|
+
)(run)
|
|
20
24
|
app.command(no_args_is_help=True)(init)
|
|
21
25
|
app.command(no_args_is_help=True)(add)
|
|
22
26
|
app.command(no_args_is_help=True)(remove)
|
groundhog_hpc/app/remove.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Remove command for managing PEP 723 script dependencies."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import subprocess
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
@@ -7,31 +8,105 @@ import typer
|
|
|
7
8
|
import uv
|
|
8
9
|
from rich.console import Console
|
|
9
10
|
|
|
11
|
+
from groundhog_hpc.configuration.endpoints import KNOWN_ENDPOINTS
|
|
12
|
+
from groundhog_hpc.configuration.pep723 import remove_endpoint_from_script
|
|
13
|
+
from groundhog_hpc.logging import setup_logging
|
|
14
|
+
|
|
10
15
|
console = Console()
|
|
11
16
|
|
|
17
|
+
KNOWN_ENDPOINT_ALIASES = []
|
|
18
|
+
for name in KNOWN_ENDPOINTS.keys():
|
|
19
|
+
KNOWN_ENDPOINT_ALIASES += [name]
|
|
20
|
+
KNOWN_ENDPOINT_ALIASES += [
|
|
21
|
+
f"{name}.{variant}" for variant in KNOWN_ENDPOINTS[name]["variants"].keys()
|
|
22
|
+
]
|
|
23
|
+
|
|
12
24
|
|
|
13
25
|
def remove(
|
|
14
26
|
script: Path = typer.Argument(..., help="Path to the script to modify"),
|
|
15
|
-
packages: list[str] = typer.Argument(
|
|
27
|
+
packages: list[str] | None = typer.Argument(None, help="Packages to remove"),
|
|
28
|
+
endpoints: list[str] = typer.Option(
|
|
29
|
+
[],
|
|
30
|
+
"--endpoint",
|
|
31
|
+
"-e",
|
|
32
|
+
help=(
|
|
33
|
+
"Remove endpoint or variant configuration (e.g., anvil, anvil.gpu, my_endpoint). "
|
|
34
|
+
f"Known endpoints: {', '.join(KNOWN_ENDPOINT_ALIASES)}. Can specify multiple. "
|
|
35
|
+
"Note: Removing a base endpoint (e.g., anvil) removes all its variants. "
|
|
36
|
+
"Removing a specific variant (e.g., anvil.gpu) leaves the base and other variants intact."
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
log_level: str = typer.Option(
|
|
40
|
+
None,
|
|
41
|
+
"--log-level",
|
|
42
|
+
help="Set logging level (DEBUG, INFO, WARNING, ERROR)\n\n[env: GROUNDHOG_LOG_LEVEL=]",
|
|
43
|
+
),
|
|
16
44
|
) -> None:
|
|
17
45
|
"""Remove dependencies from a script's PEP 723 metadata."""
|
|
46
|
+
if log_level:
|
|
47
|
+
os.environ["GROUNDHOG_LOG_LEVEL"] = log_level.upper()
|
|
48
|
+
# Reconfigure logging with the new level
|
|
49
|
+
setup_logging()
|
|
50
|
+
|
|
18
51
|
# Validate script exists
|
|
19
52
|
if not script.exists():
|
|
20
53
|
console.print(f"[red]Error: Script '{script}' not found[/red]")
|
|
21
54
|
raise typer.Exit(1)
|
|
22
55
|
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
# Handle package removal
|
|
57
|
+
packages = packages or []
|
|
58
|
+
if packages:
|
|
59
|
+
# Shell out to uv
|
|
60
|
+
cmd = [f"{uv.find_uv_bin()}", "remove", "--script", str(script)]
|
|
61
|
+
cmd.extend(packages)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
subprocess.run(
|
|
65
|
+
cmd,
|
|
66
|
+
check=True,
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
)
|
|
70
|
+
console.print(f"[green]Removed packages from {script}[/green]")
|
|
71
|
+
except subprocess.CalledProcessError as e:
|
|
72
|
+
console.print(f"[red]{e.stderr.strip()}[/red]")
|
|
73
|
+
raise typer.Exit(1)
|
|
74
|
+
|
|
75
|
+
# Handle endpoint removal
|
|
76
|
+
if endpoints:
|
|
77
|
+
content = script.read_text()
|
|
78
|
+
removed_any = False
|
|
79
|
+
|
|
80
|
+
for endpoint_spec in endpoints:
|
|
81
|
+
# Parse endpoint spec to extract base name and optional variant
|
|
82
|
+
# Format can be: "name", "name.variant", or "name:uuid"
|
|
83
|
+
# Split by ':' first to handle "name:uuid" or "name.variant:uuid"
|
|
84
|
+
name_part = endpoint_spec.split(":")[0]
|
|
85
|
+
|
|
86
|
+
# Check if user specified a variant
|
|
87
|
+
if "." in name_part:
|
|
88
|
+
base_name, variant_name = name_part.split(".", 1)
|
|
89
|
+
else:
|
|
90
|
+
base_name = name_part
|
|
91
|
+
variant_name = None
|
|
92
|
+
|
|
93
|
+
original_content = content
|
|
94
|
+
content = remove_endpoint_from_script(content, base_name, variant_name)
|
|
95
|
+
|
|
96
|
+
if content != original_content:
|
|
97
|
+
removed_any = True
|
|
98
|
+
else:
|
|
99
|
+
if variant_name:
|
|
100
|
+
console.print(
|
|
101
|
+
f"[yellow]Variant '{base_name}.{variant_name}' not found in {script}[/yellow]"
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
console.print(
|
|
105
|
+
f"[yellow]Endpoint '{base_name}' not found in {script}[/yellow]"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if removed_any:
|
|
109
|
+
script.write_text(content)
|
|
110
|
+
console.print(
|
|
111
|
+
f"[green]Removed endpoint configuration(s) from {script}[/green]"
|
|
112
|
+
)
|
groundhog_hpc/app/run.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""Run command for executing Groundhog scripts on Globus Compute endpoints."""
|
|
2
2
|
|
|
3
|
+
import inspect
|
|
3
4
|
import os
|
|
4
5
|
import sys
|
|
5
6
|
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
6
8
|
|
|
7
9
|
import typer
|
|
8
10
|
|
|
@@ -13,6 +15,7 @@ from groundhog_hpc.app.utils import (
|
|
|
13
15
|
from groundhog_hpc.configuration.pep723 import read_pep723
|
|
14
16
|
from groundhog_hpc.errors import RemoteExecutionError
|
|
15
17
|
from groundhog_hpc.harness import Harness
|
|
18
|
+
from groundhog_hpc.logging import setup_logging
|
|
16
19
|
from groundhog_hpc.utils import (
|
|
17
20
|
get_groundhog_version_spec,
|
|
18
21
|
import_user_script,
|
|
@@ -20,7 +23,38 @@ from groundhog_hpc.utils import (
|
|
|
20
23
|
)
|
|
21
24
|
|
|
22
25
|
|
|
26
|
+
def invoke_harness_with_args(harness: Harness, args: list[str]) -> Any:
|
|
27
|
+
"""Parse CLI args and invoke harness function.
|
|
28
|
+
|
|
29
|
+
Reproduces typer.run() logic but with explicit args and standalone_mode=False
|
|
30
|
+
to capture return values and exceptions instead of sys.exit().
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
harness: The harness to invoke
|
|
34
|
+
args: CLI arguments to parse (e.g., ["arg1", "--count=5"])
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The return value from the harness function
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
SystemExit: If argument parsing fails (from Click/Typer)
|
|
41
|
+
Any exception raised by the harness function
|
|
42
|
+
"""
|
|
43
|
+
original_argv = sys.argv
|
|
44
|
+
# Use harness name for better help/error messages
|
|
45
|
+
sys.argv = [harness.func.__name__] + args
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
app = typer.Typer(add_completion=False)
|
|
49
|
+
app.command()(harness.func)
|
|
50
|
+
result = app(standalone_mode=False)
|
|
51
|
+
return result
|
|
52
|
+
finally:
|
|
53
|
+
sys.argv = original_argv
|
|
54
|
+
|
|
55
|
+
|
|
23
56
|
def run(
|
|
57
|
+
ctx: typer.Context,
|
|
24
58
|
script: Path = typer.Argument(
|
|
25
59
|
..., help="Path to script with PEP 723 dependencies to deploy to the endpoint"
|
|
26
60
|
),
|
|
@@ -32,11 +66,34 @@ def run(
|
|
|
32
66
|
"--no-fun-allowed",
|
|
33
67
|
help="Suppress emoji output\n\n[env: GROUNDHOG_NO_FUN_ALLOWED=]",
|
|
34
68
|
),
|
|
69
|
+
log_level: str = typer.Option(
|
|
70
|
+
None,
|
|
71
|
+
"--log-level",
|
|
72
|
+
help="Set logging level (DEBUG, INFO, WARNING, ERROR)\n\n[env: GROUNDHOG_LOG_LEVEL=]",
|
|
73
|
+
),
|
|
35
74
|
) -> None:
|
|
36
|
-
"""Run a Python script on a Globus Compute endpoint.
|
|
75
|
+
"""Run a Python script on a Globus Compute endpoint.
|
|
76
|
+
|
|
77
|
+
Use -- to pass arguments to parameterized harnesses:
|
|
78
|
+
hog run script.py harness -- arg1 --option=value
|
|
79
|
+
"""
|
|
80
|
+
# Handle the -- separator for harness arguments
|
|
81
|
+
# ctx.args may contain ['--', 'arg1', 'arg2'] - strip the '--' if present
|
|
82
|
+
harness_args = ctx.args # List of extra args, or empty list
|
|
83
|
+
if harness_args and harness_args[0] == "--":
|
|
84
|
+
harness_args = harness_args[1:] # Strip leading '--'
|
|
85
|
+
if harness == "--":
|
|
86
|
+
# User typed: hog run script.py -- args
|
|
87
|
+
# Use default harness "main" and shift args
|
|
88
|
+
harness = "main"
|
|
37
89
|
if no_fun_allowed:
|
|
38
90
|
os.environ["GROUNDHOG_NO_FUN_ALLOWED"] = str(no_fun_allowed)
|
|
39
91
|
|
|
92
|
+
if log_level:
|
|
93
|
+
os.environ["GROUNDHOG_LOG_LEVEL"] = log_level.upper()
|
|
94
|
+
# Reconfigure logging with the new level
|
|
95
|
+
setup_logging()
|
|
96
|
+
|
|
40
97
|
script_path = script.resolve()
|
|
41
98
|
if not script_path.exists():
|
|
42
99
|
typer.echo(f"Error: Script '{script_path}' not found", err=True)
|
|
@@ -87,7 +144,18 @@ def run(
|
|
|
87
144
|
)
|
|
88
145
|
raise typer.Exit(1)
|
|
89
146
|
|
|
90
|
-
|
|
147
|
+
# Dispatch based on whether harness arguments were provided
|
|
148
|
+
if not harness_args:
|
|
149
|
+
# No extra args: zero-arg call (backward compatible)
|
|
150
|
+
result = harness_func()
|
|
151
|
+
else:
|
|
152
|
+
# Has extra args: parse and invoke parameterized harness
|
|
153
|
+
sig = inspect.signature(harness_func.func)
|
|
154
|
+
if len(sig.parameters) == 0:
|
|
155
|
+
typer.echo(f"Error: Harness '{harness}' takes no arguments", err=True)
|
|
156
|
+
raise typer.Exit(1)
|
|
157
|
+
result = invoke_harness_with_args(harness_func, harness_args)
|
|
158
|
+
|
|
91
159
|
typer.echo(result)
|
|
92
160
|
except RemoteExecutionError as e:
|
|
93
161
|
if e.returncode == 124:
|
groundhog_hpc/compute.py
CHANGED
|
@@ -5,6 +5,7 @@ ShellFunctions, registering them, and submitting them for execution on remote
|
|
|
5
5
|
endpoints.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import logging
|
|
8
9
|
import os
|
|
9
10
|
import warnings
|
|
10
11
|
from functools import lru_cache
|
|
@@ -14,6 +15,8 @@ from uuid import UUID
|
|
|
14
15
|
from groundhog_hpc.future import GroundhogFuture
|
|
15
16
|
from groundhog_hpc.templating import template_shell_command
|
|
16
17
|
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
17
20
|
warnings.filterwarnings(
|
|
18
21
|
"ignore",
|
|
19
22
|
category=UserWarning,
|
|
@@ -92,10 +95,21 @@ def submit_to_executor(
|
|
|
92
95
|
expected_keys = set(schema.get("properties", {}).keys())
|
|
93
96
|
unexpected_keys = set(config.keys()) - expected_keys
|
|
94
97
|
if unexpected_keys:
|
|
98
|
+
logger.debug(
|
|
99
|
+
f"Filtering unexpected config keys for endpoint {endpoint}: {unexpected_keys}"
|
|
100
|
+
)
|
|
95
101
|
config = {k: v for k, v in config.items() if k not in unexpected_keys}
|
|
96
102
|
|
|
103
|
+
logger.debug(f"Creating Globus Compute executor for endpoint {endpoint}")
|
|
97
104
|
with gc.Executor(endpoint, user_endpoint_config=config) as executor:
|
|
105
|
+
func_name = getattr(
|
|
106
|
+
shell_function, "__name__", getattr(shell_function, "name", "unknown")
|
|
107
|
+
)
|
|
108
|
+
logger.info(f"Submitting function '{func_name}' to endpoint '{endpoint}'")
|
|
98
109
|
future = executor.submit(shell_function)
|
|
110
|
+
task_id = getattr(future, "task_id", None)
|
|
111
|
+
if task_id:
|
|
112
|
+
logger.info(f"Task submitted with ID: {task_id}")
|
|
99
113
|
deserializing_future = GroundhogFuture(future)
|
|
100
114
|
return deserializing_future
|
|
101
115
|
|
|
@@ -114,7 +128,8 @@ def get_task_status(task_id: str | UUID | None) -> dict[str, Any]:
|
|
|
114
128
|
return {"status": "status pending", "exception": None}
|
|
115
129
|
|
|
116
130
|
client = _get_compute_client()
|
|
117
|
-
|
|
131
|
+
task_status = client.get_task(task_id)
|
|
132
|
+
return task_status
|
|
118
133
|
|
|
119
134
|
|
|
120
135
|
@lru_cache
|