groundhog-hpc 0.5.5__py3-none-any.whl → 0.5.7__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.
@@ -0,0 +1,51 @@
1
+ """Logging configuration for Groundhog HPC.
2
+
3
+ This module provides centralized logging setup with support for:
4
+ - Hierarchical per-module loggers (groundhog.compute, groundhog.serialization, etc.)
5
+ - Environment variable configuration (GROUNDHOG_LOG_LEVEL)
6
+ - CLI flag overrides
7
+ - Remote log level propagation
8
+ """
9
+
10
+ import logging
11
+ import os
12
+ import sys
13
+
14
+
15
+ def setup_logging() -> None:
16
+ """Configure the root groundhog logger.
17
+
18
+ Reads log level from GROUNDHOG_LOG_LEVEL environment variable.
19
+ Defaults to WARNING if not set.
20
+
21
+ Valid log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
22
+
23
+ Can be called multiple times to reconfigure the log level.
24
+ """
25
+ level_name = os.getenv("GROUNDHOG_LOG_LEVEL", "WARNING").upper()
26
+
27
+ # Convert string to logging level, default to WARNING if invalid
28
+ level = getattr(logging, level_name, logging.WARNING)
29
+
30
+ # Configure root groundhog logger
31
+ logger = logging.getLogger("groundhog_hpc")
32
+ logger.setLevel(level)
33
+
34
+ # Add stderr handler if not already present
35
+ if not logger.handlers:
36
+ handler = logging.StreamHandler(sys.stderr)
37
+ formatter = logging.Formatter(
38
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
39
+ datefmt="%Y-%m-%d %H:%M:%S",
40
+ )
41
+ handler.setFormatter(formatter)
42
+ logger.addHandler(handler)
43
+ else:
44
+ # Update level on existing handlers
45
+ for handler in logger.handlers:
46
+ handler.setLevel(level)
47
+
48
+ # Allow propagation to parent loggers (enables pytest caplog capture)
49
+ # This won't cause duplicate logs unless the root logger also has handlers,
50
+ # which is rare in production but common in tests
51
+ logger.propagate = True
@@ -1,6 +1,7 @@
1
1
  import atexit
2
2
  import base64
3
3
  import json
4
+ import logging
4
5
  import os
5
6
  import pickle
6
7
  import shutil
@@ -13,6 +14,8 @@ from proxystore.store import Store, get_store
13
14
 
14
15
  from groundhog_hpc.errors import DeserializationError, PayloadTooLargeError
15
16
 
17
+ logger = logging.getLogger(__name__)
18
+
16
19
  # Globus Compute payload size limit (10 MB)
17
20
  PAYLOAD_SIZE_LIMIT_BYTES = 10 * 1024 * 1024
18
21
 
