groundhog-hpc 0.5.7__py3-none-any.whl → 0.7.1__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/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
- app.command(no_args_is_help=True)(run)
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/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
 
@@ -21,7 +23,38 @@ from groundhog_hpc.utils import (
21
23
  )
22
24
 
23
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
+
24
56
  def run(
57
+ ctx: typer.Context,
25
58
  script: Path = typer.Argument(
26
59
  ..., help="Path to script with PEP 723 dependencies to deploy to the endpoint"
27
60
  ),
@@ -39,7 +72,20 @@ def run(
39
72
  help="Set logging level (DEBUG, INFO, WARNING, ERROR)\n\n[env: GROUNDHOG_LOG_LEVEL=]",
40
73
  ),
41
74
  ) -> None:
42
- """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"
43
89
  if no_fun_allowed:
44
90
  os.environ["GROUNDHOG_NO_FUN_ALLOWED"] = str(no_fun_allowed)
45
91
 
@@ -98,7 +144,18 @@ def run(
98
144
  )
99
145
  raise typer.Exit(1)
100
146
 
101
- result = harness_func()
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
+
102
159
  typer.echo(result)
103
160
  except RemoteExecutionError as e:
104
161
  if e.returncode == 124:
@@ -20,22 +20,36 @@ logger = logging.getLogger(__name__)
20
20
  def harness() -> Callable[[FunctionType], Harness]:
21
21
  """Decorator to mark a function as a local orchestrator harness.
22
22
 
23
+ Harness functions are entry points that coordinate remote function calls.
24
+ They run locally and can accept parameters passed as CLI arguments.
25
+
23
26
  Harness functions:
24
27
 
25
- - Must be called via the CLI: `hog run script.py harness_name`
26
- - Cannot accept any arguments
28
+ - Are invoked via the CLI: `hog run script.py [harness_name]`
29
+ - Can accept parameters, which map to CLI arguments
27
30
  - Can call `.remote()` or `.submit()` on `@hog.function`-decorated functions
28
31
 
29
32
  Returns:
30
33
  A decorator function that wraps the harness
31
34
 
32
35
  Example:
36
+ Zero-argument harness:
33
37
  ```python
34
38
  @hog.harness()
35
39
  def main():
36
40
  result = my_function.remote("far out, man!")
37
41
  return result
38
42
  ```
43
+
44
+ Parameterized harness:
45
+ ```python
46
+ @hog.harness()
47
+ def train(dataset: str, epochs: int = 10):
48
+ result = train_model.remote(dataset, epochs)
49
+ return result
50
+ ```
51
+
52
+ Run with: `hog run script.py train -- my_data --epochs=20`
39
53
  """
40
54
 
41
55
  def decorator(func: FunctionType) -> Harness:
groundhog_hpc/harness.py CHANGED
@@ -1,7 +1,8 @@
1
1
  """Harness wrapper for orchestrating remote function execution.
2
2
 
3
- This module provides the Harness class, which wraps zero-argument entry point
4
- functions that orchestrate calls to remote @hog.function decorated functions.
3
+ This module provides the Harness class, which wraps entry point functions that
4
+ orchestrate calls to remote @hog.function decorated functions. Harnesses can
5
+ accept parameters which are parsed from CLI arguments via `hog run`.
5
6
  """
6
7
 
7
8
  import inspect
@@ -10,13 +11,15 @@ from typing import Any
10
11
 
11
12
 
12
13
  class Harness:
13
- """Wrapper for a zero-argument orchestrator function.
14
+ """Wrapper for an orchestrator function.
14
15
 
15
16
  Harness functions are entry points that typically coordinate calls to
16
- @hog.function decorated functions. They must not accept any arguments.
17
+ @hog.function decorated functions. They can accept parameters that are
18
+ parsed from CLI arguments when invoked via `hog run script.py -- args`.
17
19
 
18
20
  Attributes:
19
21
  func: The wrapped orchestrator function
22
+ signature: The function's signature for CLI argument parsing
20
23
  """
21
24
 
22
25
  def __init__(self, func: FunctionType):
@@ -24,25 +27,18 @@ class Harness:
24
27
 
25
28
  Args:
26
29
  func: The orchestrator function to wrap
27
-
28
- Raises:
29
- TypeError: If the function accepts any arguments
30
30
  """
31
31
  self.func: FunctionType = func
32
- self._validate_signature()
32
+ self.signature: inspect.Signature = inspect.signature(func)
33
+
34
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
35
+ """Execute the harness function with optional arguments.
33
36
 
34
- def __call__(self) -> Any:
35
- """Execute the harness function.
37
+ Args:
38
+ *args: Positional arguments to pass to the harness function
39
+ **kwargs: Keyword arguments to pass to the harness function
36
40
 
37
41
  Returns:
38
42
  The result of the harness function execution
39
43
  """
