flux-batch 0.0.1__py3-none-any.whl → 0.0.11__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.
flux_batch/__init__.py CHANGED
@@ -1,7 +1,8 @@
1
1
  from .jobspec import BatchJobspecV1
2
2
  from .models import BatchAttributesV1, BatchJobV1
3
+ from .submit import preview as jobspec
3
4
  from .submit import submit
4
5
 
5
- __all__ = ["BatchJobV1", "BatchAttributesV1", "BatchJobspecV1", "submit"]
6
+ __all__ = ["BatchJobV1", "BatchAttributesV1", "BatchJobspecV1", "submit", "jobspec"]
6
7
 
7
8
  from .version import __version__ # noqa
flux_batch/jobspec.py CHANGED
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import shlex
2
3
  from typing import List
3
4
 
@@ -19,6 +20,7 @@ class BatchJobspecV1:
19
20
  self.prologs: List[str] = []
20
21
  self.epilogs: List[str] = []
21
22
  self.services: List[str] = []
23
+ self.modules: List[str] = []
22
24
 
23
25
  @classmethod
24
26
  def from_command(cls, command: List[str], **kwargs):
@@ -52,6 +54,9 @@ class BatchJobspecV1:
52
54
  def add_epilog(self, cmd: str):
53
55
  self.epilogs.append(cmd)
54
56
 
57
+ def add_module(self, service_name: str):
58
+ self.modules.append(service_name)
59
+
55
60
  def get_cli_flags(self) -> List[str]:
56
61
  """
57
62
  Converts BatchAttributesV1 into a list of strings for subprocess.
@@ -108,8 +113,25 @@ class BatchJobspecV1:
108
113
  for val in getattr(attr, field_name):
109
114
  flags.extend([flag, str(val)])
110
115
 
116
+ # If we have modules, ensure they are added --conf <module>=true
117
+ if self.modules:
118
+ # Tell Flux to look in our user home for rc scripts
119
+ modprobe_path = os.path.expanduser("~/.flux-batch")
120
+ flags.extend(["--env", f"FLUX_MODPROBE_PATH_APPEND={modprobe_path}"])
121
+
122
+ # If modules are used, we need to pass the service names into the Flux config
123
+ # so the @task 'needs_config' filter allows them to run
124
+ for mod in self.modules:
125
+ flags.extend(["--conf", f"{mod}=true"])
126
+
111
127
  return flags
112
128
 
129
+ def render(self) -> str:
130
+ """
131
+ Generate the jobspec.
132
+ """
133
+ return self.generate_wrapper_script()
134
+
113
135
  def generate_wrapper_script(self) -> str:
114
136
  """
115
137
  Generate the wrapper script.
@@ -2,10 +2,60 @@ import os
2
2
  import subprocess
3
3
  import sys
4
4
 
5
- from .scribe import SERVICE_TEMPLATE as scribe_template
5
+ import flux_batch.service.scribe as scribe
6
6
 
7
7
  # Lookup of known services
