modeldownloaderutil 1.0.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.
@@ -0,0 +1,10 @@
1
+ """
2
+ -- Created by: Ashok Kumar Pant
3
+ -- Email: asokpant@gmail.com
4
+ -- Created on: 04/06/2026
5
+ """
6
+ from .cache import cache_dir
7
+ from .downloader import download_model
8
+
9
+ __all__ = ["download_model", "cache_dir"]
10
+ __version__ = "2.0.0"
@@ -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,5 @@
1
+ """
2
+ -- Created by: Ashok Kumar Pant
3
+ -- Email: asokpant@gmail.com
4
+ -- Created on: 04/06/2026
5
+ """
@@ -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
@@ -0,0 +1,50 @@
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
+ from pathlib import Path
10
+ from urllib.parse import urlparse
11
+
12
+ import boto3
13
+ from botocore.config import Config
14
+
15
+ from .._download import download_s3_object, download_with_cache
16
+ from ..cache import cache_path, normalize_s3_uri
17
+ from .base import ModelProvider
18
+
19
+ _S3_CONFIG = Config(signature_version="s3v4", s3={"addressing_style": "path"})
20
+ _SCHEME_ENV = {"minio://": "MINIO_ENDPOINT", "rustfs://": "RUSTFS_ENDPOINT"}
21
+
22
+
23
+ class S3Provider(ModelProvider):
24
+ def can_handle(self, source: str) -> bool:
25
+ return source.startswith(("s3://", "minio://", "rustfs://"))
26
+
27
+ def download(self, source: str, *, force: bool = False) -> Path:
28
+ scheme = source.split("://", 1)[0] + "://"
29
+ normalized, endpoint = normalize_s3_uri(source)
30
+ if scheme in _SCHEME_ENV and not endpoint:
31
+ env_name = _SCHEME_ENV[scheme]
32
+ raise ValueError(f"{env_name} is not set (check .env)")
33
+ if not os.environ.get("MODEL_ACCESS_KEY") or not os.environ.get("MODEL_SECRET_KEY"):
34
+ raise ValueError("MODEL_ACCESS_KEY and MODEL_SECRET_KEY are not set (check .env)")
35
+
36
+ destination = cache_path(normalized)
37
+ parsed = urlparse(normalized)
38
+ bucket = parsed.netloc
39
+ key = parsed.path.lstrip("/")
40
+ if not bucket or not key:
41
+ raise ValueError(f"Invalid S3 URI: {source}")
42
+
43
+ client = boto3.client(
44
+ "s3",
45
+ endpoint_url=endpoint,
46
+ aws_access_key_id=os.environ.get("MODEL_ACCESS_KEY"),
47
+ aws_secret_access_key=os.environ.get("MODEL_SECRET_KEY"),
48
+ config=_S3_CONFIG,
49
+ )
50
+ return download_with_cache(destination, lambda path: download_s3_object(client, bucket, key, path), force=force)
@@ -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,15 @@
1
+ modeldownloaderutil/__init__.py,sha256=RNdHa4RDSjtq_U7OZ8rInANM4saCO6VYHDe1UmsLyIA,228
2
+ modeldownloaderutil/_download.py,sha256=D8Zbn9xujUkWsXZoi388O6Fet7bkwAtb-TK6wg1D31E,1885
3
+ modeldownloaderutil/cache.py,sha256=m9x1gduXzDwXBaNpiivL1DM5SLWfSGyYsYYeGqVmvjo,2515
4
+ modeldownloaderutil/downloader.py,sha256=zcdK6sSDdfriwb1MVIZ44v0WShvv_cmt7SQYtTJssHI,860
5
+ modeldownloaderutil/env.py,sha256=Q6LV2Ym-CE7VKp6XYkS4D2gQzpNxkOL0Tw5FQN_Yflo,758
6
+ modeldownloaderutil/providers/__init__.py,sha256=kV0qGZurx0FedRy1xoYoNqzgi8PIkM8jjL0sFuoALuU,95
7
+ modeldownloaderutil/providers/base.py,sha256=KbhCD1NlyyKnbZnTT_k0mkZBsBDMOAiHOzhGU1ZOmkA,385
8
+ modeldownloaderutil/providers/gcs.py,sha256=mho8ynJNXb6_jo5ikCJBo4VpnMqZtQIJLSEL0Caya6Y,1154
9
+ modeldownloaderutil/providers/git_lfs.py,sha256=0_OCZSCTyBvwRCCBq1lg1nRBi8JQujfmD7d6fPzrkf8,1644
10
+ modeldownloaderutil/providers/http.py,sha256=pJCeyJZewKydcDtldkhkuNCeimShqqYSYTQu0IAtBkI,635
11
+ modeldownloaderutil/providers/local.py,sha256=Z53RE-0v5fcwRKawKmng1gHgxzx6oW8irMFT7QcMvVM,539
12
+ modeldownloaderutil/providers/s3.py,sha256=7IaIRcOuFVYThsgxf9GjSMypoHntEDFWKq5UeH-W2Qw,1858
13
+ modeldownloaderutil-1.0.0.dist-info/METADATA,sha256=YgpMe8D2p9XEk3-nEcQgMrvD3E7nUr3_vj1PCSpwyBQ,2934
14
+ modeldownloaderutil-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
15
+ modeldownloaderutil-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any