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.
@@ -0,0 +1,5 @@
1
+ MODEL_CACHE_DIR=~/.cache/model-downloader
2
+ RUSTFS_ENDPOINT=http://localhost:9000
3
+ MINIO_ENDPOINT=https://minio.example.com
4
+ MODEL_ACCESS_KEY=your-access-key
5
+ MODEL_SECRET_KEY=your-secret-key
@@ -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,12 @@
1
+ .venv
2
+ *.pyc
3
+ .idea
4
+ *DS_Store
5
+ __pycache__
6
+ *.log
7
+ *.sqlite3
8
+ *.db
9
+ *.env
10
+ dist/
11
+ build/
12
+ *.egg-info/
@@ -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,5 @@
1
+ """
2
+ -- Created by: Ashok Kumar Pant
3
+ -- Email: asokpant@gmail.com
4
+ -- Created on: 04/06/2026
5
+ """
@@ -0,0 +1,9 @@
1
+ """
2
+ -- Created by: Ashok Kumar Pant
3
+ -- Email: asokpant@gmail.com
4
+ -- Created on: 04/06/2026
5
+ """
6
+ from modeldownloaderutil import download_model
7
+
8
+ if __name__ == "__main__":
9
+ print(download_model("rustfs://treeleaf-models/model_4_kps.onnx"))
@@ -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