8
- services = {"flux-scribe": scribe_template}
8
+ services = {"flux-scribe": scribe.SERVICE_TEMPLATE}
9
+ modules = {
10
+ "flux-scribe": {
11
+ "startup": scribe.START_MODULE_TEMPLATE,
12
+ "shutdown": scribe.STOP_MODULE_TEMPLATE,
13
+ "module": scribe.MODULE_NAME,
14
+ }
15
+ }
16
+
17
+
18
+ def write_modprobe_script(rc_path, script, args=None):
19
+ """
20
+ Shared function to write service file.
21
+ """
22
+ args = args or {}
23
+ if not os.path.exists(rc_path):
24
+ with open(rc_path, "w") as f:
25
+ f.write(script.format(**args))
26
+
27
+
28
+ def ensure_modprobe_scripts(service_name: str):
29
+ """
30
+ Ensures rc1.d (start) and rc3.d (stop) scripts exist for the service.
31
+ """
32
+ if service_name not in modules:
33
+ print("Warning: module {service_name} is not known.")
34
+ return
35
+
36
+ # We will add these to FLUX_MODPROBE_PATH_APPEND
37
+ base_dir = os.path.expanduser("~/.flux-batch")
38
+ for subdir in ["rc1.d", "rc3.d"]:
39
+ os.makedirs(os.path.join(base_dir, subdir), exist_ok=True)
40
+
41
+ service_func = service_name.replace("-", "_")
42
+
43
+ # Path for rc1.d (startup)
44
+ args = {
45
+ "service_name": service_name,
46
+ "service_func": service_func,
47
+ "python_bin": sys.executable,
48
+ "module_name": modules[service_name]["module"],
49
+ }
50
+ rc1_path = os.path.join(base_dir, "rc1.d", f"{service_name}.py")
51
+ script = modules[service_name]["startup"]
52
+ write_modprobe_script(rc1_path, script, args=args)
53
+
54
+ # Path for rc3.d (shutdown)
55
+ args = {"service_name": service_name, "service_func": service_func}
56
+ rc3_path = os.path.join(base_dir, "rc3.d", f"{service_name}.py")
57
+ script = modules[service_name]["shutdown"]
58
+ write_modprobe_script(rc3_path, script, args=args)
9
59
 
10
60
 
11
61
  def ensure_user_service(service_name: str):
@@ -1 +1,2 @@
1
- from .template import SERVICE_TEMPLATE
1
+ MODULE_NAME = "flux_batch.service.scribe"
2
+ from .template import SERVICE_TEMPLATE, START_MODULE_TEMPLATE, STOP_MODULE_TEMPLATE
@@ -10,3 +10,44 @@ Restart=on-failure
10
10
  [Install]
11
11
  WantedBy=default.target
12
12
  """
13
+
14
+ START_MODULE_TEMPLATE = """
15
+ from flux.modprobe import task
16
+ import flux.subprocess as subprocess
17
+
18
+ @task(
19
+ "start-{service_name}",
20
+ ranks="0",
21
+ needs_config=["{service_name}"],
22
+ after=["resource", "job-list"],
23
+ )
24
+ def start_{service_func}(context):
25
+ # This triggers the systemd user service provisioned earlier
26
+ # context.bash("systemctl --user start {service_name}")
27
+ subprocess.rexec_bg(
28
+ context.handle,
29
+ ["{python_bin}", "-m", "{module_name}"],
30
+ label="{service_name}",
31
+ nodeid=0
32
+ )
33
+ """
34
+
35
+ STOP_MODULE_TEMPLATE = """
36
+ from flux.modprobe import task
37
+ import flux.subprocess as subprocess
38
+
39
+ @task(
40
+ "stop-{service_name}",
41
+ ranks="0",
42
+ needs_config=["{service_name}"],
43
+ before=["resource", "job-list"],
44
+ )
45
+ def stop_{service_func}(context):
46
+ # context.bash("systemctl --user stop {service_name}")
47
+ subprocess.kill(context.handle, signum=2, label="{service_name}").get()
48
+ try:
49
+ status = subprocess.wait(context.handle, label="{service_name}").get()["status"]
50
+ print(status)
51
+ except:
52
+ pass
53
+ """
flux_batch/submit.py CHANGED
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import os
2
3
  import stat
3
4
  import subprocess
@@ -7,8 +8,56 @@ import flux
7
8
  import flux.job
8
9
 
9
10
  import flux_batch.models as models
11
+ import flux_batch.service as services
10
12
  import flux_batch.utils as utils
11
- from flux_batch.service import ensure_user_service
13
+
14
+
15
+ def setup(spec):
16
+ """
17
+ shared function to generate services / modules from a spec.
18
+ """
19
+ # Provision services (like flux-scribe) if requested
20
+ for service in spec.services:
21
+ services.ensure_user_service(service)
22
+ for module in spec.modules:
23
+ services.ensure_modprobe_scripts(module)
24
+
25
+
26
+ def generate_jobspec(spec, script, wrapper_path):
27
+ """
28
+ Shared function to write a script to a wrapper path and generate
29
+ a jobspec for it via flux batch --dry-run.
30
+ """
31
+ utils.write_file(script, wrapper_path)
32
+
33
+ # Make the script executable so 'flux batch' can analyze it
34
+ os.chmod(wrapper_path, os.stat(wrapper_path).st_mode | stat.S_IEXEC)
35
+
36
+ # Generate the RFC 25 Jobspec JSON via the Flux CLI
37
+ # This handles all resource mapping (-N, -n, etc.)
38
+ cmd = ["flux", "batch"] + spec.get_cli_flags() + ["--dry-run", wrapper_path]
39
+
40
+ try:
41
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
42
+ except subprocess.CalledProcessError as e:
43
+ print(f"Error during flux batch dryrun: {e.stderr}")
44
+ raise
45
+ return result.stdout
46
+
47
+
48
+ def preview(spec: models.BatchJobV1) -> int:
49
+ """
50
+ Preview the jobspec.
51
+ """
52
+ setup(spec)
53
+
54
+ with tempfile.TemporaryDirectory() as tmpdir:
55
+
56
+ # Write the wrapper script (handling prologs, services, and jobs)
57
+ wrapper_path = os.path.join(tmpdir, "wrapper.sh")
58
+ script = spec.generate_wrapper_script()
59
+ jobspec = generate_jobspec(spec, script, wrapper_path)
60
+ return json.loads(jobspec)
12
61
 
13
62
 
14
63
  def submit(handle: flux.Flux, spec: models.BatchJobV1, dry_run=False) -> int:
@@ -19,10 +68,7 @@ def submit(handle: flux.Flux, spec: models.BatchJobV1, dry_run=False) -> int:
19
68
  3. Uses 'flux batch --dryrun' to compile the Jobspec JSON.
20
69
  4. Submits the Jobspec to the Flux instance.
21
70
  """
22
-
23
- # Provision services (like flux-scribe) if requested
24
- for service in spec.services:
25
- ensure_user_service(service)
71
+ setup(spec)
26
72
 
27
73
  with tempfile.TemporaryDirectory() as tmpdir:
28
74
  # Write the wrapper script (handling prologs, services, and jobs)
@@ -33,21 +79,8 @@ def submit(handle: flux.Flux, spec: models.BatchJobV1, dry_run=False) -> int:
33
79
  if dry_run:
34
80
  return script
35
81
 
36
- utils.write_file(script, wrapper_path)
37
-
38
- # Make the script executable so 'flux batch' can analyze it
39
- os.chmod(wrapper_path, os.stat(wrapper_path).st_mode | stat.S_IEXEC)
40
-
41
- # Generate the RFC 25 Jobspec JSON via the Flux CLI
42
- # This handles all resource mapping (-N, -n, etc.)
43
- cmd = ["flux", "batch"] + spec.get_cli_flags() + ["--dry-run", wrapper_path]
44
-
45
- try:
46
- result = subprocess.run(cmd, capture_output=True, text=True, check=True)
47
- except subprocess.CalledProcessError as e:
48
- print(f"Error during flux batch dryrun: {e.stderr}")
49
- raise
82
+ jobspec = generate_jobspec(spec, script, wrapper_path)
50
83
 
51
84
  # Submit the JSON string to the Flux instance
52
85
  # The result.stdout contains the raw JSON Jobspec
53
- return flux.job.submit(handle, result.stdout)
86
+ return flux.job.submit(handle, jobspec)
flux_batch/version.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.0.1"
1
+ __version__ = "0.0.11"
2
2
  AUTHOR = "Vanessa Sochat"
3
3
  AUTHOR_EMAIL = "vsoch@users.noreply.github.com"
4
4
  NAME = "flux-batch"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flux-batch
3
- Version: 0.0.1
3
+ Version: 0.0.11
4
4
  Summary: Python SDK for flux batch jobs and services
5
5
  Home-page: https://github.com/converged-computing/flux-batch
6
6
  Author: Vanessa Sochat
@@ -36,7 +36,7 @@ Requires-Dist: rich ; extra == 'scribe'
36
36
 
37
37
  > Python SDK to generate Flux batch jobs and services
38
38
 
39
- [![PyPI version](https://badge.fury.io/py/flux-batch.svg)](https://badge.fury.io/py/flux-batch)
39
+ ![PyPI - Version](https://img.shields.io/pypi/v/flux-batch)
40
40
 
41
41
  ![https://github.com/converged-computing/flux-batch/raw/main/img/flux-batch-small.png](https://github.com/converged-computing/flux-batch/raw/main/img/flux-batch-small.png)
42
42
 
@@ -70,10 +70,21 @@ pip install -e . --break-system-packages
70
70
 
71
71
  We have a few simple examples:
72
72
 
73
+ #### Saving Logs
74
+
73
75
  ```bash
74
76
  python3 ./examples/save_logs.py
75
77
  ```
76
78
 
79
+ #### Flux Scribe Module
80
+
81
+ ```bash
82
+ export FLUX_SCRIBE_DATABASE=sqlite:///flux-batch-job.db
83
+ python3 ./examples/flux_scribe_module.py
84
+ ```
85
+
86
+ #### General Test
87
+
77
88
  Or run the controlled example to see a batch job with prolog and epilog run and complete:
78
89
 
79
90
  ```bash
@@ -1,28 +1,28 @@
1
- flux_batch/__init__.py,sha256=ZAZF-E0SbVVY2U1_WkRGZFB4rge5BGqQZJ2MdjloZhY,230
2
- flux_batch/jobspec.py,sha256=8eYPPgYmup37AvKQddtQax9RweZnsbPFUBe0eOb7__4,4638
1
+ flux_batch/__init__.py,sha256=35032OVyHkBiQIptg7NBY_6mdhthFu1hpqOCsREm5lQ,280
2
+ flux_batch/jobspec.py,sha256=MUMI4Y_DgjCRssgzUDagTTFw8L3t4L_5az1E3tmT1FI,5464
3
3
  flux_batch/models.py,sha256=JjrFqi4Skrop_cyIWfgAHTdY53kotkiGD0JpTnVlByI,2200
4
- flux_batch/submit.py,sha256=TSDg1Dwa5HKhg7Rj9Due8hTDz5__ihyoUdGGKhtVqWo,1795
5
- flux_batch/version.py,sha256=EjicWR0QF-5SiGDBpWkctAT6keC05IyZrr61fi4OjQ4,642
4
+ flux_batch/submit.py,sha256=tNJMDvnsxbAuXg_5TkB8HurzQ9I-Dot_gXXpIU5LtTA,2654
5
+ flux_batch/version.py,sha256=h-6jDEwrQuFPdUK7HJbFYJISN7mt1MwBKK0UnnS1-cM,643
6
6
  flux_batch/logger/__init__.py,sha256=eDdpw_uppR5mPLHE39qT_haqMxu-2wniLlJZDigRC2k,52
7
7
  flux_batch/logger/generate.py,sha256=L9JyMY2oapp0ss7f7LGuihbLomzVJsMq7sByy9NhbZI,4017
8
8
  flux_batch/logger/logger.py,sha256=HKymVBNcoPdX87QWy69er5wUzHVeriiKp9p0bIYboUo,5927
9
9
  flux_batch/script/__init__.py,sha256=ZR-NCvI42RrAJTKzSmV4AcxlL-aoYh7mlQNPxF8Vzok,400
10
10
  flux_batch/script/save_logs.sh,sha256=HeapqvL0iR8aX7LtbwlcFTy19j5WxkQ-1M2J9epu-EU,515
11
- flux_batch/service/__init__.py,sha256=sFJ3r7XEw4vLcmadTxsxQq715bl_tFREOoUS4erimqQ,1116
11
+ flux_batch/service/__init__.py,sha256=pD7RYpdSLZvYU4qwShYXA6UcaJy191fKtzqJL3uttEc,2724
12
12
  flux_batch/service/scribe.py,sha256=dY6geiLvXYIRcIzuP_naZscKgzX4Y5dPzxoWf9Wywg0,253
13
- flux_batch/service/scribe/__init__.py,sha256=BW4xb-QZvdrtbo73XB26MZozIbvDdnrgLzjB4GRtofU,39
13
+ flux_batch/service/scribe/__init__.py,sha256=773-AzF_WRY6udG5nQSexf7Vga4K1YZLy1sfQAEd1uo,126
14
14
  flux_batch/service/scribe/__main__.py,sha256=3S0dyhkHk-bPT_Z0laNUg-HydrCFql4hPOV2ZNF5rO0,3777
15
15
  flux_batch/service/scribe/database.py,sha256=EB8OEMfNvfCplGaz-ZNMsfIpd305eP-mfCvSd-fg_k4,5626
16
16
  flux_batch/service/scribe/models.py,sha256=7lUrRosnQ2douFL_xD9GMYex4Z4lkN-CcBeWDhzmD8c,2668
17
- flux_batch/service/scribe/template.py,sha256=YwmAX5qkMwaj9FztftdgY3DjBaF9PVd7hDZebiD50fA,256
17
+ flux_batch/service/scribe/template.py,sha256=yDsheid2FXeYr458loxB3HyZKSWpGQpM8iUQfnfOA-s,1336
18
18
  flux_batch/utils/__init__.py,sha256=CqMhw_mBfR0HBcHwv7LtFITq0J7LBV413VQE9xrz8ks,42
19
19
  flux_batch/utils/fileio.py,sha256=Elz8WkNkJ9B6x7WmCwiIBW0GgsRSSFCcbuJh7aqu2z4,4879
20
20
  flux_batch/utils/text.py,sha256=Ci1BqHs2IbOSn2o60zhLkT4kIA7CSNuGj8mdiGaDIGk,606
21
21
  flux_batch/utils/timer.py,sha256=_Weec7Wd5hWQ1r4ZHjownG4YdoIowpVqilXhvYFmIgA,491
22
- flux_batch-0.0.1.dist-info/LICENSE,sha256=AlyLB1m_z0CENCx1ob0PedLTTohtH2VLZhs2kfygrfc,1108
23
- flux_batch-0.0.1.dist-info/METADATA,sha256=G4iEbaFF1cRs12NvjbhWF76uj63MPnG4C6zF2T00e7w,5215
24
- flux_batch-0.0.1.dist-info/NOTICE,sha256=9CR93geVKl_4ZrJORbXN0fzkEM2y4DglWhY1hn9ZwQw,1167
25
- flux_batch-0.0.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
26
- flux_batch-0.0.1.dist-info/entry_points.txt,sha256=ynoKpD82xn2V2sD-aZIQoq7NnfOu9VEKqW55Y1AoPGI,67
27
- flux_batch-0.0.1.dist-info/top_level.txt,sha256=jj8zAsZzMmbjiBISJL7lRtA37MSEAQYfObGLUncn9Lw,11
28
- flux_batch-0.0.1.dist-info/RECORD,,
22
+ flux_batch-0.0.11.dist-info/LICENSE,sha256=AlyLB1m_z0CENCx1ob0PedLTTohtH2VLZhs2kfygrfc,1108
23
+ flux_batch-0.0.11.dist-info/METADATA,sha256=A0LLvVm-RKGxSM26HMfHlOKe5OxF22O7APLNWfvIBj0,5352
24
+ flux_batch-0.0.11.dist-info/NOTICE,sha256=9CR93geVKl_4ZrJORbXN0fzkEM2y4DglWhY1hn9ZwQw,1167
25
+ flux_batch-0.0.11.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
26
+ flux_batch-0.0.11.dist-info/entry_points.txt,sha256=ynoKpD82xn2V2sD-aZIQoq7NnfOu9VEKqW55Y1AoPGI,67
27
+ flux_batch-0.0.11.dist-info/top_level.txt,sha256=jj8zAsZzMmbjiBISJL7lRtA37MSEAQYfObGLUncn9Lw,11
28
+ flux_batch-0.0.11.dist-info/RECORD,,