airoh 0.1.2__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.
airoh-0.1.2/.gitignore ADDED
@@ -0,0 +1,194 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ # Abstra
171
+ # Abstra is an AI-powered process automation framework.
172
+ # Ignore directories containing user credentials, local state, and settings.
173
+ # Learn more at https://abstra.io/docs
174
+ .abstra/
175
+
176
+ # Visual Studio Code
177
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
178
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
179
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
180
+ # you could uncomment the following to ignore the enitre vscode folder
181
+ # .vscode/
182
+
183
+ # Ruff stuff:
184
+ .ruff_cache/
185
+
186
+ # PyPI configuration file
187
+ .pypirc
188
+
189
+ # Cursor
190
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
191
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
192
+ # refer to https://docs.cursor.com/context/ignore-files
193
+ .cursorignore
194
+ .cursorindexingignore
airoh-0.1.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 SIMEXP
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.
airoh-0.1.2/PKG-INFO ADDED
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: airoh
3
+ Version: 0.1.2
4
+ Summary: A lightweight task library for reproducible workflows
5
+ Project-URL: Homepage, https://github.com/simexp/airoh
6
+ Project-URL: Issues, https://github.com/SIMEXP/airoh/issues
7
+ Author-email: Lune Bellec <lune.bellec@umontreal.ca>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.8
11
+ Requires-Dist: invoke>=2.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # airoh
15
+ _Because reproducible science takes clean tasks. And why don't you have a cup of relaxing jasmin tea?_
16
+
17
+ **airoh** is a lightweight Python task library built with [`invoke`](https://www.pyinvoke.org/), designed for reproducible research workflows. It provides pre-written, modular task definitions that can be easily reused in your own `tasks.py` file โ€” no boilerplate, just useful automation.
18
+
19
+ ## Installation
20
+ Installation through PIP:
21
+ ```bash
22
+ pip install airoh
23
+ ```
24
+
25
+ For local deployment:
26
+
27
+ ```bash
28
+ git clone https://github.com/simexp/airoh.git
29
+ cd airoh
30
+ pip install -e .
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ You can use `airoh` in your project simply by importing tasks in your `tasks.py` file.
36
+
37
+ ### Minimal Example
38
+
39
+ ```python
40
+ # tasks.py
41
+ from airoh.utils import run_figures, setup_env_python
42
+ ```
43
+
44
+ Now you can call:
45
+
46
+ ```bash
47
+ invoke run-figures
48
+ invoke setup-env-python
49
+ ```
50
+
51
+ ## Available Tasks
52
+
53
+ ### From `containers.py`
54
+
55
+ * `docker-build` โ€” Build a Docker image from a Dockerfile
56
+ * `docker-archive` โ€” Archive the Docker image to `.tar.gz`
57
+ * `docker-setup` โ€” Download and load a Docker image from URL
58
+ * `docker-run` โ€” Run an `invoke` task inside Docker
59
+ * `apptainer-archive` โ€” Build an Apptainer image from Docker
60
+ * `apptainer-run` โ€” Run an `invoke` task inside Apptainer
61
+
62
+ ### From `utils.py`
63
+
64
+ * `ensure_submodule` โ€” Ensure a git submodule is present and up to date
65
+ * `install_local` โ€” Install a local Python package in editable mode using pip
66
+ * `setup-env-python` โ€” Install Python dependencies from a file
67
+ * `run-figures` โ€” Run Jupyter notebooks to generate figures
68
+ * `clean_folder` โ€” Remove an entire directory recursively. Use with caution!!!
69
+
70
+ ### From `datalad.py`
71
+ * `get-data` โ€” Install and retrieve a subdataset listed in `invoke.yaml`
72
+ * `import-archive` โ€” Download a remote archive (e.g. from Zenodo), extract its contents with `datalad`, and optionally drop the original file
73
+ * `import-file` โ€” Download a remote file (e.g. from Zenodo), using `datalad`
74
+
75
+ ## Requirements
76
+
77
+ * Python โ‰ฅ 3.8
78
+ * [`invoke`](https://www.pyinvoke.org/) โ‰ฅ 2.0
79
+ * Docker (for container tasks)
80
+ * Apptainer (optional, for `.sif` support)
81
+ * `jupyter` (if using `run-figures`)
82
+
83
+ ## Philosophy
84
+
85
+ Inspired by Uncle Iroh from *Avatar: The Last Airbender*, `airoh` aims to bring simplicity, reusability, and clarity to research infrastructure โ€” one well-structured task at a time. It is meant to support a concrete implementation of the [YODA principles](https://handbook.datalad.org/en/latest/basics/101-127-yoda.html).
86
+
87
+ ## License
88
+
89
+ MIT ยฉ Lune Bellec
airoh-0.1.2/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # airoh
2
+ _Because reproducible science takes clean tasks. And why don't you have a cup of relaxing jasmin tea?_
3
+
4
+ **airoh** is a lightweight Python task library built with [`invoke`](https://www.pyinvoke.org/), designed for reproducible research workflows. It provides pre-written, modular task definitions that can be easily reused in your own `tasks.py` file โ€” no boilerplate, just useful automation.
5
+
6
+ ## Installation
7
+ Installation through PIP:
8
+ ```bash
9
+ pip install airoh
10
+ ```
11
+
12
+ For local deployment:
13
+
14
+ ```bash
15
+ git clone https://github.com/simexp/airoh.git
16
+ cd airoh
17
+ pip install -e .
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ You can use `airoh` in your project simply by importing tasks in your `tasks.py` file.
23
+
24
+ ### Minimal Example
25
+
26
+ ```python
27
+ # tasks.py
28
+ from airoh.utils import run_figures, setup_env_python
29
+ ```
30
+
31
+ Now you can call:
32
+
33
+ ```bash
34
+ invoke run-figures
35
+ invoke setup-env-python
36
+ ```
37
+
38
+ ## Available Tasks
39
+
40
+ ### From `containers.py`
41
+
42
+ * `docker-build` โ€” Build a Docker image from a Dockerfile
43
+ * `docker-archive` โ€” Archive the Docker image to `.tar.gz`
44
+ * `docker-setup` โ€” Download and load a Docker image from URL
45
+ * `docker-run` โ€” Run an `invoke` task inside Docker
46
+ * `apptainer-archive` โ€” Build an Apptainer image from Docker
47
+ * `apptainer-run` โ€” Run an `invoke` task inside Apptainer
48
+
49
+ ### From `utils.py`
50
+
51
+ * `ensure_submodule` โ€” Ensure a git submodule is present and up to date
52
+ * `install_local` โ€” Install a local Python package in editable mode using pip
53
+ * `setup-env-python` โ€” Install Python dependencies from a file
54
+ * `run-figures` โ€” Run Jupyter notebooks to generate figures
55
+ * `clean_folder` โ€” Remove an entire directory recursively. Use with caution!!!
56
+
57
+ ### From `datalad.py`
58
+ * `get-data` โ€” Install and retrieve a subdataset listed in `invoke.yaml`
59
+ * `import-archive` โ€” Download a remote archive (e.g. from Zenodo), extract its contents with `datalad`, and optionally drop the original file
60
+ * `import-file` โ€” Download a remote file (e.g. from Zenodo), using `datalad`
61
+
62
+ ## Requirements
63
+
64
+ * Python โ‰ฅ 3.8
65
+ * [`invoke`](https://www.pyinvoke.org/) โ‰ฅ 2.0
66
+ * Docker (for container tasks)
67
+ * Apptainer (optional, for `.sif` support)
68
+ * `jupyter` (if using `run-figures`)
69
+
70
+ ## Philosophy
71
+
72
+ Inspired by Uncle Iroh from *Avatar: The Last Airbender*, `airoh` aims to bring simplicity, reusability, and clarity to research infrastructure โ€” one well-structured task at a time. It is meant to support a concrete implementation of the [YODA principles](https://handbook.datalad.org/en/latest/basics/101-127-yoda.html).
73
+
74
+ ## License
75
+
76
+ MIT ยฉ Lune Bellec
File without changes
@@ -0,0 +1,160 @@
1
+ # src/airoh/containers.py
2
+ import os
3
+ import shutil
4
+ import tempfile
5
+ import gzip
6
+ from pathlib import Path
7
+ from invoke import task
8
+
9
+ def _set_image(c, image=None):
10
+ """
11
+ Resolve the Docker image name from parameter or invoke.yaml.
12
+ """
13
+ image = image or c.config.get("docker_image")
14
+ if not image:
15
+ raise ValueError("No Docker image specified. Please set docker_image in invoke.yaml or pass it explicitly.")
16
+ return image
17
+
18
+ @task
19
+ def docker_build(c, image=None):
20
+ """
21
+ Build the Docker image from the Dockerfile in the project root.
22
+ """
23
+ image = _set_image(c, image)
24
+ print(f"๐Ÿณ Building Docker image: {image}")
25
+ c.run(f"docker build -t {image} .")
26
+
27
+ @task
28
+ def docker_archive(c, image=None):
29
+ """
30
+ Save the Docker image to a compressed archive for Zenodo or transport.
31
+ """
32
+ image = _set_image(c, image)
33
+ output = f"{image}.tar.gz"
34
+ print(f"๐Ÿ“ฆ Archiving Docker image '{image}' to {output}...")
35
+ c.run(f"docker save {image} | gzip > {output}")
36
+ print("๐Ÿชฆ Archive complete.")
37
+
38
+ @task
39
+ def docker_setup(c, url=None, image=None):
40
+ """
41
+ Download and load the prebuilt Docker image from Zenodo.
42
+ """
43
+ image = _set_image(c, image)
44
+ if not url:
45
+ url = c.config.get("docker_archive")
46
+ if not url:
47
+ raise ValueError("No archive URL provided. Set docker_archive in invoke.yaml or pass --url.")
48
+
49
+ output = f"{image}.tar.gz"
50
+ if not os.path.exists(output):
51
+ print(f"๐Ÿ“ฅ Downloading container from {url}...")
52
+ c.run(f"wget -O {output} '{url}'")
53
+ else:
54
+ print(f"๐Ÿ“ฆ Container archive already exists: {output}")
55
+
56
+ print("๐Ÿณ Loading Docker image...")
57
+ c.run(f"gunzip -c {output} | docker load")
58
+ print("โœจ Container setup complete.")
59
+
60
+ def _ensure_docker_image_loaded(c, image, image_tar):
61
+ """
62
+ Ensure the specified Docker image is available. If not, try to load it from a .tar or .tar.gz.
63
+ """
64
+ if not shutil.which("docker"):
65
+ raise RuntimeError("โŒ Docker is not installed or not in PATH. Please install Docker.")
66
+
67
+ result = c.run(f"docker images -q {image}", hide=True, warn=True)
68
+ if result.stdout.strip():
69
+ c.run(f"docker tag {image} {image}:latest", warn=True)
70
+ return
71
+
72
+ print(f"๐Ÿ“ฆ Docker image '{image}' not found. Attempting to load from {image_tar}...")
73
+
74
+ image_tar = Path(image_tar)
75
+ if not image_tar.exists():
76
+ raise FileNotFoundError(f"โŒ Docker image file not found: {image_tar}")
77
+
78
+ if image_tar.suffixes[-2:] == ['.tar', '.gz']:
79
+ print(f"๐Ÿ–œ๏ธ Extracting {image_tar}...")
80
+ with tempfile.NamedTemporaryFile(suffix=".tar", delete=False) as temp_tar:
81
+ with gzip.open(image_tar, "rb") as f_in:
82
+ shutil.copyfileobj(f_in, temp_tar)
83
+ temp_tar_path = temp_tar.name
84
+ c.run(f"docker load -i {temp_tar_path}")
85
+ os.remove(temp_tar_path)
86
+ elif image_tar.suffix == ".tar":
87
+ c.run(f"docker load -i {image_tar}")
88
+ else:
89
+ raise ValueError("โŒ Unsupported container format. Use .tar or .tar.gz")
90
+
91
+ c.run(f"docker tag {image} {image}:latest", warn=True)
92
+
93
+ @task
94
+ def docker_run(c, task, args=""):
95
+ """
96
+ Run an invoke task inside the Docker container.
97
+
98
+ Args:
99
+ task (str): the invoke task to run
100
+ args (str): any additional CLI args to pass to the task
101
+ """
102
+ image = c.config.get("docker_image")
103
+ image_tar = f"{image}.tar.gz"
104
+
105
+ _ensure_docker_image_loaded(c, image, image_tar)
106
+
107
+ hostdir = os.getcwd()
108
+ workdir = "/home/jovyan/work"
109
+ cmd = f"invoke {task} {args}"
110
+ docker_cmd = f'docker run --rm -v {hostdir}:{workdir} -w {workdir} {image} {cmd}'
111
+
112
+ print(f"๐Ÿณ Running inside container: {cmd}")
113
+ c.run(docker_cmd)
114
+
115
+ @task
116
+ def apptainer_archive(c, image=None):
117
+ """
118
+ Archive the Apptainer (Singularity) image from a Docker image using Docker daemon.
119
+ Builds the .sif file if not present.
120
+ """
121
+ image = _set_image(c, image)
122
+ sif_path = Path(f"{image}.sif")
123
+
124
+ if not shutil.which("apptainer"):
125
+ raise RuntimeError("โŒ Apptainer is not installed or not in PATH. Please install it.")
126
+
127
+ if sif_path.exists():
128
+ print(f"โœ… Apptainer image already exists at {sif_path}. Skipping build.")
129
+ return
130
+
131
+ if not shutil.which("docker"):
132
+ raise RuntimeError("โŒ Docker is required to build from Docker image. Please install it.")
133
+
134
+ _ensure_docker_image_loaded(c, image, f"{image}.tar.gz")
135
+ print(f"๐Ÿงช Building Apptainer image {sif_path} from Docker image {image}:latest...")
136
+ c.run(f"apptainer build {sif_path} docker-daemon:{image}:latest")
137
+ print("โœ… Apptainer image build complete.")
138
+
139
+ @task
140
+ def apptainer_run(c, task, args=""):
141
+ """
142
+ Run an invoke task inside the Apptainer container.
143
+
144
+ Args:
145
+ task (str): the invoke task to run
146
+ args (str): any additional CLI args to pass to the task
147
+ """
148
+ docker_image = c.config.get("docker_image")
149
+ sif_path = Path(f"{docker_image}.sif")
150
+
151
+ if not sif_path.exists():
152
+ raise FileNotFoundError(f"โŒ Apptainer image not found: {sif_path}")
153
+
154
+ hostdir = os.getcwd()
155
+ workdir = "/home/jovyan/work"
156
+ cmd = f"invoke {task} {args}"
157
+ apptainer_cmd = f"apptainer exec --cleanenv --bind {hostdir}:{workdir} {sif_path} {cmd}"
158
+
159
+ print(f"๐Ÿงช Running inside Apptainer: {cmd}")
160
+ c.run(apptainer_cmd)
@@ -0,0 +1,82 @@
1
+ # src/airoh/datalad.py
2
+ from invoke import task
3
+ import os
4
+ import shlex
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ @task
9
+ def get_data(c, name):
10
+ """
11
+ Ensure a Datalad subdataset is installed and retrieved.
12
+
13
+ Parameters:
14
+ name (str): Name of the dataset as defined in invoke.yaml under 'datasets'.
15
+ """
16
+ datasets = c.config.get("datasets", {})
17
+ if name not in datasets:
18
+ raise ValueError(f"โŒ Dataset '{name}' not found in invoke.yaml under 'datasets'.")
19
+
20
+ path = datasets[name]
21
+ print(f"๐Ÿ“ฆ Checking dataset '{name}' at: {path}")
22
+
23
+ if not os.path.exists(path):
24
+ print(f"๐Ÿ“ฅ Installing subdataset '{name}'...")
25
+ c.run(f"datalad install --recursive {path}")
26
+
27
+ print(f"๐Ÿ“ฅ Retrieving data for '{name}'...")
28
+ c.run(f"datalad get {path}")
29
+ print("โœ… Done.")
30
+
31
+ @task
32
+ def import_file(c, name):
33
+ """
34
+ Download a file using its name from invoke.yaml -> files.<name>.
35
+
36
+ Parameters:
37
+ name (str): Key from the 'files' section in invoke.yaml.
38
+ """
39
+ files = c.config.get("files", {})
40
+ if name not in files:
41
+ raise ValueError(f"โŒ No file config found for '{name}' in invoke.yaml.")
42
+
43
+ entry = files[name]
44
+ url = entry.get("url")
45
+ output_file = entry.get("output_file")
46
+
47
+ if not url or not output_file:
48
+ raise ValueError(f"โŒ Entry for '{name}' must define both 'url' and 'output_file'.")
49
+
50
+ c.run(f"datalad download-url -O {shlex.quote(output_file)} {shlex.quote(url)}")
51
+ print(f"โœ… Downloaded {name} to {output_file}")
52
+
53
+ @task
54
+ def import_archive(c, url, archive_name=None, target_dir=".", drop_archive=False):
55
+ """
56
+ Download a remote archive (e.g. from Zenodo or Figshare) and extract its content with Datalad.
57
+
58
+ Parameters:
59
+ url (str): URL to the archive (e.g. .zip, .tar.gz). For Figshare-style links, explicitly provide
60
+ `archive_name` if the URL does not end with the actual filename.
61
+ archive_name (str): Optional filename (default: basename of URL).
62
+ target_dir (str): Directory to extract into (default: current dir).
63
+ drop_archive (bool): Whether to drop the original archive from annex after extraction.
64
+ """
65
+ archive_name = archive_name or os.path.basename(url)
66
+ archive_path = os.path.join(target_dir, archive_name)
67
+
68
+ import_file(c, url, archive_path)
69
+
70
+ archive_exts = ['.zip', '.tar', '.tar.gz', '.tgz', '.tar.bz2', '.7z']
71
+ if not any(archive_path.endswith(ext) for ext in archive_exts):
72
+ print("โš ๏ธ Skipping extraction โ€” file does not appear to be a supported archive.")
73
+ return
74
+
75
+ print(f"๐Ÿ“ฆ Extracting archive content into {target_dir}...")
76
+ c.run(f"datalad add-archive-content --delete --extract {shlex.quote(archive_path)} -d {shlex.quote(target_dir)}")
77
+
78
+ if drop_archive:
79
+ print(f"๐Ÿงน Dropping archive from annex: {archive_path}")
80
+ c.run(f"datalad drop {shlex.quote(archive_path)}")
81
+
82
+ print("โœ… Archive import complete.")
@@ -0,0 +1,95 @@
1
+ # src/airoh/utils.py
2
+ import os
3
+ import shutil
4
+ from pathlib import Path
5
+ from invoke import task
6
+
7
+ @task
8
+ def setup_env_python(c, reqs="requirements.txt"):
9
+ """
10
+ Set up Python environment by installing from a requirements file.
11
+ """
12
+ if not os.path.exists(reqs):
13
+ raise FileNotFoundError(f"โš ๏ธ Requirements file not found: {reqs}")
14
+
15
+ print(f"๐Ÿ Installing Python requirements from {reqs}...")
16
+ c.run(f"pip install -r {reqs}")
17
+
18
+ @task
19
+ def ensure_submodule(c, path):
20
+ """
21
+ Ensure a git submodule is present and up to date.
22
+
23
+ Parameters:
24
+ path (str): Path to the submodule directory
25
+ """
26
+ if not os.path.exists(path) or not os.path.exists(os.path.join(path, ".git")):
27
+ print(f"๐Ÿ“ฆ Initializing submodule at {path}...")
28
+ c.run(f"git submodule update --init --recursive {path}")
29
+ else:
30
+ print(f"๐Ÿ”„ Updating submodule at {path}...")
31
+ c.run(f"git submodule update --remote {path}")
32
+
33
+ @task
34
+ def install_local(c, path):
35
+ """
36
+ Install a local Python package in editable mode using pip.
37
+
38
+ Parameters:
39
+ path (str): Path to the package directory
40
+ """
41
+ if not os.path.exists(path):
42
+ raise FileNotFoundError(f"โŒ Package path not found: {path}")
43
+
44
+ print(f"๐Ÿ”ง Installing package from {path} in editable mode...")
45
+ c.run(f"pip install -e {path}")
46
+ print("โœ… Editable install complete.")
47
+
48
+ @task
49
+ def clean_folder(c, dir_name, label=None):
50
+ """
51
+ Remove an entire directory recursively. Use with caution!!!
52
+ Parameters:
53
+ dir_name (str): Path to be removed
54
+ label (str, optional): label to use for path in the verbose
55
+ """
56
+ label = label or dir_name
57
+ if os.path.exists(dir_name):
58
+ shutil.rmtree(dir_name)
59
+ print(f"๐Ÿ’ฅ Removed {label} at {dir_name}")
60
+ else:
61
+ print(f"๐Ÿซง Nothing to clean: {label}")
62
+
63
+ @task
64
+ def run_figures(c, notebooks_path=None, figures_base=None):
65
+ """
66
+ Run figure notebooks, skipping any that already have output folders.
67
+ Notebooks directory and output location pulled from invoke.yaml.
68
+ """
69
+ import pathlib as lib
70
+
71
+ notebooks_path = Path(notebooks_path or c.config.get("notebooks_dir", "code/figures"))
72
+ figures_base = Path(figures_base or c.config.get("figures_dir", "output_data/Figures"))
73
+
74
+ if not notebooks_path.exists():
75
+ print(f"โš ๏ธ Notebooks directory not found: {notebooks_path}")
76
+ return
77
+
78
+ notebooks = sorted(notebooks_path.glob("*.ipynb"))
79
+
80
+ if not notebooks:
81
+ print(f"โš ๏ธ No notebooks found in {notebooks_path}/")
82
+ return
83
+
84
+ for nb in notebooks:
85
+ fig_name = nb.stem
86
+ fig_output_dir = figures_base / fig_name
87
+
88
+ if fig_output_dir.exists():
89
+ print(f"โœ… Skipping {nb.name} (output exists at {fig_output_dir})")
90
+ continue
91
+
92
+ print(f"๐Ÿ“ˆ Running {nb.name}...")
93
+ c.run(f"jupyter nbconvert --to notebook --execute --inplace {nb}")
94
+
95
+ print("๐ŸŽ‰ All figure notebooks processed.")
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "airoh"
7
+ version = "0.1.2"
8
+ description = "A lightweight task library for reproducible workflows"
9
+ authors = [{ name = "Lune Bellec", email = "lune.bellec@umontreal.ca" }]
10
+ readme = "README.md"
11
+ license = {text = "MIT"}
12
+ requires-python = ">=3.8"
13
+ dependencies = ["invoke>=2.0"]
14
+
15
+ [project.urls]
16
+ Homepage = "https://github.com/simexp/airoh"
17
+ Issues = "https://github.com/SIMEXP/airoh/issues"
18
+
19
+ [tool.ruff]
20
+ line-length = 100
21
+ target-version = "py38"
22
+ select = ["I"] # E = pycodestyle, F = pyflakes, I = isort
23
+
24
+ [tool.hatch.envs.dev]
25
+ dependencies = [
26
+ "invoke>=2.0",
27
+ "ruff>=0.1.0",
28
+ "pytest>=7.0"
29
+ ]
@@ -0,0 +1 @@
1
+ invoke>=2.0
@@ -0,0 +1,5 @@
1
+ hatchling>=1.18
2
+ invoke>=2.0
3
+ pytest>=7.0
4
+ ruff>=0.1.0
5
+ twine>=0.4