sfapi-client 0.5.0__tar.gz → 0.6.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.5.0 → sfapi_client-0.6.0}/.github/workflows/mkdocs.yml +1 -1
  2. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/PKG-INFO +1 -1
  3. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_async/compute.py +10 -1
  4. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_sync/compute.py +6 -1
  5. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/conftest.py +25 -0
  6. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_jobs.py +24 -0
  7. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_jobs_async.py +25 -0
  8. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/.github/dependabot.yml +0 -0
  9. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/.github/workflows/black.yml +0 -0
  10. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/.github/workflows/pypi.yml +0 -0
  11. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/.github/workflows/pytest.yml +0 -0
  12. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/.github/workflows/ruff.yml +0 -0
  13. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/.gitignore +0 -0
  14. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/.pre-commit-config.yaml +0 -0
  15. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/LEGAL +0 -0
  16. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/LICENSE +0 -0
  17. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/README.md +0 -0
  18. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/community/contributing.md +0 -0
  19. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/examples/check_current_and_past_jobs.ipynb +0 -0
  20. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/examples/getting_bearer_token.ipynb +0 -0
  21. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/examples/run_job_and_check_status.ipynb +0 -0
  22. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/gen_examples.py +0 -0
  23. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/gen_sync.py +0 -0
  24. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/index.md +0 -0
  25. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/javascript/apiselector.js +0 -0
  26. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/quickstart/index.md +0 -0
  27. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/reference/SUMMARY.txt +0 -0
  28. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/reference/async/client/index.md +0 -0
  29. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/reference/async/compute/index.md +0 -0
  30. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/reference/async/exceptions/index.md +0 -0
  31. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/reference/async/groups/index.md +0 -0
  32. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/reference/async/jobs/index.md +0 -0
  33. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/reference/async/paths/index.md +0 -0
  34. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/reference/async/projects/index.md +0 -0
  35. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/reference/async/resources/index.md +0 -0
  36. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/docs/reference/async/users/index.md +0 -0
  37. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/mkdocs.yml +0 -0
  38. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/pyproject.toml +0 -0
  39. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/pytest.ini +0 -0
  40. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/requirements-dev.txt +0 -0
  41. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/scripts/run.py +0 -0
  42. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/__init__.py +0 -0
  43. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_async/__init__.py +0 -0
  44. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_async/client.py +0 -0
  45. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_async/groups.py +0 -0
  46. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_async/jobs.py +0 -0
  47. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_async/paths.py +0 -0
  48. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_async/projects.py +0 -0
  49. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_async/storage.py +0 -0
  50. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_async/users.py +0 -0
  51. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_compute.py +0 -0
  52. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_jobs.py +0 -0
  53. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_models/__init__.py +0 -0
  54. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_models/job_status_response_sacct.py +0 -0
  55. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_models/job_status_response_squeue.py +0 -0
  56. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_models/resources.py +0 -0
  57. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_monitor.py +0 -0
  58. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_sync/__init__.py +0 -0
  59. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_sync/client.py +0 -0
  60. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_sync/groups.py +0 -0
  61. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_sync/jobs.py +0 -0
  62. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_sync/paths.py +0 -0
  63. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_sync/projects.py +0 -0
  64. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_sync/storage.py +0 -0
  65. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_sync/users.py +0 -0
  66. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/_utils.py +0 -0
  67. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/client.py +0 -0
  68. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/compute.py +0 -0
  69. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/exceptions.py +0 -0
  70. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/groups.py +0 -0
  71. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/jobs.py +0 -0
  72. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/paths.py +0 -0
  73. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/projects.py +0 -0
  74. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/resources.py +0 -0
  75. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/storage.py +0 -0
  76. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/src/sfapi_client/users.py +0 -0
  77. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_api.py +0 -0
  78. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_api_async.py +0 -0
  79. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_client.py +0 -0
  80. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_client_async.py +0 -0
  81. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_compute.py +0 -0
  82. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_compute_async.py +0 -0
  83. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_groups.py +0 -0
  84. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_groups_async.py +0 -0
  85. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_key.py +0 -0
  86. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_paths.py +0 -0
  87. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_paths_async.py +0 -0
  88. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_projects.py +0 -0
  89. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_projects_async.py +0 -0
  90. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_resources.py +0 -0
  91. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_resources_async.py +0 -0
  92. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_storage.py +0 -0
  93. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_storage_async.py +0 -0
  94. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_users.py +0 -0
  95. {sfapi_client-0.5.0 → sfapi_client-0.6.0}/tests/test_users_async.py +0 -0