@@ -137,18 +140,29 @@ def serialize(
137
140
  >>> serialize(maybe_large_obj, proxy_threshold_mb=5)
138
141
  """
139
142
  if use_proxy:
143
+ logger.debug("Using ProxyStore for serialization (explicitly requested)")
140
144
  return _proxy_serialize(obj)
141
145
 
142
146
  payload = _direct_serialize(obj)
143
147
  payload_size = len(payload.encode("utf-8"))
144
148
  payload_size_mb = payload_size / (1024 * 1024)
145
149
 
150
+ logger.debug(f"Payload size: {payload_size_mb:.2f}MB")
151
+
146
152
  if proxy_threshold_mb is not None and payload_size_mb > proxy_threshold_mb:
153
+ logger.warning(
154
+ f"Payload size {payload_size_mb:.1f}MB exceeds threshold {proxy_threshold_mb}MB, "
155
+ f"using ProxyStore for efficient transfer"
156
+ )
147
157
  return _proxy_serialize(obj)
148
158
 
149
159
  if payload_size > size_limit_bytes:
160
+ logger.error(
161
+ f"Payload size {payload_size_mb:.2f}MB exceeds limit {size_limit_bytes / (1024 * 1024):.2f}MB"
162
+ )
150
163
  raise PayloadTooLargeError(payload_size_mb)
151
164
 
165
+ logger.debug(f"Using direct serialization for {payload_size_mb:.2f}MB payload")
152
166
  return payload
153
167
 
154
168
 
@@ -187,6 +201,7 @@ def deserialize_stdout(stdout: str) -> tuple[str | None, Any]:
187
201
  user_output = None
188
202
 
189
203
  try:
204
+ logger.debug("Starting deserialization of stdout")
190
205
  if delimiter in stdout:
191
206
  parts = stdout.split(delimiter, 1)
192
207
  user_output = parts[0].rstrip(
@@ -196,8 +211,13 @@ def deserialize_stdout(stdout: str) -> tuple[str | None, Any]:
196
211
  "\n"
197
212
  ) # Remove leading newline from echo
198
213
 
199
- return user_output, deserialize(serialized_result)
214
+ result = deserialize(serialized_result)
215
+ logger.debug("Successfully deserialized result from stdout")
216
+ return user_output, result
200
217
  else:
201
- return None, deserialize(stdout)
218
+ result = deserialize(stdout)
219
+ logger.debug("Successfully deserialized result (no delimiter found)")
220
+ return None, result
202
221
  except Exception as e:
222
+ logger.error(f"Failed to deserialize result: {e}", exc_info=True)
203
223
  raise DeserializationError(user_output, e, stdout) from e
@@ -4,20 +4,18 @@
4
4
  #
5
5
  # [tool.uv]
6
6
  # exclude-newer = "{{ exclude_newer }}"
7
- # {% if endpoint_blocks %}{% for endpoint in endpoint_blocks %}
8
- {{ endpoint.toml_block }}
9
- # {% endfor %}{% else %}
7
+ # python-preference = "managed"
8
+ #
10
9
  # [tool.hog.my_endpoint]
11
10
  # endpoint = "TODO: Add Globus Compute endpoint UUID here"
12
11
  # # Uncomment and configure default options for "my_endpoint" here (available options vary by endpoint):
13
12
  # # account = "my-account"
14
13
  # # partition = "standard"
15
- # # walltime = 300 {% endif %}
16
14
  # ///
17
15
 
18
16
  import groundhog_hpc as hog
19
17
 
20
- @hog.function(endpoint="{{ endpoint_blocks[0].name if endpoint_blocks else 'my_endpoint' }}")
18
+ @hog.function(endpoint="{{ endpoint_name }}")
21
19
  def hello_world(name: str = "World") -> str:
22
20
  """Example function that can be run remotely on your HPC cluster. """
23
21
  return f"Hello, {name}!"
@@ -1,5 +1,8 @@
1
1
  set -euo pipefail
2
2
 
3
+ # Cleanup temporary files on exit
4
+ trap 'rm -f {{ user_script_name }}.py {{ runner_name }}.py {{ script_name }}.in {{ script_name }}.out' EXIT
5
+
3
6
  if command -v uv &> /dev/null; then
4
7
  UV_BIN=$(command -v uv)
5
8
  else
@@ -30,6 +33,17 @@ fi
30
33
  mkdir -p "$UV_CACHE_DIR" "$UV_PYTHON_INSTALL_DIR"
31
34
  {% endraw %}
32
35
 
36
+ # Propagate log level to remote environment
37
+ {% if log_level %}
38
+ # Local override - use value from dispatching environment
39
+ export GROUNDHOG_LOG_LEVEL="{{ log_level }}"
40
+ {% else %}
41
+ {% raw %}
42
+ # Respect remote environment if set, otherwise default to WARNING
43
+ export GROUNDHOG_LOG_LEVEL="${{GROUNDHOG_LOG_LEVEL:-WARNING}}"
44
+ {% endraw %}
45
+ {% endif %}
46
+
33
47
  cat > {{ user_script_name }}.py << 'USER_SCRIPT_EOF'
34
48
  {{ user_script_contents | escape_braces }}
35
49
  USER_SCRIPT_EOF
@@ -42,7 +56,7 @@ cat > {{ script_name }}.in << 'PAYLOAD_EOF'
42
56
  {{ payload }}
43
57
  PAYLOAD_EOF
44
58
 
45
- "$UV_BIN" run --managed-python --with {{ version_spec }} \
59
+ "$UV_BIN" run --with {{ version_spec }} \
46
60
  {{ runner_name }}.py
47
61
 
48
62
  echo "__GROUNDHOG_RESULT__"
@@ -7,6 +7,8 @@ user functions remotely. It creates shell commands that:
7
7
  3. Execute the runner with uv, which imports the user script, calls the function, and serializes results
8
8
  """
9
9
 
10
+ import logging
11
+ import os
10
12
  import uuid
11
13
  from hashlib import sha1
12
14
  from pathlib import Path
@@ -16,6 +18,8 @@ from jinja2 import Environment, FileSystemLoader
16
18
  from groundhog_hpc.configuration.pep723 import read_pep723, write_pep723
17
19
  from groundhog_hpc.utils import get_groundhog_version_spec, path_to_module_name
18
20
 
21
+ logger = logging.getLogger(__name__)
22
+
19
23
 
20
24
  def escape_braces(text: str) -> str:
21
25
  """Escape curly braces for Globus Compute's .format() call.
@@ -44,6 +48,10 @@ def template_shell_command(script_path: str, function_name: str, payload: str) -
44
48
  A fully-formed shell command string ready to be executed via Globus
45
49
  Compute or local subprocess
46
50
  """
51
+ logger.debug(
52
+ f"Templating shell command for function '{function_name}' in script '{script_path}'"
53
+ )
54
+
47
55
  with open(script_path, "r") as f_in:
48
56
  user_script = f_in.read()
49
57
 
@@ -64,6 +72,7 @@ def template_shell_command(script_path: str, function_name: str, payload: str) -
64
72
  outfile_path = f"{script_name}.out"
65
73
 
66
74
  version_spec = get_groundhog_version_spec()
75
+ logger.debug(f"Using groundhog version spec: {version_spec}")
67
76
 
68
77
  # Load runner template
69
78
  templates_dir = Path(__file__).parent / "templates"
@@ -81,6 +90,11 @@ def template_shell_command(script_path: str, function_name: str, payload: str) -
81
90
  module_name=path_to_module_name(script_path),
82
91
  )
83
92
 
93
+ # Read local log level (None if not set)
94
+ local_log_level = os.getenv("GROUNDHOG_LOG_LEVEL")
95
+ if local_log_level:
96
+ logger.debug(f"Propagating log level to remote: {local_log_level}")
97
+
84
98
  # Render shell command
85
99
  shell_template = jinja_env.get_template("shell_command.sh.jinja")
86
100
  shell_command_string = shell_template.render(
@@ -91,8 +105,11 @@ def template_shell_command(script_path: str, function_name: str, payload: str) -
91
105
  script_name=script_name,
92
106
  version_spec=version_spec,
93
107
  payload=payload,
108
+ log_level=local_log_level,
94
109
  )
95
110
 
111
+ logger.debug(f"Generated shell command ({len(shell_command_string)} chars)")
112
+
96
113
  return shell_command_string
97
114
 
98
115
 
@@ -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/)
@@ -0,0 +1,34 @@
1
+ groundhog_hpc/__init__.py,sha256=BWOp2bKdV2Oz1Ubd89a-xF6XVEHqB9HZUaSk88T9o_Y,1513
2
+ groundhog_hpc/compute.py,sha256=wQnjfYR4FSo2z2BJLVr99OIXz_AjRbiRy4-6qW0W2uE,4819
3
+ groundhog_hpc/console.py,sha256=lipheM-NECkcqjK-ehzyD_zG-FzyJQrNT__4tGNFMr0,6941
4
+ groundhog_hpc/decorators.py,sha256=XkP3Y2D6tX9KAxxY_P0A8L-zISuwXTupaP7MuSrt19Y,4714
5
+ groundhog_hpc/errors.py,sha256=qDXuKYso6ZPIq64qsuBwL8yY27LGk9fe4rtvu1YlRtc,3933
6
+ groundhog_hpc/function.py,sha256=H0wxI0sdMj6T8jpEW4yb5FUpXAr9Yow7HPCFk_NTaB4,14445
7
+ groundhog_hpc/future.py,sha256=2_zNXAvIIurFwa9kcPW0cDL-W79sHSufIA1qaj1a9gU,7074
8
+ groundhog_hpc/harness.py,sha256=7aCIHzvahwvNOj-vBnzOQ9TV9fpCNnoXiFCa6I3Dkkc,1427
9
+ groundhog_hpc/import_hook.py,sha256=McNT9Lyppp3qriWITrlpCyd0WVJiQsizKM8EGT_VZPg,4241
10
+ groundhog_hpc/logging.py,sha256=_SSI9jw5YYvFXZdIkWqr-P05WvWQ1fm2UPTuwqLh9dQ,1699
11
+ groundhog_hpc/serialization.py,sha256=tbUY0d7mNoLGs_u4-ahyqvfK1kdkjBHli54hwiaLWlE,7642
12
+ groundhog_hpc/templating.py,sha256=HnNQc-b72BcnsK8o7dZ4g5RUOI5XTRbJXeiExXGrgm4,4306
13
+ groundhog_hpc/utils.py,sha256=ig7mEgmhf-JJ_LQq81yUtZzxstk27Ns2I2P-wC3QS5g,7611
14
+ groundhog_hpc/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ groundhog_hpc/app/add.py,sha256=zZW0eWnwRWJwpXL8V_Saaeb6X2D3G_y5ws19mDk0v3g,4534
16
+ groundhog_hpc/app/init.py,sha256=7F6qbWJe6cEoAxLdWumqPDNrHHbqFHzSLpvHOXhQdMQ,5005
17
+ groundhog_hpc/app/main.py,sha256=mZRraUNXHl-60r8UFh65lpJ9_7294QwSYZAXfu2IKYc,1146
18
+ groundhog_hpc/app/remove.py,sha256=0BbUqGTy4Xj5PDegr6oAirdZc01GuYxv4CH4hYMRn-4,3872
19
+ groundhog_hpc/app/run.py,sha256=p1EcUl7Ct6d40HRF814X5JmQU3q3PQ697l_oyO3GL6w,4030
20
+ groundhog_hpc/app/utils.py,sha256=3XfES3Vp4l9m2JEmFeADdFXxTj5g1QMakbvaP3XLQP4,5434
21
+ groundhog_hpc/configuration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ groundhog_hpc/configuration/defaults.py,sha256=b6JMKI-1rt5u9XZU1APND7BZucIrG4AqIh8gat6hARo,218
23
+ groundhog_hpc/configuration/endpoints.py,sha256=U03eYM5Vjw_HHbe8XnAV5smzzBlzvluRG2e8WNi8FG4,7304
24
+ groundhog_hpc/configuration/models.py,sha256=5KL2NU1NdPycO6uLQQ7U1G-ljfD8CyuHesmj-DLSRyA,4791
25
+ groundhog_hpc/configuration/pep723.py,sha256=TNbd2_UxOb_dMwtKcEJAJq62dC7SFdu_lt-D3m6Dx58,14488
26
+ groundhog_hpc/configuration/resolver.py,sha256=VoYZ0S2X7BF6wOXz9NaxS71vqANYRsVi5pYT1B8fRAE,10326
27
+ groundhog_hpc/templates/groundhog_run.py.jinja,sha256=-MwRi_k9LswYhi7pvnPwR-SzwBXvrugLofBc_-Yteuk,633
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,,
@@ -1,33 +0,0 @@
1
- groundhog_hpc/__init__.py,sha256=cWGAeK8kcXuA8Mo_LXTHM7qvoKQ7lCIk3KpkRmr9lVE,1418
2
- groundhog_hpc/compute.py,sha256=bcscRLKlMkC3RDTNMvSUFBR-ly9wRQUeLUqfvcTXmJM,4172
3
- groundhog_hpc/console.py,sha256=jzF7WAZofDzP0JVsF8Q0-SFuNxlIEa1lebhOsfBlpaI,6915
4
- groundhog_hpc/decorators.py,sha256=af9ExJz6YYvxlEM8UKKLcFFtHOSjoPQ64yILmaQ-06Q,4645
5
- groundhog_hpc/errors.py,sha256=qDXuKYso6ZPIq64qsuBwL8yY27LGk9fe4rtvu1YlRtc,3933
6
- groundhog_hpc/function.py,sha256=i_Pg_QmCeKq7QFOM_JfHtZuFIPfqcrDWzDG3-w42H6g,13182
7
- groundhog_hpc/future.py,sha256=NsspZQ0-v1i83Lq9aH3-GsLv-eapOxzgxNW5aBdQ5WM,5713
8
- groundhog_hpc/harness.py,sha256=7aCIHzvahwvNOj-vBnzOQ9TV9fpCNnoXiFCa6I3Dkkc,1427
9
- groundhog_hpc/import_hook.py,sha256=McNT9Lyppp3qriWITrlpCyd0WVJiQsizKM8EGT_VZPg,4241
10
- groundhog_hpc/serialization.py,sha256=pmcubvY5BhfHlXGHKeX8aXpywcJmSjHcIcaMlFuTdFs,6696
11
- groundhog_hpc/templating.py,sha256=gCYWSMmMAz9QUa92-bjNDXMJY4Q_K0qWvIMcy-sqCpw,3741
12
- groundhog_hpc/utils.py,sha256=ig7mEgmhf-JJ_LQq81yUtZzxstk27Ns2I2P-wC3QS5g,7611
13
- groundhog_hpc/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- groundhog_hpc/app/add.py,sha256=Y5FVJopsnQaVUxbt1iLNWXOj2UbiriWKrP7E1urNReQ,2019
15
- groundhog_hpc/app/init.py,sha256=8R0_jDI2YDXija2SEsmg-NXrtDOrXHOJwE6w2rJI-pk,3462
16
- groundhog_hpc/app/main.py,sha256=mZRraUNXHl-60r8UFh65lpJ9_7294QwSYZAXfu2IKYc,1146
17
- groundhog_hpc/app/remove.py,sha256=6m0cmiebOycCrMcvoZiFyOvgRTNaa2mZxcCCrEqUd6I,1043
18
- groundhog_hpc/app/run.py,sha256=0sR9Eh42u-4JNo0sbFKXJXrWGywoXY9i80xoz5DvybY,3528
19
- groundhog_hpc/app/utils.py,sha256=3XfES3Vp4l9m2JEmFeADdFXxTj5g1QMakbvaP3XLQP4,5434
20
- groundhog_hpc/configuration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- groundhog_hpc/configuration/defaults.py,sha256=OadDIrkJxYzns0Nd4x242wsahf2x3OJwuv9vNohcE08,192
22
- groundhog_hpc/configuration/endpoints.py,sha256=HFllc-RDrPjmiqi5AGWUgThUrIRsEpUHMQIUjVD1IRQ,12150
23
- groundhog_hpc/configuration/models.py,sha256=Xf4GvLmYBLwPnC1oO1s7ygCAjZyk_wjZFfen3UdzQNM,3452
24
- groundhog_hpc/configuration/pep723.py,sha256=VQVEMo_Z39y2x3W3cVLjPdcU19qrXbBxNsv3jgSMgVg,4360
25
- groundhog_hpc/configuration/resolver.py,sha256=Fyyk6ZarB_iWo1hHqRBjqWOzE1yrGTkR8OwdLl6ElMw,8660
26
- groundhog_hpc/templates/groundhog_run.py.jinja,sha256=-MwRi_k9LswYhi7pvnPwR-SzwBXvrugLofBc_-Yteuk,633
27
- groundhog_hpc/templates/init_script.py.jinja,sha256=vQtlup9SvOUhsCRHrieCb0GZpDnV0eZ_P-0ixyzv6i8,1177
28
- groundhog_hpc/templates/shell_command.sh.jinja,sha256=RBYj7m45MiYfNTwuzsVBm6PWziL0vtu7EXPXrSzXjVI,1473
29
- groundhog_hpc-0.5.5.dist-info/METADATA,sha256=xcThaPiPz6XoyJxrR1_-KI3qJkSOxCcMWzTx2TpIt9c,2071
30
- groundhog_hpc-0.5.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
31
- groundhog_hpc-0.5.5.dist-info/entry_points.txt,sha256=Dk9LTPJ-3vxm-sGsAaddEawPpIDzhM-ZVuyHKjzUCd0,51
32
- groundhog_hpc-0.5.5.dist-info/licenses/LICENSE,sha256=4WiSzz3h9qYXwc4sFWWFzc45ws_0HLZJmbT6_69IBjs,1078
33
- groundhog_hpc-0.5.5.dist-info/RECORD,,