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.
Files changed (38) hide show
  1. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/.gitignore +1 -0
  2. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/PKG-INFO +12 -6
  3. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/README.md +10 -4
  4. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/pyproject.toml +7 -1
  5. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/__init__.py +4 -0
  6. groundhog_hpc-0.5.7/src/groundhog_hpc/app/add.py +136 -0
  7. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/app/init.py +54 -10
  8. groundhog_hpc-0.5.7/src/groundhog_hpc/app/remove.py +112 -0
  9. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/app/run.py +14 -2
  10. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/compute.py +23 -8
  11. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/configuration/defaults.py +1 -0
  12. groundhog_hpc-0.5.7/src/groundhog_hpc/configuration/endpoints.py +221 -0
  13. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/configuration/models.py +26 -3
  14. groundhog_hpc-0.5.7/src/groundhog_hpc/configuration/pep723.py +415 -0
  15. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/configuration/resolver.py +36 -8
  16. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/console.py +1 -1
  17. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/decorators.py +26 -24
  18. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/function.py +63 -36
  19. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/future.py +48 -10
  20. groundhog_hpc-0.5.7/src/groundhog_hpc/logging.py +51 -0
  21. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/serialization.py +22 -2
  22. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/templates/init_script.py.jinja +3 -5
  23. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/templates/shell_command.sh.jinja +15 -1
  24. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/templating.py +17 -0
  25. groundhog_hpc-0.5.5/src/groundhog_hpc/app/add.py +0 -62
  26. groundhog_hpc-0.5.5/src/groundhog_hpc/app/remove.py +0 -37
  27. groundhog_hpc-0.5.5/src/groundhog_hpc/configuration/endpoints.py +0 -354
  28. groundhog_hpc-0.5.5/src/groundhog_hpc/configuration/pep723.py +0 -139
  29. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/LICENSE +0 -0
  30. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/app/__init__.py +0 -0
  31. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/app/main.py +0 -0
  32. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/app/utils.py +0 -0
  33. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/configuration/__init__.py +0 -0
  34. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/errors.py +0 -0
  35. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/harness.py +0 -0
  36. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/import_hook.py +0 -0
  37. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/templates/groundhog_run.py.jinja +0 -0
  38. {groundhog_hpc-0.5.5 → groundhog_hpc-0.5.7}/src/groundhog_hpc/utils.py +0 -0
@@ -13,3 +13,4 @@ wheels/
13
13
  /CLAUDE.md
14
14
  /.DS_Store
15
15
  /.coverage
16
+ /docs/plans/*.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: groundhog-hpc
3
- Version: 0.5.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.10"
40
- # dependencies = ["numpy"]
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="your-endpoint-id", account="your-account")
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/README.md](./examples/README.md)
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.10"
17
- # dependencies = ["numpy"]
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="your-endpoint-id", account="your-account")
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/README.md](./examples/README.md)
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
- "tomli-w>=1.0.0",
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
- fetch_and_format_endpoints,
15
+ get_endpoint_schema_comments,
16
+ parse_endpoint_spec,
15
17
  )
16
- from groundhog_hpc.configuration.pep723 import Pep723Metadata
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
- # Fetch and format endpoint configurations if provided
71
- endpoint_blocks = []
87
+ # Parse endpoint specs if provided
88
+ endpoint_specs = []
72
89
  if endpoints:
73
90
  try:
74
- endpoint_blocks = fetch_and_format_endpoints(endpoints)
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
- endpoint_blocks=endpoint_blocks,
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 endpoint_blocks:
136
+ if endpoint_specs:
93
137
  console.print("\nNext steps:")
94
138
  console.print(
95
- f" 1. Update fields in the \\[tool.hog.{endpoint_blocks[0].name}] block"
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 - try "
96
- "increasing walltime for long running jobs)",
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, function_name: str, payload: 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
- return client.get_task(task_id)
131
+ task_status = client.get_task(task_id)
132
+ return task_status
118
133
 
119
134
 
120
135
  @lru_cache
@@ -1,6 +1,7 @@
1
1
  # Default Globus Compute Executor configuration
2
2
  DEFAULT_USER_CONFIG = {
3
3
  "worker_init": "",
4
+ "endpoint_setup": "",
4
5
  }
5
6
 
6
7
  # default maximum execution time for remote functions (in seconds)