groundhog-hpc 0.5.5__tar.gz → 0.5.7__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.
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/.gitignore +1 -0
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/PKG-INFO +12 -6
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/README.md +10 -4
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/pyproject.toml +7 -1
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/__init__.py +4 -0
- groundhog_hpc-0.5.7/src/groundhog_hpc/app/add.py +136 -0
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/app/init.py +54 -10
- groundhog_hpc-0.5.7/src/groundhog_hpc/app/remove.py +112 -0
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/app/run.py +14 -2
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/compute.py +23 -8
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/configuration/defaults.py +1 -0
- groundhog_hpc-0.5.7/src/groundhog_hpc/configuration/endpoints.py +221 -0
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/configuration/models.py +26 -3
- groundhog_hpc-0.5.7/src/groundhog_hpc/configuration/pep723.py +415 -0
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/configuration/resolver.py +36 -8
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/console.py +1 -1
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/decorators.py +26 -24
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/function.py +63 -36
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/future.py +48 -10
- groundhog_hpc-0.5.7/src/groundhog_hpc/logging.py +51 -0
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/serialization.py +22 -2
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/templates/init_script.py.jinja +3 -5
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/templates/shell_command.sh.jinja +15 -1
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/templating.py +17 -0
- groundhog_hpc-0.5.5/src/groundhog_hpc/app/add.py +0 -62
- groundhog_hpc-0.5.5/src/groundhog_hpc/app/remove.py +0 -37
- groundhog_hpc-0.5.5/src/groundhog_hpc/configuration/endpoints.py +0 -354
- groundhog_hpc-0.5.5/src/groundhog_hpc/configuration/pep723.py +0 -139
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/LICENSE +0 -0
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/app/__init__.py +0 -0
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/app/main.py +0 -0
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/app/utils.py +0 -0
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/configuration/__init__.py +0 -0
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/errors.py +0 -0
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/harness.py +0 -0
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/import_hook.py +0 -0
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/templates/groundhog_run.py.jinja +0 -0
- {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: groundhog-hpc
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.7
|
|
4
4
|
Summary: Iterative HPC function development. As many 'first tries' as you need.
|
|
5
5
|
Author-email: Owen Price Skelly <OwenPriceSkelly@uchicago.edu>
|
|
6
6
|
License: MIT
|
|
@@ -15,8 +15,8 @@ Requires-Dist: packaging>=24.0
|
|
|
15
15
|
Requires-Dist: proxystore>=0.8.3
|
|
16
16
|
Requires-Dist: pydantic>=2.0.0
|
|
17
17
|
Requires-Dist: rich>=13.0.0
|
|
18
|
-
Requires-Dist: tomli-w>=1.0.0
|
|
19
18
|
Requires-Dist: tomli>=1.1.0; python_full_version < '3.11'
|
|
19
|
+
Requires-Dist: tomlkit>=0.12.0
|
|
20
20
|
Requires-Dist: typer>=0.16.1
|
|
21
21
|
Requires-Dist: uv>=0.9.5
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
@@ -36,13 +36,19 @@ Groundhog automatically manages remote environments (powered by [uv](https://doc
|
|
|
36
36
|
|
|
37
37
|
```python
|
|
38
38
|
# /// script
|
|
39
|
-
# requires-python = ">=3.
|
|
40
|
-
# dependencies = [
|
|
39
|
+
# requires-python = ">=3.12,<3.13"
|
|
40
|
+
# dependencies = [
|
|
41
|
+
# numpy,
|
|
42
|
+
# ]
|
|
43
|
+
#
|
|
44
|
+
# [tool.hog.tutorial] # Globus Compute Tutorial Endpoint
|
|
45
|
+
# endpoint = "4b116d3c-1703-4f8f-9f6f-39921e5864df"
|
|
46
|
+
#
|
|
41
47
|
# ///
|
|
42
48
|
|
|
43
49
|
import groundhog_hpc as hog
|
|
44
50
|
|
|
45
|
-
@hog.function(endpoint=
|
|
51
|
+
@hog.function(endpoint='tutorial') # points to [tool.hog.tutorial] config
|
|
46
52
|
def compute(x: int) -> int:
|
|
47
53
|
import numpy as np
|
|
48
54
|
return int(np.sum(range(x)))
|
|
@@ -57,4 +63,4 @@ Run with: `hog run myscript.py main`
|
|
|
57
63
|
|
|
58
64
|
---
|
|
59
65
|
|
|
60
|
-
see also: [examples
|
|
66
|
+
see also: [examples](https://groundhog-hpc.readthedocs.io/en/latest/examples/)
|
|
@@ -13,13 +13,19 @@ Groundhog automatically manages remote environments (powered by [uv](https://doc
|
|
|
13
13
|
|
|
14
14
|
```python
|
|
15
15
|
# /// script
|
|
16
|
-
# requires-python = ">=3.
|
|
17
|
-
# dependencies = [
|
|
16
|
+
# requires-python = ">=3.12,<3.13"
|
|
17
|
+
# dependencies = [
|
|
18
|
+
# numpy,
|
|
19
|
+
# ]
|
|
20
|
+
#
|
|
21
|
+
# [tool.hog.tutorial] # Globus Compute Tutorial Endpoint
|
|
22
|
+
# endpoint = "4b116d3c-1703-4f8f-9f6f-39921e5864df"
|
|
23
|
+
#
|
|
18
24
|
# ///
|
|
19
25
|
|
|
20
26
|
import groundhog_hpc as hog
|
|
21
27
|
|
|
22
|
-
@hog.function(endpoint=
|
|
28
|
+
@hog.function(endpoint='tutorial') # points to [tool.hog.tutorial] config
|
|
23
29
|
def compute(x: int) -> int:
|
|
24
30
|
import numpy as np
|
|
25
31
|
return int(np.sum(range(x)))
|
|
@@ -34,4 +40,4 @@ Run with: `hog run myscript.py main`
|
|
|
34
40
|
|
|
35
41
|
---
|
|
36
42
|
|
|
37
|
-
see also: [examples
|
|
43
|
+
see also: [examples](https://groundhog-hpc.readthedocs.io/en/latest/examples/)
|
|
@@ -12,7 +12,7 @@ dependencies = [
|
|
|
12
12
|
"pydantic>=2.0.0",
|
|
13
13
|
"rich>=13.0.0",
|
|
14
14
|
"tomli>=1.1.0 ; python_full_version < '3.11'",
|
|
15
|
-
"
|
|
15
|
+
"tomlkit>=0.12.0",
|
|
16
16
|
"typer>=0.16.1",
|
|
17
17
|
"uv>=0.9.5",
|
|
18
18
|
]
|
|
@@ -65,6 +65,12 @@ dev = [
|
|
|
65
65
|
"ruff>=0.12.11",
|
|
66
66
|
"ty>=0.0.1a21",
|
|
67
67
|
]
|
|
68
|
+
docs = [
|
|
69
|
+
"mkdocs>=1.6.0",
|
|
70
|
+
"mkdocs-material>=9.5.0",
|
|
71
|
+
"mkdocstrings[python]>=0.26.0",
|
|
72
|
+
"click<=8.2.1", # NOTE: can be changed to >=8.3.2 when released
|
|
73
|
+
]
|
|
68
74
|
|
|
69
75
|
|
|
70
76
|
[tool.ruff.lint]
|
|
@@ -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()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Add command for managing PEP 723 script dependencies."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
import uv
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from groundhog_hpc.app.utils import (
|
|
12
|
+
normalize_python_version_with_uv,
|
|
13
|
+
update_requires_python,
|
|
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
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
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
|
+
|
|
32
|
+
|
|
33
|
+
def add(
|
|
34
|
+
script: Path = typer.Argument(..., help="Path to the script to modify"),
|
|
35
|
+
packages: list[str] | None = typer.Argument(None, help="Packages to add"),
|
|
36
|
+
requirements: list[Path] | None = typer.Option(
|
|
37
|
+
None, "--requirements", "--requirement", "-r", help="Add dependencies from file"
|
|
38
|
+
),
|
|
39
|
+
python: str | None = typer.Option(
|
|
40
|
+
None, "--python", "-p", help="Python version specifier"
|
|
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
|
+
),
|
|
56
|
+
) -> None:
|
|
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
|
+
|
|
63
|
+
if not script.exists():
|
|
64
|
+
console.print(f"[red]Error: Script '{script}' not found[/red]")
|
|
65
|
+
raise typer.Exit(1)
|
|
66
|
+
|
|
67
|
+
# handle --python flag separately
|
|
68
|
+
if python:
|
|
69
|
+
try:
|
|
70
|
+
normalized_python = normalize_python_version_with_uv(python)
|
|
71
|
+
except subprocess.CalledProcessError as e:
|
|
72
|
+
console.print(f"[red]{e.stderr.strip()}[/red]")
|
|
73
|
+
raise typer.Exit(1)
|
|
74
|
+
|
|
75
|
+
update_requires_python(script, normalized_python)
|
|
76
|
+
console.print(f"[green]Updated Python requirement in {script}[/green]")
|
|
77
|
+
|
|
78
|
+
packages, requirements = packages or [], requirements or []
|
|
79
|
+
if packages or requirements:
|
|
80
|
+
cmd = [f"{uv.find_uv_bin()}", "add", "--script", str(script)]
|
|
81
|
+
cmd += packages
|
|
82
|
+
|
|
83
|
+
for req_file in requirements:
|
|
84
|
+
cmd += ["-r", str(req_file)]
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
subprocess.run(
|
|
88
|
+
cmd,
|
|
89
|
+
check=True,
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True,
|
|
92
|
+
)
|
|
93
|
+
console.print(f"[green]Added dependencies to {script}[/green]")
|
|
94
|
+
except subprocess.CalledProcessError as e:
|
|
95
|
+
console.print(f"[red]{e.stderr.strip()}[/red]")
|
|
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]")
|
|
@@ -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:
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Remove command for managing PEP 723 script dependencies."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
import uv
|
|
9
|
+
from rich.console import Console
|
|
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
|
+
|
|
15
|
+
console = Console()
|
|
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
|
+
|
|
24
|
+
|
|
25
|
+
def remove(
|
|
26
|
+
script: Path = typer.Argument(..., help="Path to the script to modify"),
|
|
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
|
+
),
|
|
44
|
+
) -> None:
|
|
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
|
+
|
|
51
|
+
# Validate script exists
|
|
52
|
+
if not script.exists():
|
|
53
|
+
console.print(f"[red]Error: Script '{script}' not found[/red]")
|
|
54
|
+
raise typer.Exit(1)
|
|
55
|
+
|
|
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
|
+
)
|
|
@@ -13,6 +13,7 @@ from groundhog_hpc.app.utils import (
|
|
|
13
13
|
from groundhog_hpc.configuration.pep723 import read_pep723
|
|
14
14
|
from groundhog_hpc.errors import RemoteExecutionError
|
|
15
15
|
from groundhog_hpc.harness import Harness
|
|
16
|
+
from groundhog_hpc.logging import setup_logging
|
|
16
17
|
from groundhog_hpc.utils import (
|
|
17
18
|
get_groundhog_version_spec,
|
|
18
19
|
import_user_script,
|
|
@@ -32,11 +33,21 @@ def run(
|
|
|
32
33
|
"--no-fun-allowed",
|
|
33
34
|
help="Suppress emoji output\n\n[env: GROUNDHOG_NO_FUN_ALLOWED=]",
|
|
34
35
|
),
|
|
36
|
+
log_level: str = typer.Option(
|
|
37
|
+
None,
|
|
38
|
+
"--log-level",
|
|
39
|
+
help="Set logging level (DEBUG, INFO, WARNING, ERROR)\n\n[env: GROUNDHOG_LOG_LEVEL=]",
|
|
40
|
+
),
|
|
35
41
|
) -> None:
|
|
36
42
|
"""Run a Python script on a Globus Compute endpoint."""
|
|
37
43
|
if no_fun_allowed:
|
|
38
44
|
os.environ["GROUNDHOG_NO_FUN_ALLOWED"] = str(no_fun_allowed)
|
|
39
45
|
|
|
46
|
+
if log_level:
|
|
47
|
+
os.environ["GROUNDHOG_LOG_LEVEL"] = log_level.upper()
|
|
48
|
+
# Reconfigure logging with the new level
|
|
49
|
+
setup_logging()
|
|
50
|
+
|
|
40
51
|
script_path = script.resolve()
|
|
41
52
|
if not script_path.exists():
|
|
42
53
|
typer.echo(f"Error: Script '{script_path}' not found", err=True)
|
|
@@ -92,8 +103,9 @@ def run(
|
|
|
92
103
|
except RemoteExecutionError as e:
|
|
93
104
|
if e.returncode == 124:
|
|
94
105
|
typer.echo(
|
|
95
|
-
"Remote execution failed (timed out
|
|
96
|
-
"
|
|
106
|
+
"Remote execution failed: (exit code 124 - timed out). \nTry increasing walltime for "
|
|
107
|
+
"long running jobs by setting my_function.walltime (in seconds) "
|
|
108
|
+
"before invoking my_function.remote/submit()",
|
|
97
109
|
err=True,
|
|
98
110
|
)
|
|
99
111
|
raise
|
|
@@ -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,
|
|
@@ -44,7 +47,10 @@ def _get_compute_client() -> Client:
|
|
|
44
47
|
|
|
45
48
|
|
|
46
49
|
def script_to_submittable(
|
|
47
|
-
script_path: str,
|
|
50
|
+
script_path: str,
|
|
51
|
+
function_name: str,
|
|
52
|
+
payload: str,
|
|
53
|
+
walltime: int | float | None = None,
|
|
48
54
|
) -> ShellFunction:
|
|
49
55
|
"""Convert a user script and function name into a Globus Compute ShellFunction.
|
|
50
56
|
|
|
@@ -52,6 +58,7 @@ def script_to_submittable(
|
|
|
52
58
|
script_path: Path to the Python script containing the function
|
|
53
59
|
function_name: Name of the function to execute remotely
|
|
54
60
|
payload: Serialized arguments string
|
|
61
|
+
walltime: Optional maximum execution time in seconds for ShellFunction timeout
|
|
55
62
|
|
|
56
63
|
Returns:
|
|
57
64
|
A ShellFunction ready to be submitted to a Globus Compute executor
|
|
@@ -60,7 +67,7 @@ def script_to_submittable(
|
|
|
60
67
|
|
|
61
68
|
shell_command = template_shell_command(script_path, function_name, payload)
|
|
62
69
|
shell_function = gc.ShellFunction(
|
|
63
|
-
shell_command, name=function_name.replace(".", "_")
|
|
70
|
+
shell_command, name=function_name.replace(".", "_"), walltime=walltime
|
|
64
71
|
)
|
|
65
72
|
return shell_function
|
|
66
73
|
|
|
@@ -82,20 +89,27 @@ def submit_to_executor(
|
|
|
82
89
|
"""
|
|
83
90
|
import globus_compute_sdk as gc
|
|
84
91
|
|
|
85
|
-
# Extract walltime and set it on the shell function
|
|
86
|
-
config = user_endpoint_config.copy()
|
|
87
|
-
if "walltime" in config:
|
|
88
|
-
shell_function.walltime = config.pop("walltime")
|
|
89
|
-
|
|
90
92
|
# Validate config against endpoint schema and filter out unexpected keys
|
|
93
|
+
config = user_endpoint_config.copy()
|
|
91
94
|
if schema := get_endpoint_schema(endpoint):
|
|
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
|