40
- return self.func()
41
-
42
- def _validate_signature(self) -> None:
43
- sig = inspect.signature(self.func)
44
- if len(sig.parameters) > 0:
45
- raise TypeError(
46
- f"Harness function '{self.func.__qualname__}' must not accept any arguments, "
47
- f"but has parameters: {list(sig.parameters.keys())}"
48
- )
44
+ return self.func(*args, **kwargs)
@@ -37,10 +37,12 @@ mkdir -p "$UV_CACHE_DIR" "$UV_PYTHON_INSTALL_DIR"
37
37
  {% if log_level %}
38
38
  # Local override - use value from dispatching environment
39
39
  export GROUNDHOG_LOG_LEVEL="{{ log_level }}"
40
+ export RUST_LOG=uv="${{GROUNDHOG_LOG_LEVEL}}"
40
41
  {% else %}
41
42
  {% raw %}
42
43
  # Respect remote environment if set, otherwise default to WARNING
43
44
  export GROUNDHOG_LOG_LEVEL="${{GROUNDHOG_LOG_LEVEL:-WARNING}}"
45
+ export RUST_LOG=uv="${{GROUNDHOG_LOG_LEVEL:-WARNING}}"
44
46
  {% endraw %}
45
47
  {% endif %}
46
48
 
@@ -57,6 +59,7 @@ cat > {{ script_name }}.in << 'PAYLOAD_EOF'
57
59
  PAYLOAD_EOF
58
60
 
59
61
  "$UV_BIN" run --with {{ version_spec }} \
62
+ --exclude-newer-package groundhog-hpc={{ groundhog_timestamp }} \
60
63
  {{ runner_name }}.py
61
64
 
62
65
  echo "__GROUNDHOG_RESULT__"
@@ -10,6 +10,7 @@ user functions remotely. It creates shell commands that:
10
10
  import logging
11
11
  import os
12
12
  import uuid
13
+ from datetime import datetime, timezone
13
14
  from hashlib import sha1
14
15
  from pathlib import Path
15
16
 
@@ -74,6 +75,10 @@ def template_shell_command(script_path: str, function_name: str, payload: str) -
74
75
  version_spec = get_groundhog_version_spec()
75
76
  logger.debug(f"Using groundhog version spec: {version_spec}")
76
77
 
78
+ # Generate timestamp for groundhog-hpc exclude-newer override
79
+ # This allows groundhog to bypass user's exclude-newer restrictions
80
+ groundhog_timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
81
+
77
82
  # Load runner template
78
83
  templates_dir = Path(__file__).parent / "templates"
79
84
  jinja_env = Environment(loader=FileSystemLoader(templates_dir))
@@ -106,6 +111,7 @@ def template_shell_command(script_path: str, function_name: str, payload: str) -
106
111
  version_spec=version_spec,
107
112
  payload=payload,
108
113
  log_level=local_log_level,
114
+ groundhog_timestamp=groundhog_timestamp,
109
115
  )
110
116
 
111
117
  logger.debug(f"Generated shell command ({len(shell_command_string)} chars)")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: groundhog-hpc
3
- Version: 0.5.7
3
+ Version: 0.7.1
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
@@ -1,22 +1,22 @@
1
1
  groundhog_hpc/__init__.py,sha256=BWOp2bKdV2Oz1Ubd89a-xF6XVEHqB9HZUaSk88T9o_Y,1513
2
2
  groundhog_hpc/compute.py,sha256=wQnjfYR4FSo2z2BJLVr99OIXz_AjRbiRy4-6qW0W2uE,4819
3
3
  groundhog_hpc/console.py,sha256=lipheM-NECkcqjK-ehzyD_zG-FzyJQrNT__4tGNFMr0,6941
4
- groundhog_hpc/decorators.py,sha256=XkP3Y2D6tX9KAxxY_P0A8L-zISuwXTupaP7MuSrt19Y,4714
4
+ groundhog_hpc/decorators.py,sha256=njmmv45stY8ou6mK8tlF6XmRqrfqOaQJ55bPVqGYq1Y,5204
5
5
  groundhog_hpc/errors.py,sha256=qDXuKYso6ZPIq64qsuBwL8yY27LGk9fe4rtvu1YlRtc,3933
6
6
  groundhog_hpc/function.py,sha256=H0wxI0sdMj6T8jpEW4yb5FUpXAr9Yow7HPCFk_NTaB4,14445
7
7
  groundhog_hpc/future.py,sha256=2_zNXAvIIurFwa9kcPW0cDL-W79sHSufIA1qaj1a9gU,7074
8
- groundhog_hpc/harness.py,sha256=7aCIHzvahwvNOj-vBnzOQ9TV9fpCNnoXiFCa6I3Dkkc,1427
8
+ groundhog_hpc/harness.py,sha256=YjZ6IRmjukofIjqXGnUN9THSwpw8XNIwtC17wv0ImaY,1478
9
9
  groundhog_hpc/import_hook.py,sha256=McNT9Lyppp3qriWITrlpCyd0WVJiQsizKM8EGT_VZPg,4241
10
10
  groundhog_hpc/logging.py,sha256=_SSI9jw5YYvFXZdIkWqr-P05WvWQ1fm2UPTuwqLh9dQ,1699
