sfapi-client 0.6.0__tar.gz → 0.7.0__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 (95) hide show
  1. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/.github/workflows/pytest.yml +1 -1
  2. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/PKG-INFO +5 -3
  3. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/README.md +2 -0
  4. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/mkdocs.yml +0 -2
  5. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/pyproject.toml +4 -1
  6. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_async/compute.py +11 -1
  7. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_sync/compute.py +17 -4
  8. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_sync/paths.py +1 -1
  9. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_compute.py +27 -0
  10. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_compute_async.py +28 -0
  11. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/.github/dependabot.yml +0 -0
  12. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/.github/workflows/black.yml +0 -0
  13. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/.github/workflows/mkdocs.yml +0 -0
  14. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/.github/workflows/pypi.yml +0 -0
  15. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/.github/workflows/ruff.yml +0 -0
  16. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/.gitignore +0 -0
  17. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/.pre-commit-config.yaml +0 -0
  18. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/LEGAL +0 -0
  19. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/LICENSE +0 -0
  20. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/community/contributing.md +0 -0
  21. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/examples/check_current_and_past_jobs.ipynb +0 -0
  22. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/examples/getting_bearer_token.ipynb +0 -0
  23. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/examples/run_job_and_check_status.ipynb +0 -0
  24. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/gen_examples.py +0 -0
  25. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/gen_sync.py +0 -0
  26. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/index.md +0 -0
  27. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/javascript/apiselector.js +0 -0
  28. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/quickstart/index.md +0 -0
  29. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/reference/SUMMARY.txt +0 -0
  30. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/reference/async/client/index.md +0 -0
  31. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/reference/async/compute/index.md +0 -0
  32. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/reference/async/exceptions/index.md +0 -0
  33. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/reference/async/groups/index.md +0 -0
  34. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/reference/async/jobs/index.md +0 -0
  35. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/reference/async/paths/index.md +0 -0
  36. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/reference/async/projects/index.md +0 -0
  37. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/reference/async/resources/index.md +0 -0
  38. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/docs/reference/async/users/index.md +0 -0
  39. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/pytest.ini +0 -0
  40. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/requirements-dev.txt +0 -0
  41. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/scripts/run.py +0 -0
  42. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/__init__.py +0 -0
  43. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_async/__init__.py +0 -0
  44. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_async/client.py +0 -0
  45. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_async/groups.py +0 -0
  46. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_async/jobs.py +0 -0
  47. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_async/paths.py +0 -0
  48. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_async/projects.py +0 -0
  49. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_async/storage.py +0 -0
  50. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_async/users.py +0 -0
  51. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_compute.py +0 -0
  52. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_jobs.py +0 -0
  53. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_models/__init__.py +0 -0
  54. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_models/job_status_response_sacct.py +0 -0
  55. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_models/job_status_response_squeue.py +0 -0
  56. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_models/resources.py +0 -0
  57. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_monitor.py +0 -0
  58. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_sync/__init__.py +0 -0
  59. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_sync/client.py +0 -0
  60. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_sync/groups.py +0 -0
  61. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_sync/jobs.py +0 -0
  62. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_sync/projects.py +0 -0
  63. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_sync/storage.py +0 -0
  64. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_sync/users.py +0 -0
  65. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/_utils.py +0 -0
  66. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/client.py +0 -0
  67. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/compute.py +0 -0
  68. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/exceptions.py +0 -0
  69. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/groups.py +0 -0
  70. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/jobs.py +0 -0
  71. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/paths.py +0 -0
  72. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/projects.py +0 -0
  73. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/resources.py +0 -0
  74. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/storage.py +0 -0
  75. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/src/sfapi_client/users.py +0 -0
  76. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/conftest.py +0 -0
  77. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_api.py +0 -0
  78. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_api_async.py +0 -0
  79. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_client.py +0 -0
  80. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_client_async.py +0 -0
  81. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_groups.py +0 -0
  82. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_groups_async.py +0 -0
  83. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_jobs.py +0 -0
  84. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_jobs_async.py +0 -0
  85. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_key.py +0 -0
  86. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_paths.py +0 -0
  87. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_paths_async.py +0 -0
  88. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_projects.py +0 -0
  89. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_projects_async.py +0 -0
  90. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_resources.py +0 -0
  91. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_resources_async.py +0 -0
  92. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_storage.py +0 -0
  93. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_storage_async.py +0 -0
  94. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_users.py +0 -0
  95. {sfapi_client-0.6.0 → sfapi_client-0.7.0}/tests/test_users_async.py +0 -0
@@ -13,7 +13,7 @@ jobs:
13
13
  strategy:
14
14
  fail-fast: false
15
15
  matrix:
16
- python-version: ["3.8", "3.9", "3.10", "3.11"]
16
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
17
17
 
18
18
  steps:
19
19
  - uses: actions/checkout@v4
@@ -1,13 +1,13 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: sfapi_client
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Python client for NERSC SF API
5
5
  Author-email: Chris Harris <cjh@lbl.gov>, Nicholas Tyler <tylern@lbl.gov>
6
6
  License-File: LICENSE
7
7
  Classifier: License :: OSI Approved :: BSD License
8
8
  Classifier: Operating System :: OS Independent
9
9
  Classifier: Programming Language :: Python :: 3
10
- Requires-Python: >=3.8
10
+ Requires-Python: >=3.10
11
11
  Requires-Dist: authlib
12
12
  Requires-Dist: httpx
13
13
  Requires-Dist: pydantic-settings
@@ -76,6 +76,8 @@ The sfapi_client project relies on these libraries:
76
76
 
77
77
  ## Installation
78
78
 
79
+ Requires Python 3.10 or newer.
80
+
79
81
  Install with pip:
80
82
 
81
83
  ```shell
@@ -47,6 +47,8 @@ The sfapi_client project relies on these libraries:
47
47
 
48
48
  ## Installation
49
49
 
50
+ Requires Python 3.10 or newer.
51
+
50
52
  Install with pip:
51
53
 
52
54
  ```shell
@@ -37,8 +37,6 @@ plugins:
37
37
  - https://docs.python.org/3/objects.inv
38
38
  options:
39
39
  docstring_style: sphinx
40
- docstring_options:
41
- ignore_init_summary: yes
42
40
  merge_init_into_class: yes
43
41
  show_submodules: no
44
42
 
@@ -14,7 +14,7 @@ authors = [
14
14
  ]
15
15
  description = "Python client for NERSC SF API"
16
16
  readme = "README.md"
