kblaunch 0.1.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.
@@ -0,0 +1,21 @@
1
+ # publish.yml
2
+ name: "Publish"
3
+ on:
4
+ release:
5
+ types: ["published"]
6
+
7
+ jobs:
8
+ pypi:
9
+ name: Publish to PyPI
10
+ runs-on: ubuntu-latest
11
+ # Environment and permissions trusted publishing.
12
+ environment:
13
+ # Create this environment in the GitHub repository under Settings -> Environments
14
+ name: release
15
+ permissions:
16
+ id-token: write
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: astral-sh/setup-uv@v3
20
+ - run: uv build
21
+ - run: uv publish --trusted-publishing always
@@ -0,0 +1,26 @@
1
+ # publish.yml
2
+ name: "Test"
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ branches:
9
+ - main
10
+ jobs:
11
+ run:
12
+ name: "Run tests"
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - name: Install uv
17
+ uses: astral-sh/setup-uv@v3
18
+ with:
19
+ enable-cache: true
20
+ cache-dependency-glob: uv.lock
21
+
22
+ - name: Set up Python
23
+ run: uv python install 3.12
24
+
25
+ - name: Run tests
26
+ run: uv run --extra test pytest
@@ -0,0 +1,173 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ .python-version
3
+ .vscode/
4
+ __pycache__/
5
+ *.py[cod]
6
+ *$py.class
7
+
8
+ # C extensions
9
+ *.so
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ build/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ .eggs/
19
+ lib/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ share/python-wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+ MANIFEST
30
+
31
+ # PyInstaller
32
+ # Usually these files are written by a python script from a template
33
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
34
+ *.manifest
35
+ *.spec
36
+
37
+ # Installer logs
38
+ pip-log.txt
39
+ pip-delete-this-directory.txt
40
+
41
+ # Unit test / coverage reports
42
+ htmlcov/
43
+ .tox/
44
+ .nox/
45
+ .coverage
46
+ .coverage.*
47
+ .cache
48
+ nosetests.xml
49
+ coverage.xml
50
+ *.cover
51
+ *.py,cover
52
+ .hypothesis/
53
+ .pytest_cache/
54
+ cover/
55
+
56
+ # Translations
57
+ *.mo
58
+ *.pot
59
+
60
+ # Django stuff:
61
+ *.log
62
+ local_settings.py
63
+ db.sqlite3
64
+ db.sqlite3-journal
65
+
66
+ # Flask stuff:
67
+ instance/
68
+ .webassets-cache
69
+
70
+ # Scrapy stuff:
71
+ .scrapy
72
+
73
+ # Sphinx documentation
74
+ docs/_build/
75
+
76
+ # PyBuilder
77
+ .pybuilder/
78
+ target/
79
+
80
+ # Jupyter Notebook
81
+ .ipynb_checkpoints
82
+
83
+ # IPython
84
+ profile_default/
85
+ ipython_config.py
86
+
87
+ # pyenv
88
+ # For a library or package, you might want to ignore these files since the code is
89
+ # intended to run in multiple environments; otherwise, check them in:
90
+ # .python-version
91
+
92
+ # pipenv
93
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
95
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
96
+ # install all needed dependencies.
97
+ #Pipfile.lock
98
+
99
+ # UV
100
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
101
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
102
+ # commonly ignored for libraries.
103
+ #uv.lock
104
+
105
+ # poetry
106
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
107
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
108
+ # commonly ignored for libraries.
109
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
110
+ #poetry.lock
111
+
112
+ # pdm
113
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
114
+ #pdm.lock
115
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
116
+ # in version control.
117
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
118
+ .pdm.toml
119
+ .pdm-python
120
+ .pdm-build/
121
+
122
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
123
+ __pypackages__/
124
+
125
+ # Celery stuff
126
+ celerybeat-schedule
127
+ celerybeat.pid
128
+
129
+ # SageMath parsed files
130
+ *.sage.py
131
+
132
+ # Environments
133
+ .env
134
+ .venv
135
+ env/
136
+ venv/
137
+ ENV/
138
+ env.bak/
139
+ venv.bak/
140
+
141
+ # Spyder project settings
142
+ .spyderproject
143
+ .spyproject
144
+
145
+ # Rope project settings
146
+ .ropeproject
147
+
148
+ # mkdocs documentation
149
+ /site
150
+
151
+ # mypy
152
+ .mypy_cache/
153
+ .dmypy.json
154
+ dmypy.json
155
+
156
+ # Pyre type checker
157
+ .pyre/
158
+
159
+ # pytype static type analyzer
160
+ .pytype/
161
+
162
+ # Cython debug symbols
163
+ cython_debug/
164
+
165
+ # PyCharm
166
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
167
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
168
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
169
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
170
+ #.idea/
171
+
172
+ # PyPI configuration file
173
+ .pypirc
kblaunch-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Gautier Dagan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: kblaunch
3
+ Version: 0.1.0
4
+ Summary: A CLI tool for launching Kubernetes jobs in EIDF
5
+ Author-email: Gautier Dagan <gautier.dagan@ed.ac.uk>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.9
9
+ Requires-Dist: kubejobs>=0.4.5
10
+ Requires-Dist: kubernetes>=31.0.0
11
+ Requires-Dist: loguru>=0.7.3
12
+ Requires-Dist: python-dotenv>=1.0.1
13
+ Requires-Dist: typer>=0.15.1
14
+ Provides-Extra: test
15
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'test'
16
+ Requires-Dist: pytest-mock>=3.10.0; extra == 'test'
17
+ Requires-Dist: pytest>=7.0; extra == 'test'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # kblaunch
21
+
22
+ [![Test](https://github.com/gautierdag/kblaunch/actions/workflows/test.yaml/badge.svg)](https://github.com/gautierdag/kblaunch/actions/workflows/test.yaml)
23
+ ![Python Version](https://img.shields.io/badge/python-3.9+-blue)
24
+ ![Ruff](https://img.shields.io/badge/linter-ruff-blue)
25
+ [![PyPI Version](https://img.shields.io/pypi/v/kblaunch)](https://pypi.org/project/kblaunch/)
26
+
27
+ A CLI tool for launching Kubernetes jobs with environment variable and secret management.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install git+https://github.com/gautierdag/kblaunch.git
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ### Basic Usage
38
+
39
+ Launch a simple job:
40
+
41
+ ```bash
42
+ kblaunch \
43
+ --email your.email@ed.ac.uk \
44
+ --job-name myjob \
45
+ --command "python script.py"
46
+ ```
47
+
48
+ ### With Environment Variables
49
+
50
+ 1. From local environment:
51
+
52
+ ```bash
53
+ kblaunch \
54
+ --job-name myjob \
55
+ --command "python script.py" \
56
+ --local-env-vars PATH \
57
+ --local-env-vars PYTHONPATH
58
+ ```
59
+
60
+ 2. From Kubernetes secrets:
61
+
62
+ ```bash
63
+ kblaunch \
64
+ --job-name myjob \
65
+ --command "python script.py" \
66
+ --secrets-env-vars mysecret1 \
67
+ --secrets-env-vars mysecret2
68
+ ```
69
+
70
+ 3. From .env file:
71
+
72
+ ```bash
73
+ kblaunch \
74
+ --job-name myjob \
75
+ --command "python script.py" \
76
+ --load-dotenv
77
+ ```
78
+
79
+ ### GPU Jobs
80
+
81
+ Specify GPU requirements:
82
+
83
+ ```bash
84
+ kblaunch \
85
+ --job-name gpu-job \
86
+ --command "python train.py" \
87
+ --gpu-limit 2 \
88
+ --gpu-product "NVIDIA-A100-80GB-PCIe"
89
+ ```
90
+
91
+ ### Interactive Mode
92
+
93
+ Launch an interactive job:
94
+
95
+ ```bash
96
+ kblaunch \
97
+ --job-name interactive \
98
+ --interactive
99
+ ```
100
+
101
+ ## Options
102
+
103
+ - `--email`: User email [required]
104
+ - `--job-name`: Name of the Kubernetes job [required]
105
+ - `--docker-image`: Docker image (default: "nvcr.io/nvidia/cuda:12.0.0-devel-ubuntu22.04")
106
+ - `--namespace`: Kubernetes namespace (default: "informatics")
107
+ - `--queue-name`: Kueue queue name
108
+ - `--interactive`: Run in interactive mode (default: False)
109
+ - `--command`: Command to run in the container [required]
110
+ - `--cpu-request`: CPU request (default: "1")
111
+ - `--ram-request`: RAM request (default: "8Gi")
112
+ - `--gpu-limit`: GPU limit (default: 1)
113
+ - `--gpu-product`: GPU product (default: "NVIDIA-A100-80GB-PCIe")
114
+ - `--secrets-env-vars`: List of secret environment variables
115
+ - `--local-env-vars`: List of local environment variables
116
+ - `--load-dotenv`: Load environment variables from .env file (default: True)
117
+
118
+ ## Features
119
+
120
+ - Kubernetes job management
121
+ - Environment variable handling from multiple sources
122
+ - Kubernetes secrets integration
123
+ - GPU job support
124
+ - Interactive mode
125
+ - Automatic job cleanup
126
+ - Slack notifications (when configured)
@@ -0,0 +1,107 @@
1
+ # kblaunch
2
+
3
+ [![Test](https://github.com/gautierdag/kblaunch/actions/workflows/test.yaml/badge.svg)](https://github.com/gautierdag/kblaunch/actions/workflows/test.yaml)
4
+ ![Python Version](https://img.shields.io/badge/python-3.9+-blue)
5
+ ![Ruff](https://img.shields.io/badge/linter-ruff-blue)
6
+ [![PyPI Version](https://img.shields.io/pypi/v/kblaunch)](https://pypi.org/project/kblaunch/)
7
+
8
+ A CLI tool for launching Kubernetes jobs with environment variable and secret management.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pip install git+https://github.com/gautierdag/kblaunch.git
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ### Basic Usage
19
+
20
+ Launch a simple job:
21
+
22
+ ```bash
23
+ kblaunch \
24
+ --email your.email@ed.ac.uk \
25
+ --job-name myjob \
26
+ --command "python script.py"
27
+ ```
28
+
29
+ ### With Environment Variables
30
+
31
+ 1. From local environment:
32
+
33
+ ```bash
34
+ kblaunch \
35
+ --job-name myjob \
36
+ --command "python script.py" \
37
+ --local-env-vars PATH \
38
+ --local-env-vars PYTHONPATH
39
+ ```
40
+
41
+ 2. From Kubernetes secrets:
42
+
43
+ ```bash
44
+ kblaunch \
45
+ --job-name myjob \
46
+ --command "python script.py" \
47
+ --secrets-env-vars mysecret1 \
48
+ --secrets-env-vars mysecret2
49
+ ```
50
+
51
+ 3. From .env file:
52
+
53
+ ```bash
54
+ kblaunch \
55
+ --job-name myjob \
56
+ --command "python script.py" \
57
+ --load-dotenv
58
+ ```
59
+
60
+ ### GPU Jobs
61
+
62
+ Specify GPU requirements:
63
+
64
+ ```bash
65
+ kblaunch \
66
+ --job-name gpu-job \
67
+ --command "python train.py" \
68
+ --gpu-limit 2 \
69
+ --gpu-product "NVIDIA-A100-80GB-PCIe"
70
+ ```
71
+
72
+ ### Interactive Mode
73
+
74
+ Launch an interactive job:
75
+
76
+ ```bash
77
+ kblaunch \
78
+ --job-name interactive \
79
+ --interactive
80
+ ```
81
+
82
+ ## Options
83
+
84
+ - `--email`: User email [required]
85
+ - `--job-name`: Name of the Kubernetes job [required]
86
+ - `--docker-image`: Docker image (default: "nvcr.io/nvidia/cuda:12.0.0-devel-ubuntu22.04")
87
+ - `--namespace`: Kubernetes namespace (default: "informatics")
88
+ - `--queue-name`: Kueue queue name
89
+ - `--interactive`: Run in interactive mode (default: False)
90
+ - `--command`: Command to run in the container [required]
91
+ - `--cpu-request`: CPU request (default: "1")
92
+ - `--ram-request`: RAM request (default: "8Gi")
93
+ - `--gpu-limit`: GPU limit (default: 1)
94
+ - `--gpu-product`: GPU product (default: "NVIDIA-A100-80GB-PCIe")
95
+ - `--secrets-env-vars`: List of secret environment variables
96
+ - `--local-env-vars`: List of local environment variables
97
+ - `--load-dotenv`: Load environment variables from .env file (default: True)
98
+
99
+ ## Features
100
+
101
+ - Kubernetes job management
102
+ - Environment variable handling from multiple sources
103
+ - Kubernetes secrets integration
104
+ - GPU job support
105
+ - Interactive mode
106
+ - Automatic job cleanup
107
+ - Slack notifications (when configured)
File without changes
@@ -0,0 +1,195 @@
1
+ import base64
2
+ import os
3
+
4
+ import typer
5
+ from kubejobs.jobs import KubernetesJob, KueueQueue
6
+ from kubernetes import client, config
7
+ from loguru import logger
8
+
9
+ app = typer.Typer()
10
+
11
+
12
+ def check_if_completed(job_name: str, namespace: str = "informatics") -> bool:
13
+ # Load the kube config
14
+ config.load_kube_config()
15
+
16
+ # Create an instance of the API class
17
+ api = client.BatchV1Api()
18
+
19
+ job_exists = False
20
+ is_completed = True
21
+
22
+ # Check if the job exists in the specified namespace
23
+ jobs = api.list_namespaced_job(namespace)
24
+ if job_name in {job.metadata.name for job in jobs.items}:
25
+ job_exists = True
26
+
27
+ if job_exists is True:
28
+ job = api.read_namespaced_job(job_name, namespace)
29
+ is_completed = False
30
+
31
+ # Check the status conditions
32
+ if job.status.conditions:
33
+ for condition in job.status.conditions:
34
+ if condition.type == "Complete" and condition.status == "True":
35
+ is_completed = True
36
+ elif condition.type == "Failed" and condition.status == "True":
37
+ logger.error(f"Job {job_name} has failed.")
38
+ else:
39
+ logger.info(f"Job {job_name} still running or status is unknown.")
40
+
41
+ if is_completed:
42
+ api_res = api.delete_namespaced_job(
43
+ name=job_name,
44
+ namespace=namespace,
45
+ body=client.V1DeleteOptions(propagation_policy="Foreground"),
46
+ )
47
+ logger.info(f"Job '{job_name}' deleted. Status: {api_res.status}")
48
+ return is_completed
49
+
50
+
51
+ def send_message_command(env_vars: dict) -> str:
52
+ if "SLACK_WEBHOOK" not in env_vars:
53
+ logger.debug("SLACK_WEBHOOK not found in env_vars.")
54
+ return ""
55
+ webhook = env_vars["SLACK_WEBHOOK"]
56
+ return (
57
+ """curl -X POST -H 'Content-type: application/json' --data '{"text":"Job started in '"$POD_NAME"'"}' """
58
+ + webhook
59
+ + " ; "
60
+ )
61
+
62
+
63
+ def get_env_vars(
64
+ local_env_vars: list[str],
65
+ secrets_env_vars: list[str],
66
+ load_dotenv: bool = False,
67
+ namespace: str = "informatics",
68
+ ) -> dict:
69
+ """Get environment variables from local environment and secrets."""
70
+
71
+ if load_dotenv:
72
+ from dotenv import load_dotenv
73
+
74
+ load_dotenv()
75
+
76
+ env_vars = {}
77
+ for var_name in local_env_vars:
78
+ value = os.getenv(var_name)
79
+ if value is not None:
80
+ env_vars[var_name] = value
81
+ else:
82
+ logger.warning(
83
+ f"Environment variable {var_name} not found in local environment"
84
+ )
85
+
86
+ for secret_name in secrets_env_vars:
87
+ v1 = client.CoreV1Api()
88
+ try:
89
+ secret = v1.read_namespaced_secret(name=secret_name, namespace=namespace)
90
+ for key, value in secret.data.items():
91
+ decoded_value = base64.b64decode(value).decode("utf-8")
92
+ if key in env_vars:
93
+ logger.warning(f"Key {key} already set in env_vars.")
94
+ env_vars[key] = decoded_value
95
+ except Exception as e:
96
+ logger.error(f"Error reading secret {secret_name}: {e}")
97
+ raise e
98
+
99
+ return env_vars
100
+
101
+
102
+ def export_env_vars(env_vars: dict) -> str:
103
+ """Export environment variables."""
104
+ cmd = ""
105
+ for key, value in env_vars.items():
106
+ cmd += f" export {key}='{value}' &&"
107
+ cmd = cmd.strip(" &&") + " ; "
108
+ return cmd
109
+
110
+
111
+ @app.command()
112
+ def launch(
113
+ email: str = typer.Option(..., help="User email"),
114
+ job_name: str = typer.Option(..., help="Name of the Kubernetes job"),
115
+ docker_image: str = typer.Option(
116
+ "nvcr.io/nvidia/cuda:12.0.0-devel-ubuntu22.04", help="Docker image"
117
+ ),
118
+ namespace: str = typer.Option("informatics", help="Kubernetes namespace"),
119
+ queue_name: str = typer.Option(KueueQueue.INFORMATICS, help="Kueue queue name"),
120
+ interactive: bool = typer.Option(False, help="Run in interactive mode"),
121
+ command: str = typer.Option(..., help="Command to run in the container"),
122
+ cpu_request: str = typer.Option("1", help="CPU request"),
123
+ ram_request: str = typer.Option("8Gi", help="RAM request"),
124
+ gpu_limit: int = typer.Option(1, help="GPU limit"),
125
+ gpu_product: str = typer.Option("NVIDIA-A100-80GB-PCIe", help="GPU product"),
126
+ secrets_env_vars: list[str] = typer.Option(
127
+ [], # Use empty list as default instead of None
128
+ help="List of secret environment variables to export to the container",
129
+ ),
130
+ local_env_vars: list[str] = typer.Option(
131
+ [], # Use empty list as default instead of None
132
+ help="List of local environment variables to export to the container",
133
+ ),
134
+ load_dotenv: bool = typer.Option(
135
+ True, help="Load environment variables from .env file"
136
+ ),
137
+ ):
138
+ """Launch a Kubernetes job with the specified configuration."""
139
+
140
+ is_completed = check_if_completed(job_name, namespace=namespace)
141
+
142
+ if is_completed is True:
143
+ logger.info(f"Job '{job_name}' is completed. Launching a new job.")
144
+
145
+ if interactive:
146
+ cmd = "while true; do sleep 60; done;"
147
+ else:
148
+ cmd = command
149
+ logger.info(f"Command: {cmd}")
150
+
151
+ # Get local environment variables
152
+ env_vars = get_env_vars(
153
+ local_env_vars=local_env_vars,
154
+ secrets_env_vars=secrets_env_vars,
155
+ load_dotenv=load_dotenv,
156
+ namespace=namespace,
157
+ )
158
+
159
+ logger.info(f"Creating job for: {cmd}")
160
+ job = KubernetesJob(
161
+ name=job_name,
162
+ cpu_request=cpu_request,
163
+ ram_request=ram_request,
164
+ image=docker_image,
165
+ gpu_type="nvidia.com/gpu",
166
+ gpu_limit=gpu_limit,
167
+ gpu_product=gpu_product,
168
+ backoff_limit=0,
169
+ command=["/bin/bash", "-c", "--"],
170
+ args=[export_env_vars(env_vars) + send_message_command(env_vars) + cmd],
171
+ user_email=email,
172
+ namespace=namespace,
173
+ kueue_queue_name=queue_name,
174
+ secret_env_vars=env_vars,
175
+ volume_mounts={
176
+ "nfs": {"mountPath": "/nfs", "server": "10.24.1.255", "path": "/"}
177
+ },
178
+ )
179
+
180
+ job_yaml = job.generate_yaml()
181
+ logger.info(job_yaml)
182
+
183
+ # Run the Job on the Kubernetes cluster
184
+ job.run()
185
+ else:
186
+ logger.info(f"Job '{job_name}' is still running.")
187
+
188
+
189
+ def cli():
190
+ """Entry point for the application"""
191
+ app()
192
+
193
+
194
+ if __name__ == "__main__":
195
+ cli()