modeldownloaderutil 1.0.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.
- modeldownloaderutil-1.0.0/.env.example +5 -0
- modeldownloaderutil-1.0.0/.github/workflows/workflow.yml +48 -0
- modeldownloaderutil-1.0.0/.gitignore +12 -0
- modeldownloaderutil-1.0.0/Makefile +26 -0
- modeldownloaderutil-1.0.0/PKG-INFO +109 -0
- modeldownloaderutil-1.0.0/README.md +83 -0
- modeldownloaderutil-1.0.0/examples/__init__.py +5 -0
- modeldownloaderutil-1.0.0/examples/downloader_example.py +9 -0
- modeldownloaderutil-1.0.0/modeldownloaderutil/__init__.py +10 -0
- modeldownloaderutil-1.0.0/modeldownloaderutil/_download.py +46 -0
- modeldownloaderutil-1.0.0/modeldownloaderutil/cache.py +83 -0
- modeldownloaderutil-1.0.0/modeldownloaderutil/downloader.py +32 -0
- modeldownloaderutil-1.0.0/modeldownloaderutil/env.py +33 -0
- modeldownloaderutil-1.0.0/modeldownloaderutil/providers/__init__.py +5 -0
- modeldownloaderutil-1.0.0/modeldownloaderutil/providers/base.py +17 -0
- modeldownloaderutil-1.0.0/modeldownloaderutil/providers/gcs.py +36 -0
- modeldownloaderutil-1.0.0/modeldownloaderutil/providers/git_lfs.py +45 -0
- modeldownloaderutil-1.0.0/modeldownloaderutil/providers/http.py +21 -0
- modeldownloaderutil-1.0.0/modeldownloaderutil/providers/local.py +21 -0
- modeldownloaderutil-1.0.0/modeldownloaderutil/providers/s3.py +50 -0
- modeldownloaderutil-1.0.0/pyproject.toml +58 -0
- modeldownloaderutil-1.0.0/tests/__init__.py +5 -0
- modeldownloaderutil-1.0.0/tests/test_cache.py +38 -0
- modeldownloaderutil-1.0.0/uv.lock +669 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v5
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
|
|
24
|
+
- name: Set up Python
|
|
25
|
+
run: uv python install ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: uv sync --group dev
|
|
29
|
+
|
|
30
|
+
- name: Ruff
|
|
31
|
+
run: uv run ruff check modeldownloaderutil tests
|
|
32
|
+
|
|
33
|
+
- name: Pytest
|
|
34
|
+
run: uv run pytest
|
|
35
|
+
|
|
36
|
+
build:
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
needs: test
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
|
|
42
|
+
- name: Install uv
|
|
43
|
+
uses: astral-sh/setup-uv@v5
|
|
44
|
+
with:
|
|
45
|
+
enable-cache: true
|
|
46
|
+
|
|
47
|
+
- name: Build package
|
|
48
|
+
run: uv build
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.PHONY: install upgrade test clean build publish publish-test
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
uv sync --group dev
|
|
5
|
+
|
|
6
|
+
upgrade:
|
|
7
|
+
uv sync --upgrade
|
|
8
|
+
|
|
9
|
+
test:
|
|
10
|
+
uv run pytest
|
|
11
|
+
|
|
12
|
+
clean:
|
|
13
|
+
rm -rf .venv dist build __pycache__ *.egg-info
|
|
14
|
+
find . -type d -name __pycache__ -exec rm -rf {} +
|
|
15
|
+
find . -type f -name "*.pyc" -delete
|
|
16
|
+
|
|
17
|
+
build:
|
|
18
|
+
uv build
|
|
19
|
+
|
|
20
|
+
publish: build
|
|
21
|
+
@test -n "$$UV_PUBLISH_TOKEN" || (echo "Set UV_PUBLISH_TOKEN (PyPI API token)" && exit 1)
|
|
22
|
+
uv publish
|
|
23
|
+
|
|
24
|
+
publish-test: build
|
|
25
|
+
@test -n "$$UV_PUBLISH_TOKEN" || (echo "Set UV_PUBLISH_TOKEN (TestPyPI API token)" && exit 1)
|
|
26
|
+
uv publish --publish-url https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modeldownloaderutil
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Download model files from local paths, HTTP, S3-compatible storage, GCS, and Git LFS.
|
|
5
|
+
Author-email: Ashok Pant <ashok@treeleaf.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: download,gcs,git-lfs,minio,model,rustfs,s3
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: boto3>=1.43.22
|
|
19
|
+
Requires-Dist: google-cloud-storage>=3.11.0
|
|
20
|
+
Requires-Dist: platformdirs>=4.3.8
|
|
21
|
+
Requires-Dist: python-dotenv>=1.1.0
|
|
22
|
+
Requires-Dist: requests>=2.34.2
|
|
23
|
+
Requires-Dist: tenacity>=9.1.4
|
|
24
|
+
Requires-Dist: tqdm>=4.67.3
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# model-downloader-util
|
|
28
|
+
|
|
29
|
+
Download or resolve model files from local paths, HTTP(S), S3-compatible storage (S3, MinIO, RustFS), Google Cloud Storage, and Git LFS.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install modeldownloaderutil
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
From source:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv sync
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from modeldownloaderutil import download_model, cache_dir
|
|
47
|
+
|
|
48
|
+
path = download_model("s3://my-bucket/models/weights.onnx")
|
|
49
|
+
path = download_model("https://example.com/model.onnx", force_download=True)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Supported sources
|
|
53
|
+
|
|
54
|
+
| Scheme | Example |
|
|
55
|
+
|--------|---------|
|
|
56
|
+
| Local | `/path/to/model.onnx`, `~/models/x.onnx` |
|
|
57
|
+
| HTTP(S) | `https://host/path/model.onnx` |
|
|
58
|
+
| S3 | `s3://bucket/key` |
|
|
59
|
+
| MinIO | `minio://bucket/key` (`MINIO_ENDPOINT`) |
|
|
60
|
+
| RustFS | `rustfs://bucket/key` (`RUSTFS_ENDPOINT`) |
|
|
61
|
+
| GCS | `gs://bucket/object` |
|
|
62
|
+
| Git LFS | `git+https://github.com/org/repo.git#path/in/repo.onnx` |
|
|
63
|
+
|
|
64
|
+
## Cache
|
|
65
|
+
|
|
66
|
+
Default: `~/.cache/model-downloader` (override with `MODEL_CACHE_DIR`).
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
~/.cache/model-downloader/
|
|
70
|
+
s3/<bucket>/<key>
|
|
71
|
+
gs/<bucket>/<object>
|
|
72
|
+
url/<host>/<path>
|
|
73
|
+
git/<repo_slug>/<file_path>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Environment
|
|
77
|
+
|
|
78
|
+
Copy `.env.example` to `.env` in the project root (loaded automatically on `download_model`).
|
|
79
|
+
|
|
80
|
+
| Variable | Purpose |
|
|
81
|
+
|----------|---------|
|
|
82
|
+
| `MODEL_CACHE_DIR` | Download cache root (`cache_dir()`) |
|
|
83
|
+
| `RUSTFS_ENDPOINT` | Required for `rustfs://` URLs |
|
|
84
|
+
| `MINIO_ENDPOINT` | Required for `minio://` URLs |
|
|
85
|
+
| `MODEL_ACCESS_KEY` / `MODEL_SECRET_KEY` | S3-compatible credentials |
|
|
86
|
+
|
|
87
|
+
## Development
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
make test
|
|
91
|
+
make build
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Publish to PyPI
|
|
95
|
+
|
|
96
|
+
Create an API token at [pypi.org](https://pypi.org/manage/account/token/) (scope: project `modeldownloaderutil` or entire account).
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
export UV_PUBLISH_TOKEN=pypi-xxxxxxxx
|
|
100
|
+
make publish
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Dry run on TestPyPI first:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
export UV_PUBLISH_TOKEN=pypi-xxxxxxxx
|
|
107
|
+
make publish-test
|
|
108
|
+
pip install --index-url https://test.pypi.org/simple/ modeldownloaderutil
|
|
109
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# model-downloader-util
|
|
2
|
+
|
|
3
|
+
Download or resolve model files from local paths, HTTP(S), S3-compatible storage (S3, MinIO, RustFS), Google Cloud Storage, and Git LFS.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install modeldownloaderutil
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
From source:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv sync
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from modeldownloaderutil import download_model, cache_dir
|
|
21
|
+
|
|
22
|
+
path = download_model("s3://my-bucket/models/weights.onnx")
|
|
23
|
+
path = download_model("https://example.com/model.onnx", force_download=True)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Supported sources
|
|
27
|
+
|
|
28
|
+
| Scheme | Example |
|
|
29
|
+
|--------|---------|
|
|
30
|
+
| Local | `/path/to/model.onnx`, `~/models/x.onnx` |
|
|
31
|
+
| HTTP(S) | `https://host/path/model.onnx` |
|
|
32
|
+
| S3 | `s3://bucket/key` |
|
|
33
|
+
| MinIO | `minio://bucket/key` (`MINIO_ENDPOINT`) |
|
|
34
|
+
| RustFS | `rustfs://bucket/key` (`RUSTFS_ENDPOINT`) |
|
|
35
|
+
| GCS | `gs://bucket/object` |
|
|
36
|
+
| Git LFS | `git+https://github.com/org/repo.git#path/in/repo.onnx` |
|
|
37
|
+
|
|
38
|
+
## Cache
|
|
39
|
+
|
|
40
|
+
Default: `~/.cache/model-downloader` (override with `MODEL_CACHE_DIR`).
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
~/.cache/model-downloader/
|
|
44
|
+
s3/<bucket>/<key>
|
|
45
|
+
gs/<bucket>/<object>
|
|
46
|
+
url/<host>/<path>
|
|
47
|
+
git/<repo_slug>/<file_path>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Environment
|
|
51
|
+
|
|
52
|
+
Copy `.env.example` to `.env` in the project root (loaded automatically on `download_model`).
|
|
53
|
+
|
|
54
|
+
| Variable | Purpose |
|
|
55
|
+
|----------|---------|
|
|
56
|
+
| `MODEL_CACHE_DIR` | Download cache root (`cache_dir()`) |
|
|
57
|
+
| `RUSTFS_ENDPOINT` | Required for `rustfs://` URLs |
|
|
58
|
+
| `MINIO_ENDPOINT` | Required for `minio://` URLs |
|
|
59
|
+
| `MODEL_ACCESS_KEY` / `MODEL_SECRET_KEY` | S3-compatible credentials |
|
|
60
|
+
|
|
61
|
+
## Development
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
make test
|
|
65
|
+
make build
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Publish to PyPI
|
|
69
|
+
|
|
70
|
+
Create an API token at [pypi.org](https://pypi.org/manage/account/token/) (scope: project `modeldownloaderutil` or entire account).
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
export UV_PUBLISH_TOKEN=pypi-xxxxxxxx
|
|
74
|
+
make publish
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Dry run on TestPyPI first:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export UV_PUBLISH_TOKEN=pypi-xxxxxxxx
|
|
81
|
+
make publish-test
|
|
82
|
+
pip install --index-url https://test.pypi.org/simple/ modeldownloaderutil
|
|
83
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
-- Created by: Ashok Kumar Pant
|
|
3
|
+
-- Email: asokpant@gmail.com
|
|
4
|
+
-- Created on: 04/06/2026
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
from tenacity import retry, stop_after_attempt, wait_exponential
|
|
13
|
+
from tqdm import tqdm
|
|
14
|
+
|
|
15
|
+
from .cache import commit_download, incomplete_path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=8))
|
|
19
|
+
def download_http(url: str, destination: Path) -> Path:
|
|
20
|
+
tmp = incomplete_path(destination)
|
|
21
|
+
tmp.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
with requests.get(url, stream=True, timeout=300) as response:
|
|
23
|
+
response.raise_for_status()
|
|
24
|
+
total = int(response.headers.get("Content-Length", 0)) or None
|
|
25
|
+
with open(tmp, "wb") as handle, tqdm(total=total, unit="B", unit_scale=True, unit_divisor=1024, desc=destination.name, disable=total is None) as bar:
|
|
26
|
+
for chunk in response.iter_content(chunk_size=1024 * 1024):
|
|
27
|
+
if chunk:
|
|
28
|
+
handle.write(chunk)
|
|
29
|
+
bar.update(len(chunk))
|
|
30
|
+
return commit_download(tmp, destination)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def download_s3_object(client, bucket: str, key: str, destination: Path) -> Path:
|
|
34
|
+
tmp = incomplete_path(destination)
|
|
35
|
+
tmp.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
size = client.head_object(Bucket=bucket, Key=key).get("ContentLength")
|
|
37
|
+
with tqdm(total=size, unit="B", unit_scale=True, unit_divisor=1024, desc=destination.name, disable=not size) as bar:
|
|
38
|
+
client.download_file(bucket, key, str(tmp), Callback=lambda n: bar.update(n))
|
|
39
|
+
return commit_download(tmp, destination)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def download_with_cache(destination: Path, fetch: Callable[[Path], Path], *, force: bool = False) -> Path:
|
|
43
|
+
if destination.exists() and not force:
|
|
44
|
+
return destination.resolve()
|
|
45
|
+
fetch(destination)
|
|
46
|
+
return destination.resolve()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
-- Created by: Ashok Kumar Pant
|
|
3
|
+
-- Email: asokpant@gmail.com
|
|
4
|
+
-- Created on: 04/06/2026
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
from platformdirs import user_cache_dir
|
|
14
|
+
|
|
15
|
+
APP_NAME = "model-downloader"
|
|
16
|
+
APP_AUTHOR = "treeleaf"
|
|
17
|
+
_SCHEME_ALIASES = {"minio": "s3", "rustfs": "s3"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def cache_dir() -> Path:
|
|
21
|
+
if cache := os.environ.get("MODEL_CACHE_DIR"):
|
|
22
|
+
return Path(cache).expanduser()
|
|
23
|
+
return Path(user_cache_dir(APP_NAME, appauthor=APP_AUTHOR))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def cache_path(source: str) -> Path:
|
|
27
|
+
parsed = urlparse(source)
|
|
28
|
+
scheme = _SCHEME_ALIASES.get(parsed.scheme, parsed.scheme)
|
|
29
|
+
root = cache_dir()
|
|
30
|
+
|
|
31
|
+
if scheme in ("s3", "gs"):
|
|
32
|
+
key = parsed.path.lstrip("/")
|
|
33
|
+
if not parsed.netloc or not key:
|
|
34
|
+
raise ValueError(f"Invalid object URI: {source}")
|
|
35
|
+
return root / scheme / parsed.netloc / key
|
|
36
|
+
|
|
37
|
+
if scheme in ("http", "https"):
|
|
38
|
+
path = parsed.path.lstrip("/") or "download"
|
|
39
|
+
return root / "url" / _safe_segment(parsed.netloc) / path
|
|
40
|
+
|
|
41
|
+
if scheme == "git":
|
|
42
|
+
file_path = parsed.fragment
|
|
43
|
+
if not file_path:
|
|
44
|
+
raise ValueError(f"Invalid git URI (missing #<path>): {source}")
|
|
45
|
+
repo_url = f"{parsed.netloc}{parsed.path}" if parsed.netloc else parsed.path.lstrip("/")
|
|
46
|
+
if not repo_url:
|
|
47
|
+
raise ValueError(f"Invalid git URI: {source}")
|
|
48
|
+
return git_file_path(repo_url, file_path)
|
|
49
|
+
|
|
50
|
+
raise ValueError(f"Cannot derive cache path for: {source}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def git_repo_dir(repo_url: str) -> Path:
|
|
54
|
+
return cache_dir() / "git" / _safe_segment(repo_url)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def git_file_path(repo_url: str, file_path: str) -> Path:
|
|
58
|
+
return git_repo_dir(repo_url) / file_path
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def incomplete_path(path: Path) -> Path:
|
|
62
|
+
return path.with_suffix(path.suffix + ".incomplete")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def commit_download(tmp: Path, destination: Path) -> Path:
|
|
66
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
tmp.replace(destination)
|
|
68
|
+
return destination
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _safe_segment(value: str) -> str:
|
|
72
|
+
return re.sub(r"[^a-zA-Z0-9._-]+", "_", value)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def normalize_s3_uri(source: str) -> tuple[str, str | None]:
|
|
76
|
+
endpoint: str | None = None
|
|
77
|
+
if source.startswith("minio://"):
|
|
78
|
+
endpoint = os.environ.get("MINIO_ENDPOINT")
|
|
79
|
+
source = "s3://" + source[len("minio://") :]
|
|
80
|
+
elif source.startswith("rustfs://"):
|
|
81
|
+
endpoint = os.environ.get("RUSTFS_ENDPOINT")
|
|
82
|
+
source = "s3://" + source[len("rustfs://") :]
|
|
83
|
+
return source, endpoint
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
-- Created by: Ashok Kumar Pant
|
|
3
|
+
-- Email: asokpant@gmail.com
|
|
4
|
+
-- Created on: 04/06/2026
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .env import load_env
|
|
11
|
+
from .providers.base import ModelProvider
|
|
12
|
+
from .providers.gcs import GCSProvider
|
|
13
|
+
from .providers.git_lfs import GitLFSProvider
|
|
14
|
+
from .providers.http import HttpProvider
|
|
15
|
+
from .providers.local import LocalProvider
|
|
16
|
+
from .providers.s3 import S3Provider
|
|
17
|
+
|
|
18
|
+
_PROVIDERS: tuple[ModelProvider, ...] = (
|
|
19
|
+
LocalProvider(),
|
|
20
|
+
HttpProvider(),
|
|
21
|
+
S3Provider(),
|
|
22
|
+
GCSProvider(),
|
|
23
|
+
GitLFSProvider(),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def download_model(source: str, *, force_download: bool = False) -> Path:
|
|
28
|
+
load_env()
|
|
29
|
+
for provider in _PROVIDERS:
|
|
30
|
+
if provider.can_handle(source):
|
|
31
|
+
return provider.download(source, force=force_download)
|
|
32
|
+
raise ValueError(f"Unsupported source: {source}")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
-- Created by: Ashok Kumar Pant
|
|
3
|
+
-- Email: asokpant@gmail.com
|
|
4
|
+
-- Created on: 04/06/2026
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
_LOADED = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_env() -> None:
|
|
14
|
+
global _LOADED
|
|
15
|
+
if _LOADED:
|
|
16
|
+
return
|
|
17
|
+
from dotenv import load_dotenv
|
|
18
|
+
|
|
19
|
+
for path in _env_candidates():
|
|
20
|
+
load_dotenv(path, override=False)
|
|
21
|
+
_LOADED = True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _env_candidates() -> list[Path]:
|
|
25
|
+
seen: set[Path] = set()
|
|
26
|
+
out: list[Path] = []
|
|
27
|
+
for base in (Path.cwd(), Path(__file__).resolve().parents[1]):
|
|
28
|
+
for parent in (base, *base.parents):
|
|
29
|
+
env = (parent / ".env").resolve()
|
|
30
|
+
if env.is_file() and env not in seen:
|
|
31
|
+
seen.add(env)
|
|
32
|
+
out.append(env)
|
|
33
|
+
return out
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
-- Created by: Ashok Kumar Pant
|
|
3
|
+
-- Email: asokpant@gmail.com
|
|
4
|
+
-- Created on: 04/06/2026
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ModelProvider(ABC):
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def can_handle(self, source: str) -> bool: ...
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def download(self, source: str, *, force: bool = False) -> Path: ...
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
-- Created by: Ashok Kumar Pant
|
|
3
|
+
-- Email: asokpant@gmail.com
|
|
4
|
+
-- Created on: 04/06/2026
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
from google.cloud import storage
|
|
12
|
+
|
|
13
|
+
from .._download import download_with_cache
|
|
14
|
+
from ..cache import cache_path, commit_download, incomplete_path
|
|
15
|
+
from .base import ModelProvider
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GCSProvider(ModelProvider):
|
|
19
|
+
def can_handle(self, source: str) -> bool:
|
|
20
|
+
return source.startswith("gs://")
|
|
21
|
+
|
|
22
|
+
def download(self, source: str, *, force: bool = False) -> Path:
|
|
23
|
+
destination = cache_path(source)
|
|
24
|
+
parsed = urlparse(source)
|
|
25
|
+
bucket = parsed.netloc
|
|
26
|
+
blob_name = parsed.path.lstrip("/")
|
|
27
|
+
if not bucket or not blob_name:
|
|
28
|
+
raise ValueError(f"Invalid GCS URI: {source}")
|
|
29
|
+
|
|
30
|
+
def fetch(path: Path) -> Path:
|
|
31
|
+
tmp = incomplete_path(path)
|
|
32
|
+
tmp.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
storage.Client().bucket(bucket).blob(blob_name).download_to_filename(str(tmp))
|
|
34
|
+
return commit_download(tmp, path)
|
|
35
|
+
|
|
36
|
+
return download_with_cache(destination, fetch, force=force)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
-- Created by: Ashok Kumar Pant
|
|
3
|
+
-- Email: asokpant@gmail.com
|
|
4
|
+
-- Created on: 04/06/2026
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from ..cache import git_file_path, git_repo_dir
|
|
12
|
+
from .base import ModelProvider
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GitLFSProvider(ModelProvider):
|
|
16
|
+
def can_handle(self, source: str) -> bool:
|
|
17
|
+
return source.startswith("git+")
|
|
18
|
+
|
|
19
|
+
def download(self, source: str, *, force: bool = False) -> Path:
|
|
20
|
+
canonical = source[4:]
|
|
21
|
+
repo_url, _, file_path = canonical.partition("#")
|
|
22
|
+
if not repo_url or not file_path:
|
|
23
|
+
raise ValueError(f"Invalid git source (expected git+<url>#<path>): {source}")
|
|
24
|
+
|
|
25
|
+
target = git_file_path(repo_url, file_path)
|
|
26
|
+
repo_dir = git_repo_dir(repo_url)
|
|
27
|
+
if target.exists() and not force:
|
|
28
|
+
return target.resolve()
|
|
29
|
+
|
|
30
|
+
repo_dir.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
if not (repo_dir / ".git").exists():
|
|
32
|
+
self._run(["git", "clone", "--filter=blob:none", "--no-checkout", repo_url, str(repo_dir)])
|
|
33
|
+
self._run(["git", "-C", str(repo_dir), "sparse-checkout", "init", "--no-cone"])
|
|
34
|
+
|
|
35
|
+
self._run(["git", "-C", str(repo_dir), "sparse-checkout", "set", file_path])
|
|
36
|
+
self._run(["git", "-C", str(repo_dir), "checkout", "HEAD"])
|
|
37
|
+
self._run(["git", "-C", str(repo_dir), "lfs", "pull", "--include", file_path])
|
|
38
|
+
|
|
39
|
+
if not target.exists():
|
|
40
|
+
raise FileNotFoundError(f"File not found after git LFS pull: {file_path} in {repo_url}")
|
|
41
|
+
return target.resolve()
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _run(cmd: list[str]) -> None:
|
|
45
|
+
subprocess.run(cmd, check=True)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
-- Created by: Ashok Kumar Pant
|
|
3
|
+
-- Email: asokpant@gmail.com
|
|
4
|
+
-- Created on: 04/06/2026
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .._download import download_http, download_with_cache
|
|
11
|
+
from ..cache import cache_path
|
|
12
|
+
from .base import ModelProvider
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HttpProvider(ModelProvider):
|
|
16
|
+
def can_handle(self, source: str) -> bool:
|
|
17
|
+
return source.startswith(("http://", "https://"))
|
|
18
|
+
|
|
19
|
+
def download(self, source: str, *, force: bool = False) -> Path:
|
|
20
|
+
destination = cache_path(source)
|
|
21
|
+
return download_with_cache(destination, lambda path: download_http(source, path), force=force)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
-- Created by: Ashok Kumar Pant
|
|
3
|
+
-- Email: asokpant@gmail.com
|
|
4
|
+
-- Created on: 04/06/2026
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .base import ModelProvider
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LocalProvider(ModelProvider):
|
|
14
|
+
def can_handle(self, source: str) -> bool:
|
|
15
|
+
return Path(source).expanduser().exists()
|
|
16
|
+
|
|
17
|
+
def download(self, source: str, *, force: bool = False) -> Path:
|
|
18
|
+
path = Path(source).expanduser().resolve()
|
|
19
|
+
if not path.exists():
|
|
20
|
+
raise FileNotFoundError(source)
|
|
21
|
+
return path
|