pybolo 0.1.0__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.
bolo/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """BoloPipe: a lightweight registry for verified HuggingFace inference pipelines."""
2
+
3
+ from ._version import __version__
4
+ from .api import list_params, create_a_venv, pipeline, remove_venv, fetch_templates
5
+
6
+ __all__ = [
7
+ "__version__",
8
+ "list_params",
9
+ "create_a_venv",
10
+ "pipeline",
11
+ "remove_venv",
12
+ "fetch_templates",
13
+ ]
bolo/_version.py ADDED
@@ -0,0 +1,2 @@
1
+ __version__ = "0.1.0"
2
+ __templates_version__ = "0.1.0"
bolo/api.py ADDED
@@ -0,0 +1,317 @@
1
+ import os
2
+ import re
3
+ import ast
4
+ import json
5
+ import shutil
6
+ import subprocess
7
+ import tarfile
8
+ import urllib.request
9
+ from pathlib import Path
10
+ from jinja2 import Environment, StrictUndefined
11
+
12
+ from ._version import __version__, __templates_version__
13
+
14
+ _TEMPLATES_RELEASE_URL = (
15
+ "https://github.com/illinoisdata/bolo-templates/releases/download/"
16
+ "v{version}/templates.tar.gz"
17
+ )
18
+
19
+
20
+ class InferClient:
21
+ _instance = None
22
+ _torch_cu128_url = "https://download.pytorch.org/whl/cu128"
23
+
24
+ def __new__(cls, *args, **kwargs):
25
+ if cls._instance is None:
26
+ cls._instance = super().__new__(cls)
27
+ return cls._instance
28
+
29
+ def __init__(self):
30
+ if getattr(self, "_initialized", False):
31
+ return
32
+
33
+ self.db_dir = self._ensure_templates()
34
+ self.supported_models = set()
35
+
36
+ for rid_dir in self.db_dir.iterdir():
37
+ if rid_dir.is_dir():
38
+ repo_name = rid_dir.name
39
+ repo_id = "/".join(repo_name.split('__SEP__'))
40
+ self.supported_models.add(repo_id)
41
+
42
+ self._initialized = True
43
+
44
+ @staticmethod
45
+ def _ensure_templates() -> Path:
46
+ env_path = os.environ.get("BOLO_TEMPLATES_DIR")
47
+ if env_path:
48
+ return Path(env_path).expanduser()
49
+
50
+ local = Path(__file__).parent / "templates"
51
+ if local.exists() and any(local.iterdir()):
52
+ return local
53
+
54
+ cache_root = Path.home() / ".cache" / "bolo" / "templates"
55
+ cache = cache_root / __templates_version__
56
+ if cache.exists() and any(cache.iterdir()):
57
+ return cache
58
+
59
+ url = _TEMPLATES_RELEASE_URL.format(version=__templates_version__)
60
+ print(f"fetching bolo templates v{__templates_version__} from {url} ...", flush=True)
61
+
62
+ cache_root.mkdir(parents=True, exist_ok=True)
63
+ tmp = cache_root / f".tmp.{__templates_version__}"
64
+ if tmp.exists():
65
+ shutil.rmtree(tmp)
66
+ tmp.mkdir()
67
+
68
+ try:
69
+ with urllib.request.urlopen(url) as resp, tarfile.open(fileobj=resp, mode="r|gz") as tar:
70
+ tar.extractall(tmp)
71
+ tmp.rename(cache)
72
+ except Exception:
73
+ shutil.rmtree(tmp, ignore_errors=True)
74
+ raise
75
+
76
+ return cache
77
+
78
+ @staticmethod
79
+ def _bolo_install_args() -> list[str]:
80
+ """Args to pass to `uv pip install` when seeding bolo into a model venv."""
81
+ project_root = Path(__file__).resolve().parent.parent.parent
82
+ if (project_root / "pyproject.toml").exists():
83
+ return ["-e", str(project_root)]
84
+ return ["bolo"]
85
+
86
+ def query_a_repo(self, repo_id: str):
87
+ if repo_id not in self.supported_models:
88
+ print(f"bolo does not support {repo_id}")
89
+ return False
90
+
91
+ return True
92
+
93
+ def create_venv(self, repo_id: str, venv_path: str | Path | None = None):
94
+ repo_name = "__SEP__".join(repo_id.split('/'))
95
+
96
+ if not venv_path:
97
+ venv_path = self.db_dir / "tmp_venv" / repo_name
98
+ print(f"you do not specify the venv path for {repo_id}, we create by default at {venv_path}")
99
+
100
+ if venv_path.exists():
101
+ print(f"ven for {repo_id} has been already existed")
102
+ return venv_path / "bin" / "python", True
103
+
104
+ os.makedirs(venv_path, exist_ok=True)
105
+
106
+ req_txt = self.db_dir / repo_name / "requirements.txt"
107
+
108
+ cmd1 = ['uv', 'venv', str(venv_path), '--python', '3.13']
109
+ try:
110
+ subprocess.run(cmd1, check=True, text=True, capture_output=True)
111
+ except Exception as e:
112
+ print(f"failed to create uv venv for {repo_id}, due to the following error : ")
113
+ print(e.stderr)
114
+ return "", False
115
+
116
+ python_bin = venv_path / "bin" / "python"
117
+ cmd2 = ['uv', 'pip', 'install', '-r', str(req_txt), '--python', str(python_bin), '--extra-index-url', self._torch_cu128_url]
118
+ try:
119
+ subprocess.run(cmd2, check=True, text=True, capture_output=True)
120
+ except subprocess.CalledProcessError as e:
121
+ print(f"failed to install requirements.txt for {repo_id}, due to the following error : ")
122
+ print(e.stderr)
123
+ return "", False
124
+
125
+ cmd3 = ['uv', 'pip', 'install', *self._bolo_install_args(), '--python', str(python_bin)]
126
+ try:
127
+ subprocess.run(cmd3, check=True, text=True, capture_output=True)
128
+ except subprocess.CalledProcessError as e:
129
+ print(f"failed to install bolo into the venv for {repo_id}, due to the following error : ")
130
+ print(e.stderr)
131
+ return "", False
132
+
133
+ print(f"installed requirements.txt and bolo in a venv : {python_bin}!")
134
+ return python_bin, True
135
+
136
+ def remove_venv(self, repo_id: str, venv_path: str | Path | None = None):
137
+ repo_name = "__SEP__".join(repo_id.split('/'))
138
+ if not venv_path:
139
+ venv_path = self.db_dir / "tmp_venv" / repo_name
140
+ shutil.rmtree(venv_path, ignore_errors=True)
141
+
142
+ def render_template(self, repo_id: str, params: dict):
143
+ rid_name = "__SEP__".join(repo_id.split('/'))
144
+ template_path = self.db_dir / rid_name / "template.j2"
145
+
146
+ params["repo_id"] = repo_id
147
+
148
+ with open(template_path, 'r', encoding="utf-8") as f:
149
+ template = f.read().strip()
150
+
151
+ try:
152
+ rendered_script = Environment(undefined=StrictUndefined).from_string(template).render(**params)
153
+ except Exception as e:
154
+ print(f"failed to render template for {repo_id}m due to the following error : ")
155
+ print(e)
156
+ return "", False
157
+
158
+ return rendered_script, True
159
+
160
+ def list_params(self, repo_id: str):
161
+ rid_name = "__SEP__".join(repo_id.split('/'))
162
+ template_path = self.db_dir / rid_name / "template.j2"
163
+
164
+ with open(template_path, 'r', encoding="utf-8") as f:
165
+ template = f.read().strip()
166
+
167
+ set_pattern = re.compile(r'^\s*\{\%\s*set\s+([a-zA-Z_]\w*)\s*=\s*(.*?)\s*\%\}\s*$')
168
+ lines = template.splitlines()
169
+ params = []
170
+ started = False
171
+
172
+ for line in lines:
173
+ matched = set_pattern.match(line)
174
+ if matched:
175
+ started = True
176
+ name = matched.group(1)
177
+ raw_default = matched.group(2).strip()
178
+
179
+ lowered = raw_default.lower()
180
+ if lowered == "true":
181
+ default = True
182
+ elif lowered == "false":
183
+ default = False
184
+ elif lowered in {"none", "null"}:
185
+ default = None
186
+ else:
187
+ try:
188
+ default = ast.literal_eval(raw_default)
189
+ except Exception:
190
+ default = raw_default
191
+
192
+ if name == "repo_id" or name == "output_dir":
193
+ continue
194
+
195
+ params.append(
196
+ {
197
+ "name": name,
198
+ "default": default,
199
+ "type": type(default).__name__,
200
+ "raw_default": raw_default,
201
+ }
202
+ )
203
+ continue
204
+
205
+ if started:
206
+ break
207
+
208
+ if not any(p["name"] == "device" for p in params):
209
+ params.insert(0, {
210
+ "name": "device",
211
+ "default": "<required>",
212
+ "type": "str",
213
+ "raw_default": "<required>",
214
+ })
215
+
216
+ result = {
217
+ "num_params": len(params),
218
+ "params": params,
219
+ }
220
+
221
+ def _to_display(v):
222
+ if isinstance(v, (dict, list)):
223
+ return json.dumps(v, ensure_ascii=False)
224
+ return str(v)
225
+
226
+ headers = ["name", "type", "default", "raw_default"]
227
+ rows = [
228
+ [p["name"], p["type"], p["default"], p["raw_default"]]
229
+ for p in params
230
+ ]
231
+ widths = [len(h) for h in headers]
232
+ for row in rows:
233
+ for i, cell in enumerate(row):
234
+ widths[i] = max(widths[i], len(_to_display(cell)))
235
+
236
+ print(f"repo_id: {_to_display(repo_id)}")
237
+ print(f"num_params: {_to_display(len(params))}")
238
+ if not rows:
239
+ print("(no top-level Jinja set variables found)")
240
+ return result
241
+
242
+ sep = " | "
243
+ header_line = sep.join(headers[i].ljust(widths[i]) for i in range(len(headers)))
244
+ divider = "-+-".join("-" * widths[i] for i in range(len(headers)))
245
+ print(header_line)
246
+ print(divider)
247
+ for row in rows:
248
+ print(sep.join(_to_display(row[i]).ljust(widths[i]) for i in range(len(headers))))
249
+
250
+ return result
251
+
252
+ def run_inference(self, code: str):
253
+ try:
254
+ ns = {"__name__": "__api__"}
255
+ exec(code, ns, ns)
256
+ if "RESULT" not in ns:
257
+ raise RuntimeError("RESULT not defined")
258
+ return ns["RESULT"]
259
+ except Exception:
260
+ raise
261
+
262
+
263
+ def list_params(repo_id: str):
264
+ client = InferClient()
265
+
266
+ is_exist = client.query_a_repo(repo_id)
267
+ if not is_exist:
268
+ raise ValueError(f"bolo does not support {repo_id}")
269
+
270
+ _ = client.list_params(repo_id)
271
+
272
+
273
+ def create_a_venv(repo_id: str, venv_path: str | Path | None = None):
274
+ client = InferClient()
275
+
276
+ is_exist = client.query_a_repo(repo_id)
277
+ if not is_exist:
278
+ raise ValueError(f"bolo does not support {repo_id}")
279
+
280
+ if venv_path is not None and os.path.exists(venv_path):
281
+ print(f"already created venv at : {venv_path}")
282
+ return venv_path
283
+
284
+ venv_path, staus = client.create_venv(repo_id, venv_path)
285
+
286
+ if not staus:
287
+ raise RuntimeError(f"failed to create venv for {repo_id}")
288
+
289
+ return venv_path
290
+
291
+
292
+ def pipeline(repo_id: str, **kwargs):
293
+ client = InferClient()
294
+
295
+ is_exist = client.query_a_repo(repo_id)
296
+ if not is_exist:
297
+ raise ValueError(f"bolo does not support {repo_id}")
298
+
299
+ _ = client.list_params(repo_id)
300
+
301
+ code, is_success = client.render_template(repo_id, kwargs)
302
+ if not is_success:
303
+ raise RuntimeError(f"failed to render template for {repo_id}")
304
+
305
+ return client.run_inference(code)
306
+
307
+
308
+ def remove_venv(repo_id: str):
309
+ client = InferClient()
310
+ client.remove_venv(repo_id)
311
+
312
+
313
+ def fetch_templates() -> Path:
314
+ """Pre-populate the bolo templates cache (downloads from the GitHub release if needed)."""
315
+ path = InferClient._ensure_templates()
316
+ print(f"templates available at : {path}")
317
+ return path
bolo/cli.py ADDED
@@ -0,0 +1,78 @@
1
+ import argparse
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Sequence
6
+
7
+ from .api import InferClient, create_a_venv, pipeline, remove_venv, fetch_templates
8
+
9
+
10
+ def build_parser() -> argparse.ArgumentParser:
11
+ parser = argparse.ArgumentParser(prog="bolopipe", description="Inspect BoloPipe registry records")
12
+ sub = parser.add_subparsers(dest="command", required=True)
13
+
14
+ create_venv_parser = sub.add_parser("create-venv", help="Create a venv for a model")
15
+ create_venv_parser.add_argument("repo_id", help="HuggingFace repository id")
16
+ create_venv_parser.add_argument("--venv-path", default=None, help="Path to create the venv")
17
+
18
+ run_parser = sub.add_parser("run", help="Run inference for a model")
19
+ run_parser.add_argument("repo_id", help="HuggingFace repository id")
20
+ run_parser.add_argument("params", nargs="*", metavar="KEY=VALUE", help="Template parameters")
21
+
22
+ remove_venv_parser = sub.add_parser("remove-venv", help="Remove the venv for a model")
23
+ remove_venv_parser.add_argument("repo_id", help="HuggingFace repository id")
24
+
25
+ sub.add_parser("fetch-templates", help="Download the bolo templates cache for this version")
26
+
27
+ return parser
28
+
29
+
30
+ def main(argv: Sequence[str] | None = None) -> int:
31
+ parser = build_parser()
32
+ args = parser.parse_args(argv)
33
+
34
+ if args.command == "create-venv":
35
+ create_a_venv(args.repo_id, args.venv_path)
36
+ return 0
37
+
38
+ if args.command == "run":
39
+ client = InferClient()
40
+ repo_name = "__SEP__".join(args.repo_id.split("/"))
41
+ venv_dir = client.db_dir / "tmp_venv" / repo_name
42
+ venv_python = venv_dir / "bin" / "python"
43
+ venv_bolo = venv_dir / "bin" / "bolo"
44
+
45
+ if not venv_python.exists():
46
+ parser.error(
47
+ f"no venv found at {venv_dir}; "
48
+ f"run `bolo create-venv {args.repo_id}` first"
49
+ )
50
+
51
+ if Path(sys.executable).resolve() != venv_python.resolve():
52
+ return subprocess.call([str(venv_bolo), "run", args.repo_id, *args.params])
53
+
54
+ kwargs = {}
55
+ for item in args.params:
56
+ if "=" not in item:
57
+ parser.error(f"invalid parameter {item!r}, expected KEY=VALUE")
58
+ k, _, v = item.partition("=")
59
+ kwargs[k] = v
60
+ result = pipeline(args.repo_id, **kwargs)
61
+ if result is not None:
62
+ print(result)
63
+ return 0
64
+
65
+ if args.command == "remove-venv":
66
+ remove_venv(args.repo_id)
67
+ return 0
68
+
69
+ if args.command == "fetch-templates":
70
+ fetch_templates()
71
+ return 0
72
+
73
+ parser.error(f"Unknown command: {args.command}")
74
+ return 2
75
+
76
+
77
+ if __name__ == "__main__": # pragma: no cover
78
+ raise SystemExit(main())
bolo/py.typed ADDED
File without changes
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: pybolo
3
+ Version: 0.1.0
4
+ Summary: A lightweight library of verified inference pipelines for HuggingFace models.
5
+ Author: Illinois CreateLab
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/illinoisdata/Bolo
8
+ Project-URL: Repository, https://github.com/illinoisdata/Bolo
9
+ Project-URL: Issues, https://github.com/illinoisdata/Bolo/issues
10
+ Keywords: huggingface,inference,pipeline,verified,curated,model-registry,llm-agents
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.13
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: jinja2>=3.0
22
+ Provides-Extra: hf
23
+ Requires-Dist: transformers>=4.40; extra == "hf"
24
+ Requires-Dist: torch>=2.0; extra == "hf"
25
+ Provides-Extra: dev
26
+ Requires-Dist: build>=1.2; extra == "dev"
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Requires-Dist: twine>=5.0; extra == "dev"
29
+ Requires-Dist: ruff>=0.6; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ <p align="center">
33
+ <a href="https://github.com/illinoisdata/bolo/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/illinoisdata/bolo.svg?color=blue"></a>
34
+ <a href="https://github.com/illinoisdata/bolo-templates/releases"><img alt="bolo-templates release" src="https://img.shields.io/github/v/release/illinoisdata/bolo-templates?include_prereleases&label=templates"></a>
35
+ <a href="https://pypi.org/project/pybolo"><img alt="PyPI" src="https://img.shields.io/pypi/v/pybolo.svg"></a>
36
+ </p>
37
+
38
+ <p align="center">
39
+ <img src="assets/logo.png" alt="bolo logo" width="180">
40
+ </p>
41
+
42
+ <h1 align="center">Bolo: Curated, Verified, and Ready-to-run Inference Pipelines for HuggingFace Models</h1>
43
+
44
+ **Bolo** is a lightweight Python library that gives you curated, verified, and ready-to-run inference pipelines for HuggingFace models with ZERO efforts.
45
+
46
+ 📦 **Curated templates**: every supported model ships with a tested Jinja2 inference template maintained by the Illinois CreateLab team.
47
+
48
+ 🔒 **Isolated venvs**: each model runs inside its own `uv`-managed virtual environment, so dependency conflicts between models are impossible.
49
+
50
+ ⚡ **One-call API**: `bolo.pipeline(repo_id, device="cuda", ...)` conducts inference in one API call, simpler than HuggingFace two-stages `pipeline` API.
51
+
52
+ 🖥️ **CLI included**: a `bolo` command lets you manage venvs and run inference directly from the shell.
53
+
54
+ 🌐 **Auto-fetching templates**: template bundles are downloaded on first use from the [bolo-templates](https://github.com/illinoisdata/bolo-templates) GitHub release and cached locally — no manual setup needed.
55
+
56
+ ## Quick demo
57
+
58
+ Install bolo:
59
+
60
+ ```bash
61
+ pip install pybolo
62
+ ```
63
+
64
+ Run inference with two lines of Python:
65
+
66
+ ```python
67
+ import bolo
68
+
69
+ result = bolo.pipeline("<REPO_ID>", device="cuda:0")
70
+ print(result)
71
+ ```
72
+
73
+ Before running inference you can inspect what parameters the template accepts:
74
+
75
+ ```python
76
+ bolo.list_params("<REPO_ID>")
77
+ ```
78
+
79
+ And manage venvs explicitly:
80
+
81
+ ```python
82
+ python_bin = bolo.create_a_venv("<REPO_ID>")
83
+ # ... activate the created venv and do inference ...
84
+ bolo.remove_venv("<REPO_ID>")
85
+ ```
86
+
87
+ ## CLI
88
+
89
+ The `bolo` command mirrors the Python API from your shell.
90
+
91
+ **Create an isolated venv for a model:**
92
+ ```bash
93
+ bolo create-venv <REPO_ID>
94
+ bolo create-venv <REPO_ID> --venv-path /path/to/venv
95
+ # then activate the venv
96
+ ```
97
+
98
+ **Run inference:**
99
+ ```bash
100
+ bolo run <REPO_ID> device=cuda:0
101
+ ```
102
+
103
+ **Pre-download the templates cache (optional, useful on air-gapped machines):**
104
+ ```bash
105
+ bolo fetch-templates
106
+ ```
107
+
108
+ ## How does bolo work?
109
+
110
+ bolo separates *what to run* (the Jinja2 template) from *where to run it* (the model's isolated venv).
111
+
112
+ ```mermaid
113
+ flowchart LR
114
+ A[User calls bolo.pipeline] --> B[Fetch / load templates]
115
+ B --> C[Render Jinja2 template\nwith user params]
116
+ C --> D[Execute rendered script\ninside model venv]
117
+ D --> E[Return RESULT]
118
+ ```
119
+
120
+ 1. **Templates** — stored in the [bolo-templates](https://github.com/illinoisdata/bolo-templates) release bundle. Each model folder contains a `template.j2` (the inference script template) and a `requirements.txt` (the model's exact dependencies). Templates are downloaded once and cached at `~/.cache/bolo/templates/`.
121
+
122
+ 2. **Venvs** — created with `uv venv` + `uv pip install -r requirements.txt`. Every model gets its own venv so you can safely use models with conflicting PyTorch or CUDA versions side-by-side.
123
+
124
+ 3. **Rendering** — template parameters are collected from the leading `{% set key = default %}` blocks in each `template.j2`. `bolo list_params` shows you every knob with its type and default value.
125
+
126
+ 4. **Execution** — the rendered script is executed and its `RESULT` variable is returned to the caller.
127
+
128
+ ## Install from source
129
+
130
+ ```bash
131
+ git clone https://github.com/illinoisdata/Bolo.git
132
+ cd Bolo
133
+ pip install -e .
134
+ ```
135
+
136
+ ## Custom templates directory
137
+
138
+ Set `BOLO_TEMPLATES_DIR` to point bolo at your own templates folder:
139
+
140
+ ```bash
141
+ export BOLO_TEMPLATES_DIR=/path/to/my/templates
142
+ ```
143
+
144
+ ## Platforms
145
+ `bolo` is developed and tested on Red Hat Enterprise Linux, with CUDA version 12.8. So all dependencies (`torch`-related) assume the `cu128`.
146
+
147
+ ## Contributing
148
+
149
+ Contributions are welcome! Please open an issue or pull request on [GitHub](https://github.com/illinoisdata/bolo/issues).
150
+
151
+ ## License
152
+
153
+ MIT — see [LICENSE](LICENSE) for details.
154
+
155
+ ## Acknowledgement
156
+ - Thanks Jojo and her sister for designing the mascot.
@@ -0,0 +1,11 @@
1
+ bolo/__init__.py,sha256=aPfnAYYRXnYiV-yf9cAPmHDz3yxH10jzlVm9ppZ8F3w,336
2
+ bolo/_version.py,sha256=VDUvW59hnYcAuAz1kbw-UXv87xOenI0dQSaQL-6fRg0,54
3
+ bolo/api.py,sha256=mVOHlSaydeb3YjdbVlKVqPjtLmvx9ZbpA9O5-9dqay0,10826
4
+ bolo/cli.py,sha256=ZQXpOSR4vgHv8g9bGdbNJFYAiftnyk5fJ76kUiyEWg0,2728
5
+ bolo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ pybolo-0.1.0.dist-info/licenses/LICENSE,sha256=BI8sbF3C8lnlNsDNTMxeW1lMtEZwBfyiGMVIUJj44ho,1075
7
+ pybolo-0.1.0.dist-info/METADATA,sha256=uSfqTKc6zUc6ZDYHE4m1RhNgJREJW-TE4QujE4W3PSI,5658
8
+ pybolo-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ pybolo-0.1.0.dist-info/entry_points.txt,sha256=MMy8Nkni80Qkc9B3gfxKsdAoIbLLQjb2y8swdh6JSzE,39
10
+ pybolo-0.1.0.dist-info/top_level.txt,sha256=XJPzoQx5ssYSgfFMdT-AEW5NgpSX-S_oQFv0kRv7Rwc,5
11
+ pybolo-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bolo = bolo.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Illinois CreateLab
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 @@
1
+ bolo