17
- requires-python = ">=3.8"
17
+ requires-python = ">=3.10"
18
18
  dependencies = [
19
19
  "authlib",
20
20
  "httpx",
@@ -43,6 +43,9 @@ exclude = [
43
43
  "src/sfapi_client/_async/_models.py"
44
44
  ]
45
45
 
46
+ [tool.ruff.lint]
47
+ select = ["E4", "E7", "E9", "F"]
48
+
46
49
  [project.optional-dependencies]
47
50
  docs = [
48
51
  "mkdocs-material",
@@ -224,7 +224,17 @@ class AsyncCompute(ComputeBase):
224
224
  task = await self._run_task(args)
225
225
  await task._wait()
226
226
 
227
- if task.result.error:
227
+ # A non-empty `error` (stderr) alone doesn't mean the command failed - many commands
228
+ # write non-fatal warnings to stderr while still exiting zero. `exit_code` is the
229
+ # authoritative success/failure signal when the API provides it; only fall back to
230
+ # treating a non-empty `error` as fatal if `exit_code` is unavailable.
231
+ if task.result.exit_code is not None:
232
+ if task.result.exit_code != 0:
233
+ raise SfApiError(
234
+ task.result.error
235
+ or f"Command exited with code {task.result.exit_code}"
236
+ )
237
+ elif task.result.error:
228
238
  raise SfApiError(task.result.error)
229
239
 
230
240
  return task.result.output
@@ -1,5 +1,4 @@
1
1
  from typing import Dict, List, Optional, Union
2
- import json
3
2
  from pydantic import PrivateAttr, ConfigDict, BaseModel
4
3
  from ..exceptions import SfApiError
5
4
  from .._utils import _SLEEP
@@ -127,7 +126,9 @@ class Compute(ComputeBase):
127
126
  return task.result
128
127
 
129
128
  @check_auth
130
- def submit_job(self, script: Union[str, RemotePath], args: Optional[List[str]] = None) -> JobSqueue:
129
+ def submit_job(
130
+ self, script: Union[str, RemotePath], args: Optional[List[str]] = None
131
+ ) -> JobSqueue:
131
132
  """Submit a job to the compute resource
132
133
 
133
134
  :param script: Path to file on the compute system, or script to run beginning with `#!`.
@@ -154,7 +155,9 @@ class Compute(ComputeBase):
154
155
  data = {"job": script, "isPath": is_path}
155
156
  if args is not None:
156
157
  if not is_path:
157
- raise ValueError("Command line arguments cannot be passed when the script is not a file.")
158
+ raise ValueError(
159
+ "Command line arguments cannot be passed when the script is not a file."
160
+ )
158
161
  data["args"] = args
159
162
 
160
163
  r = self.client.post(f"compute/jobs/{self.name}", data)
@@ -221,7 +224,17 @@ class Compute(ComputeBase):
221
224
  task = self._run_task(args)
222
225
  task._wait()
223
226
 
224
- if task.result.error:
227
+ # A non-empty `error` (stderr) alone doesn't mean the command failed - many commands
228
+ # write non-fatal warnings to stderr while still exiting zero. `exit_code` is the
229
+ # authoritative success/failure signal when the API provides it; only fall back to
230
+ # treating a non-empty `error` as fatal if `exit_code` is unavailable.
231
+ if task.result.exit_code is not None:
232
+ if task.result.exit_code != 0:
233
+ raise SfApiError(
234
+ task.result.error
235
+ or f"Command exited with code {task.result.exit_code}"
236
+ )
237
+ elif task.result.error:
225
238
  raise SfApiError(task.result.error)
226
239
 
227
240
  return task.result.output
@@ -162,7 +162,7 @@ class RemotePath(PathBase):
162
162
 
163
163
  r = self.compute.client.get(
164
164
  f"utilities/download/{self.compute.name}/{self._path}",
165
- params={"binary": binary}
165
+ params={"binary": binary},
166
166
  )
167
167
  json_response = r.json()
168
168
  download_response = FileDownloadResponse.model_validate(json_response)
@@ -1,6 +1,9 @@
1
1
  import time
2
2
  from pathlib import Path
3
3
 
4
+ import pytest
5
+
6
+ from sfapi_client import SfApiError
4
7
  from sfapi_client.jobs import JobState
5
8
  from sfapi_client.compute import CommandTask, TaskStatus
6
9
 
@@ -125,6 +128,30 @@ def test_run_arg_path(authenticated_client, test_machine):
125
128
  assert output
126
129
 
127
130
 
131
+ def test_run_success_with_stderr(authenticated_client, test_machine):
132
+ with authenticated_client as client:
133
+ machine = client.compute(test_machine)
134
+ output = machine.run("bash -lc 'echo hello; echo warning >&2; exit 0'")
135
+
136
+ assert output == "hello\n"
137
+
138
+
139
+ def test_run_failure_with_stderr(authenticated_client, test_machine):
140
+ with authenticated_client as client:
141
+ machine = client.compute(test_machine)
142
+
143
+ with pytest.raises(SfApiError, match="failing"):
144
+ machine.run(_failing_command())
145
+
146
+
147
+ def test_run_failure_without_stderr(authenticated_client, test_machine):
148
+ with authenticated_client as client:
149
+ machine = client.compute(test_machine)
150
+
151
+ with pytest.raises(SfApiError, match="Command exited with code 7"):
152
+ machine.run("bash -lc 'exit 7'")
153
+
154
+
128
155
  def test_run_task(authenticated_client, test_machine):
129
156
  with authenticated_client as client:
130
157
  machine = client.compute(test_machine)
@@ -3,6 +3,7 @@ import asyncio
3
3
  import pytest
4
4
  from pathlib import Path
5
5
 
6
+ from sfapi_client import SfApiError
6
7
  from sfapi_client.compute import AsyncCommandTask, TaskStatus
7
8
  from sfapi_client.jobs import JobState
8
9
 
@@ -140,6 +141,33 @@ async def test_run_arg_path(async_authenticated_client, test_machine):
140
141
  assert output
141
142
 
142
143
 
144
+ @pytest.mark.asyncio
145
+ async def test_run_success_with_stderr(async_authenticated_client, test_machine):
146
+ async with async_authenticated_client as client:
147
+ machine = await client.compute(test_machine)
148
+ output = await machine.run("bash -lc 'echo hello; echo warning >&2; exit 0'")
149
+
150
+ assert output == "hello\n"
151
+
152
+
153
+ @pytest.mark.asyncio
154
+ async def test_run_failure_with_stderr(async_authenticated_client, test_machine):
155
+ async with async_authenticated_client as client:
156
+ machine = await client.compute(test_machine)
157
+
158
+ with pytest.raises(SfApiError, match="failing"):
159
+ await machine.run(_failing_command())
160
+
161
+
162
+ @pytest.mark.asyncio
163
+ async def test_run_failure_without_stderr(async_authenticated_client, test_machine):
164
+ async with async_authenticated_client as client:
165
+ machine = await client.compute(test_machine)
166
+
167
+ with pytest.raises(SfApiError, match="Command exited with code 7"):
168
+ await machine.run("bash -lc 'exit 7'")
169
+
170
+
143
171
  @pytest.mark.asyncio
144
172
  async def test_run_task(async_authenticated_client, test_machine):
145
173
  async with async_authenticated_client as client:
File without changes
File without changes
File without changes
File without changes
File without changes