11
11
  groundhog_hpc/serialization.py,sha256=tbUY0d7mNoLGs_u4-ahyqvfK1kdkjBHli54hwiaLWlE,7642
12
- groundhog_hpc/templating.py,sha256=HnNQc-b72BcnsK8o7dZ4g5RUOI5XTRbJXeiExXGrgm4,4306
12
+ groundhog_hpc/templating.py,sha256=L32Cv1RrLqnTf719i8NGPVHKUCKuw14oGxQX9aif_uA,4618
13
13
  groundhog_hpc/utils.py,sha256=ig7mEgmhf-JJ_LQq81yUtZzxstk27Ns2I2P-wC3QS5g,7611
14
14
  groundhog_hpc/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  groundhog_hpc/app/add.py,sha256=zZW0eWnwRWJwpXL8V_Saaeb6X2D3G_y5ws19mDk0v3g,4534
16
16
  groundhog_hpc/app/init.py,sha256=7F6qbWJe6cEoAxLdWumqPDNrHHbqFHzSLpvHOXhQdMQ,5005
17
- groundhog_hpc/app/main.py,sha256=mZRraUNXHl-60r8UFh65lpJ9_7294QwSYZAXfu2IKYc,1146
17
+ groundhog_hpc/app/main.py,sha256=c6aDcGRaXQShAEc1BtG0u7JFD3P8hIFNtGFEiAEikIg,1310
18
18
  groundhog_hpc/app/remove.py,sha256=0BbUqGTy4Xj5PDegr6oAirdZc01GuYxv4CH4hYMRn-4,3872
19
- groundhog_hpc/app/run.py,sha256=p1EcUl7Ct6d40HRF814X5JmQU3q3PQ697l_oyO3GL6w,4030
19
+ groundhog_hpc/app/run.py,sha256=xH7d4JzTtPqMDeWrXiIlyP-1aLRCfEkTjFZGyJqmQ4o,6141
20
20
  groundhog_hpc/app/utils.py,sha256=3XfES3Vp4l9m2JEmFeADdFXxTj5g1QMakbvaP3XLQP4,5434
21
21
  groundhog_hpc/configuration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  groundhog_hpc/configuration/defaults.py,sha256=b6JMKI-1rt5u9XZU1APND7BZucIrG4AqIh8gat6hARo,218
@@ -26,9 +26,9 @@ groundhog_hpc/configuration/pep723.py,sha256=TNbd2_UxOb_dMwtKcEJAJq62dC7SFdu_lt-
26
26
  groundhog_hpc/configuration/resolver.py,sha256=VoYZ0S2X7BF6wOXz9NaxS71vqANYRsVi5pYT1B8fRAE,10326
27
27
  groundhog_hpc/templates/groundhog_run.py.jinja,sha256=-MwRi_k9LswYhi7pvnPwR-SzwBXvrugLofBc_-Yteuk,633
28
28
  groundhog_hpc/templates/init_script.py.jinja,sha256=O3-DkastzzpFl8XKsfI3EX3eM0fAsDqalPMCZHeULSE,1017
29
- groundhog_hpc/templates/shell_command.sh.jinja,sha256=CA_7XsLRzQc3xhQ6VcNt_sCRhKpyIQBXdgbZVa9Dtr4,1941
30
- groundhog_hpc-0.5.7.dist-info/METADATA,sha256=jEQNwFNXcotLHZUzvEUdzpS4tEBG-WScKFfMAJ-XCyM,2234
31
- groundhog_hpc-0.5.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
32
- groundhog_hpc-0.5.7.dist-info/entry_points.txt,sha256=Dk9LTPJ-3vxm-sGsAaddEawPpIDzhM-ZVuyHKjzUCd0,51
33
- groundhog_hpc-0.5.7.dist-info/licenses/LICENSE,sha256=4WiSzz3h9qYXwc4sFWWFzc45ws_0HLZJmbT6_69IBjs,1078
34
- groundhog_hpc-0.5.7.dist-info/RECORD,,
29
+ groundhog_hpc/templates/shell_command.sh.jinja,sha256=UOw3LUbOG8dtRzXnPhfnfr-AhpMLRjwquf89ZAo6inA,2110
30
+ groundhog_hpc-0.7.1.dist-info/METADATA,sha256=U47qNQgGWon9xKs4_OQxt3ZjO7v7NIco7rcV55wSDe0,2234
31
+ groundhog_hpc-0.7.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
32
+ groundhog_hpc-0.7.1.dist-info/entry_points.txt,sha256=Dk9LTPJ-3vxm-sGsAaddEawPpIDzhM-ZVuyHKjzUCd0,51
33
+ groundhog_hpc-0.7.1.dist-info/licenses/LICENSE,sha256=4WiSzz3h9qYXwc4sFWWFzc45ws_0HLZJmbT6_69IBjs,1078
34
+ groundhog_hpc-0.7.1.dist-info/RECORD,,