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.
- modeldownloaderutil/__init__.py +10 -0
- modeldownloaderutil/_download.py +46 -0
- modeldownloaderutil/cache.py +83 -0
- modeldownloaderutil/downloader.py +32 -0
- modeldownloaderutil/env.py +33 -0
- modeldownloaderutil/providers/__init__.py +5 -0
- modeldownloaderutil/providers/base.py +17 -0
- modeldownloaderutil/providers/gcs.py +36 -0
- modeldownloaderutil/providers/git_lfs.py +45 -0
- modeldownloaderutil/providers/http.py +21 -0
- modeldownloaderutil/providers/local.py +21 -0
- modeldownloaderutil/providers/s3.py +50 -0
- modeldownloaderutil-1.0.0.dist-info/METADATA +109 -0
- modeldownloaderutil-1.0.0.dist-info/RECORD +15 -0
- modeldownloaderutil-1.0.0.dist-info/WHEEL +4 -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,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,,
|