airoh 0.1.2__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.
- airoh/__init__.py +0 -0
- airoh/containers.py +160 -0
- airoh/datalad.py +82 -0
- airoh/utils.py +95 -0
- airoh-0.1.2.dist-info/METADATA +89 -0
- airoh-0.1.2.dist-info/RECORD +8 -0
- airoh-0.1.2.dist-info/WHEEL +4 -0
- airoh-0.1.2.dist-info/licenses/LICENSE +21 -0
airoh/__init__.py
ADDED
|
File without changes
|
airoh/containers.py
ADDED
|
@@ -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)
|
airoh/datalad.py
ADDED
|
@@ -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.")
|
airoh/utils.py
ADDED
|
@@ -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,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
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
airoh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
airoh/containers.py,sha256=8fy7SbvcgvGB7zUz3XujZt0V1Mw3df5bu9tpPSGz5w8,5370
|
|
3
|
+
airoh/datalad.py,sha256=UchPdADLLUIMWVpAaleFqVNxqzCCyMyCEs18NhYG3ms,2968
|
|
4
|
+
airoh/utils.py,sha256=yjcxMiinRD79NTrGxHouQv0MEU6p0_lz7B7-6QUWDUs,2997
|
|
5
|
+
airoh-0.1.2.dist-info/METADATA,sha256=mQsmrjvEyCI2SrXxS6dxD9CKWaYcTofzwdV5wvAqVZc,2944
|
|
6
|
+
airoh-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
airoh-0.1.2.dist-info/licenses/LICENSE,sha256=g9RmS4EgjG249_mj6p16XgUnsqp4cpigQnI_pGyEKnk,1063
|
|
8
|
+
airoh-0.1.2.dist-info/RECORD,,
|
|
@@ -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.
|