@@ -15,7 +15,7 @@ jobs:
15
15
  - uses: actions/checkout@v4
16
16
  - uses: actions/setup-python@v5
17
17
  with:
18
- python-version: 3.x
18
+ python-version: "3.13"
19
19
  - uses: actions/cache@v4
20
20
  with:
21
21
  key: mkdocs-${{ github.ref }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sfapi_client
3
- Version: 0.5.0
3
+ Version: 0.6.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
@@ -126,10 +126,13 @@ class AsyncCompute(ComputeBase):
126
126
  return task.result
127
127
 
128
128
  @check_auth
129
- async def submit_job(self, script: Union[str, AsyncRemotePath]) -> AsyncJobSqueue:
129
+ async def submit_job(
130
+ self, script: Union[str, AsyncRemotePath], args: Optional[List[str]] = None
131
+ ) -> AsyncJobSqueue:
130
132
  """Submit a job to the compute resource
131
133
 
132
134
  :param script: Path to file on the compute system, or script to run beginning with `#!`.
135
+ :param args: An optional list of command line arguments to pass to the script.
133
136
  :return: Object containing information about the job, its job id, and status on the system.
134
137
  """
135
138
 
@@ -150,6 +153,12 @@ class AsyncCompute(ComputeBase):
150
153
  raise SfApiError(f"Script path not present or is not a file, {script}")
151
154
 
152
155
  data = {"job": script, "isPath": is_path}
156
+ if args is not None:
157
+ if not is_path:
158
+ raise ValueError(
159
+ "Command line arguments cannot be passed when the script is not a file."
160
+ )
161
+ data["args"] = args
153
162
 
154
163
  r = await self.client.post(f"compute/jobs/{self.name}", data)
155
164
  r.raise_for_status()
@@ -127,10 +127,11 @@ class Compute(ComputeBase):
127
127
  return task.result
128
128
 
129
129
  @check_auth
130
- def submit_job(self, script: Union[str, RemotePath]) -> JobSqueue:
130
+ def submit_job(self, script: Union[str, RemotePath], args: Optional[List[str]] = None) -> JobSqueue:
131
131
  """Submit a job to the compute resource
132
132
 
133
133
  :param script: Path to file on the compute system, or script to run beginning with `#!`.
134
+ :param args: An optional list of command line arguments to pass to the script.
134
135
  :return: Object containing information about the job, its job id, and status on the system.
135
136
  """
136
137
 
@@ -151,6 +152,10 @@ class Compute(ComputeBase):
151
152
  raise SfApiError(f"Script path not present or is not a file, {script}")
152
153
 
153
154
  data = {"job": script, "isPath": is_path}
155
+ if args is not None:
156
+ if not is_path:
157
+ raise ValueError("Command line arguments cannot be passed when the script is not a file.")
158
+ data["args"] = args
154
159
 
155
160
  r = self.client.post(f"compute/jobs/{self.name}", data)
156
161
  r.raise_for_status()
@@ -1,6 +1,7 @@
1
1
  import json
2
2
  import random
3
3
  import string
4
+ from io import BytesIO
4
5
  from typing import Optional, Union, Dict
5
6
  from pathlib import Path
6
7
  from cryptography.hazmat.primitives.asymmetric import rsa
@@ -23,6 +24,7 @@ class Settings(BaseSettings):
23
24
  SFAPI_DEV_CLIENT_ID: Optional[str] = None
24
25
  SFAPI_DEV_CLIENT_SECRET: Optional[Union[str, Dict]] = None
25
26
  TEST_JOB_PATH: Optional[str] = None
27
+ TEST_JOB_ACCOUNT: Optional[str] = None
26
28
  TEST_MACHINE: Machine = Machine.perlmutter
27
29
  TEST_RESOURCE: Resource = Resource.spin
28
30
  TEST_USERNAME: Optional[str] = None
@@ -75,6 +77,11 @@ def test_job_path():
75
77
  return settings.TEST_JOB_PATH
76
78
 
77
79
 
80
+ @pytest.fixture
81
+ def test_job_account():
82
+ return settings.TEST_JOB_ACCOUNT
83
+
84
+
78
85
  @pytest.fixture
79
86
  def test_machine():
80
87
  return settings.TEST_MACHINE
@@ -214,3 +221,21 @@ def empty_key_file(tmp_path_factory):
214
221
  if temp_path.exists():
215
222
  (temp_path / "nokey.pem").unlink(missing_ok=True)
216
223
  temp_path.rmdir()
224
+
225
+
226
+ @pytest.fixture
227
+ def test_arg_job_script(test_job_account, test_tmp_dir):
228
+ script = BytesIO(f"""#!/usr/bin/env bash
229
+ #SBATCH --nodes=1
230
+ #SBATCH --constraint=cpu
231
+ #SBATCH --account={test_job_account}
232
+ #SBATCH --qos=debug
233
+ #SBATCH --time=00:01:00
234
+ #SBATCH --output={test_tmp_dir}/slurm-%j.out
235
+
236
+ set -euo pipefail
237
+
238
+ echo "$@"
239
+ """.encode())
240
+ script.filename = "echo-args.sh"
241
+ return script
@@ -17,6 +17,30 @@ def test_submit(authenticated_client, test_job_path, test_machine):
17
17
  assert state == JobState.COMPLETED
18
18
 
19
19
 
20
+ def test_submit_with_args(
21
+ authenticated_client, test_machine, test_tmp_dir, test_arg_job_script
22
+ ):
23
+ with authenticated_client as client:
24
+ machine = client.compute(test_machine)
25
+
26
+ [remote_dir] = machine.ls(test_tmp_dir, directory=True)
27
+ remote_script = remote_dir.upload(test_arg_job_script)
28
+ try:
29
+ args = ["1", "2", "3"]
30
+
31
+ job = machine.submit_job(remote_script, args)
32
+
33
+ state = job.complete()
34
+
35
+ assert state == JobState.COMPLETED
36
+
37
+ [output_path] = machine.ls(f"{test_tmp_dir}/slurm-{job.jobid}.out")
38
+ output = output_path.download().read()
39
+ assert "1 2 3" in output
40
+ finally:
41
+ machine.run(["rm", "-f", str(remote_script)])
42
+
43
+
20
44
  def test_cancel(authenticated_client, test_job_path, test_machine):
21
45
  with authenticated_client as client:
22
46
  machine = client.compute(test_machine)
@@ -16,6 +16,31 @@ async def test_submit(async_authenticated_client, test_job_path, test_machine):
16
16
  assert state == JobState.COMPLETED
17
17
 
18
18
 
19
+ @pytest.mark.asyncio
20
+ async def test_submit_with_args(
21
+ async_authenticated_client, test_machine, test_tmp_dir, test_arg_job_script
22
+ ):
23
+ async with async_authenticated_client as client:
24
+ machine = await client.compute(test_machine)
25
+
26
+ [remote_dir] = await machine.ls(test_tmp_dir, directory=True)
27
+ remote_script = await remote_dir.upload(test_arg_job_script)
28
+ try:
29
+ args = ["1", "2", "3"]
30
+
31
+ job = await machine.submit_job(remote_script, args)
32
+
33
+ state = await job.complete()
34
+
35
+ assert state == JobState.COMPLETED
36
+
37
+ [output_path] = await machine.ls(f"{test_tmp_dir}/slurm-{job.jobid}.out")
38
+ output = (await output_path.download()).read()
39
+ assert "1 2 3" in output
40
+ finally:
41
+ await machine.run(["rm", "-f", str(remote_script)])
42
+
43
+
19
44
  @pytest.mark.asyncio
20
45
  async def test_cancel(async_authenticated_client, test_job_path, test_machine):
21
46
  async with async_authenticated_client as client:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes