flux-batch 0.0.1__tar.gz → 0.0.11__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 (40) hide show
  1. {flux_batch-0.0.1/flux_batch.egg-info → flux_batch-0.0.11}/PKG-INFO +13 -2
  2. {flux_batch-0.0.1 → flux_batch-0.0.11}/README.md +12 -1
  3. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch/__init__.py +2 -1
  4. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch/jobspec.py +22 -0
  5. flux_batch-0.0.11/flux_batch/service/__init__.py +81 -0
  6. flux_batch-0.0.11/flux_batch/service/scribe/__init__.py +2 -0
  7. flux_batch-0.0.11/flux_batch/service/scribe/template.py +53 -0
  8. flux_batch-0.0.11/flux_batch/submit.py +86 -0
  9. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch/version.py +1 -1
  10. {flux_batch-0.0.1 → flux_batch-0.0.11/flux_batch.egg-info}/PKG-INFO +13 -2
  11. flux_batch-0.0.1/flux_batch/service/__init__.py +0 -31
  12. flux_batch-0.0.1/flux_batch/service/scribe/__init__.py +0 -1
  13. flux_batch-0.0.1/flux_batch/service/scribe/template.py +0 -12
  14. flux_batch-0.0.1/flux_batch/submit.py +0 -53
  15. {flux_batch-0.0.1 → flux_batch-0.0.11}/LICENSE +0 -0
  16. {flux_batch-0.0.1 → flux_batch-0.0.11}/MANIFEST.in +0 -0
  17. {flux_batch-0.0.1 → flux_batch-0.0.11}/NOTICE +0 -0
  18. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch/logger/__init__.py +0 -0
  19. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch/logger/generate.py +0 -0
  20. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch/logger/logger.py +0 -0
  21. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch/models.py +0 -0
  22. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch/script/__init__.py +0 -0
  23. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch/script/save_logs.sh +0 -0
  24. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch/service/scribe/__main__.py +0 -0
  25. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch/service/scribe/database.py +0 -0
  26. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch/service/scribe/models.py +0 -0
  27. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch/utils/__init__.py +0 -0
  28. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch/utils/fileio.py +0 -0
  29. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch/utils/text.py +0 -0
  30. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch/utils/timer.py +0 -0
  31. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch.egg-info/SOURCES.txt +0 -0
  32. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch.egg-info/dependency_links.txt +0 -0
  33. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch.egg-info/entry_points.txt +0 -0
  34. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch.egg-info/not-zip-safe +0 -0
  35. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch.egg-info/requires.txt +0 -0
  36. {flux_batch-0.0.1 → flux_batch-0.0.11}/flux_batch.egg-info/top_level.txt +0 -0
  37. {flux_batch-0.0.1 → flux_batch-0.0.11}/pyproject.toml +0 -0
  38. {flux_batch-0.0.1 → flux_batch-0.0.11}/setup.cfg +0 -0
  39. {flux_batch-0.0.1 → flux_batch-0.0.11}/setup.py +0 -0
  40. {flux_batch-0.0.1 → flux_batch-0.0.11}/tests/test_flux_batch.py +0 -0
@@ -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
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Python SDK to generate Flux batch jobs and services
4
4
 
5
- [![PyPI version](https://badge.fury.io/py/flux-batch.svg)](https://badge.fury.io/py/flux-batch)
5
+ ![PyPI - Version](https://img.shields.io/pypi/v/flux-batch)
6
6
 
7
7
  ![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)
8
8
 
@@ -36,10 +36,21 @@ pip install -e . --break-system-packages
36
36
 
37
37
  We have a few simple examples:
38
38
 
39
+ #### Saving Logs
40
+
39
41
  ```bash
40
42
  python3 ./examples/save_logs.py
41
43
  ```
42
44
 
45
+ #### Flux Scribe Module
46
+
47
+ ```bash
48
+ export FLUX_SCRIBE_DATABASE=sqlite:///flux-batch-job.db
49
+ python3 ./examples/flux_scribe_module.py
50
+ ```
51
+
52
+ #### General Test
53
+
43
54
  Or run the controlled example to see a batch job with prolog and epilog run and complete:
44
55
 
45
56
  ```bash
@@ -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
@@ -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.
@@ -0,0 +1,81 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+
5
+ import flux_batch.service.scribe as scribe
6
+
7
+ # Lookup of known services
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)
59
+
60
+
61
+ def ensure_user_service(service_name: str):
62
+ """
63
+ Checks for the existence of a systemd service file in the user's home.
64
+ If it doesn't exist, it creates it and reloads the daemon.
65
+ """
66
+ user_systemd_dir = os.path.expanduser("~/.config/systemd/user")
67
+ os.makedirs(user_systemd_dir, exist_ok=True)
68
+ service_path = os.path.join(user_systemd_dir, f"{service_name}.service")
69
+
70
+ if not os.path.exists(service_path):
71
+ if service_name in services:
72
+ template = services[service_name]
73
+ print(f"[*] Provisioning {service_name} at {service_path}")
74
+ with open(service_path, "w") as f:
75
+ f.write(template.format(python_path=sys.executable))
76
+
77
+ else:
78
+ print(f"[*] Service {service_name} is not known, assuming exists.")
79
+
80
+ # Reload the user-session manager to recognize the new unit
81
+ subprocess.run(["systemctl", "--user", "daemon-reload"], check=True)
@@ -0,0 +1,2 @@
1
+ MODULE_NAME = "flux_batch.service.scribe"
2
+ from .template import SERVICE_TEMPLATE, START_MODULE_TEMPLATE, STOP_MODULE_TEMPLATE
@@ -0,0 +1,53 @@
1
+ # Template for the Scribe Journal Consumer
2
+ SERVICE_TEMPLATE = """[Unit]
3
+ Description=Flux Scribe Journal Consumer
4
+ After=network.target
5
+
6
+ [Service]
7
+ ExecStart={python_path} -m flux_batch.service.scribe
8
+ Restart=on-failure
9
+
10
+ [Install]
11
+ WantedBy=default.target
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
+ """
@@ -0,0 +1,86 @@
1
+ import json
2
+ import os
3
+ import stat
4
+ import subprocess
5
+ import tempfile
6
+
7
+ import flux
8
+ import flux.job
9
+
10
+ import flux_batch.models as models
11
+ import flux_batch.service as services
12
+ import flux_batch.utils as utils
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)
61
+
62
+
63
+ def submit(handle: flux.Flux, spec: models.BatchJobV1, dry_run=False) -> int:
64
+ """
65
+ Orchestrates the submission process:
66
+ 1. Provisions any required user-space services.
67
+ 2. Generates the wrapper shell script.
68
+ 3. Uses 'flux batch --dryrun' to compile the Jobspec JSON.
69
+ 4. Submits the Jobspec to the Flux instance.
70
+ """
71
+ setup(spec)
72
+
73
+ with tempfile.TemporaryDirectory() as tmpdir:
74
+ # Write the wrapper script (handling prologs, services, and jobs)
75
+ wrapper_path = os.path.join(tmpdir, "wrapper.sh")
76
+
77
+ # dry run here just displays it
78
+ script = spec.generate_wrapper_script()
79
+ if dry_run:
80
+ return script
81
+
82
+ jobspec = generate_jobspec(spec, script, wrapper_path)
83
+
84
+ # Submit the JSON string to the Flux instance
85
+ # The result.stdout contains the raw JSON Jobspec
86
+ return flux.job.submit(handle, jobspec)
@@ -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,31 +0,0 @@
1
- import os
2
- import subprocess
3
- import sys
4
-
5
- from .scribe import SERVICE_TEMPLATE as scribe_template
6
-
7
- # Lookup of known services
8
- services = {"flux-scribe": scribe_template}
9
-
10
-
11
- def ensure_user_service(service_name: str):
12
- """
13
- Checks for the existence of a systemd service file in the user's home.
14
- If it doesn't exist, it creates it and reloads the daemon.
15
- """
16
- user_systemd_dir = os.path.expanduser("~/.config/systemd/user")
17
- os.makedirs(user_systemd_dir, exist_ok=True)
18
- service_path = os.path.join(user_systemd_dir, f"{service_name}.service")
19
-
20
- if not os.path.exists(service_path):
21
- if service_name in services:
22
- template = services[service_name]
23
- print(f"[*] Provisioning {service_name} at {service_path}")
24
- with open(service_path, "w") as f:
25
- f.write(template.format(python_path=sys.executable))
26
-
27
- else:
28
- print(f"[*] Service {service_name} is not known, assuming exists.")
29
-
30
- # Reload the user-session manager to recognize the new unit
31
- subprocess.run(["systemctl", "--user", "daemon-reload"], check=True)
@@ -1 +0,0 @@
1
- from .template import SERVICE_TEMPLATE
@@ -1,12 +0,0 @@
1
- # Template for the Scribe Journal Consumer
2
- SERVICE_TEMPLATE = """[Unit]
3
- Description=Flux Scribe Journal Consumer
4
- After=network.target
5
-
6
- [Service]
7
- ExecStart={python_path} -m flux_batch.service.scribe
8
- Restart=on-failure
9
-
10
- [Install]
11
- WantedBy=default.target
12
- """
@@ -1,53 +0,0 @@
1
- import os
2
- import stat
3
- import subprocess
4
- import tempfile
5
-
6
- import flux
7
- import flux.job
8
-
9
- import flux_batch.models as models
10
- import flux_batch.utils as utils
11
- from flux_batch.service import ensure_user_service
12
-
13
-
14
- def submit(handle: flux.Flux, spec: models.BatchJobV1, dry_run=False) -> int:
15
- """
16
- Orchestrates the submission process:
17
- 1. Provisions any required user-space services.
18
- 2. Generates the wrapper shell script.
19
- 3. Uses 'flux batch --dryrun' to compile the Jobspec JSON.
20
- 4. Submits the Jobspec to the Flux instance.
21
- """
22
-
23
- # Provision services (like flux-scribe) if requested
24
- for service in spec.services:
25
- ensure_user_service(service)
26
-
27
- with tempfile.TemporaryDirectory() as tmpdir:
28
- # Write the wrapper script (handling prologs, services, and jobs)
29
- wrapper_path = os.path.join(tmpdir, "wrapper.sh")
30
-
31
- # dry run here just displays it
32
- script = spec.generate_wrapper_script()
33
- if dry_run:
34
- return script
35
-
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
50
-
51
- # Submit the JSON string to the Flux instance
52
- # The result.stdout contains the raw JSON Jobspec
53
- return flux.job.submit(handle, result.stdout)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes