Skip to content

Commit 75a3cfd

Browse files
anirbanbasuCopilot
andauthored
Improvements to support Windows (#21)
* chore: Added Windows tests -- do they run? chore: Corrected code to support Windows. * fix: Attempting to correct the Windows runner workflow. * fix: Correcting Windows path separator for selecting tests. * fix: Further correcting pytest path separator for Windows. * fix: Added the `pywin32` package if platform is Windows. * fix: Using ~ to obtain the user home path on Windows too. * chore: Upgraded packages. * chore: Removing `pytest` on Windows because the runner is on Windows server, which is unlikely to be the end-user platform. * debug: Hugging Face model tag extraction has a problem: #20 (comment) * fix: Partly correcting the Windows related problems in #20 * copilot: Removing unnecessary commented out code. Co-authored-by: Copilot <[email protected]> Signed-off-by: Anirban Basu <[email protected]> * copilot: Simplifying windows platform condition for package. Co-authored-by: Copilot <[email protected]> Signed-off-by: Anirban Basu <[email protected]> * fix: Correcting half-right CoPilot edits for removing unnecessary commented out code. --------- Signed-off-by: Anirban Basu <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 45588ee commit 75a3cfd

File tree

10 files changed

+214
-161
lines changed

10 files changed

+214
-161
lines changed

.github/workflows/uv-pytest.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ name: pytest
1515

1616
jobs:
1717
uv-pytest:
18-
name: python
18+
name: python-on-nix
1919
runs-on: ubuntu-latest
20-
# runs-on: self-hosted
2120

2221
steps:
2322
- uses: actions/checkout@v4
@@ -32,5 +31,4 @@ jobs:
3231
run: uv sync --all-groups
3332

3433
- name: Run tests using pytest
35-
# Disable model_download tests on non self-hosted runners for now.
3634
run: uv run --group test pytest -v tests/

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies = [
3838
"ollama>=0.6.0",
3939
"psutil>=7.0.0",
4040
"typer>=0.16.0",
41+
"pywin32>=311 ; sys_platform == 'win32'",
4142
]
4243

4344
[tool.coverage.run]

src/ollama_downloader/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ class EnvVar:
2525

2626
OD_UA_NAME_VER = env.str("OD_UA_NAME_VER", default="ollama-downloader/0.1.1")
2727

28-
OD_SETTINGS_FILE = env.str("OD_SETTINGS_FILE", default="conf/settings.json")
28+
OD_SETTINGS_FILE = env.str(
29+
"OD_SETTINGS_FILE", default=os.path.join("conf", "settings.json")
30+
)
2931

3032

3133
logging.basicConfig(

src/ollama_downloader/cli.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55
from types import FrameType
66
from typing import Optional
7+
from httpx import HTTPStatusError
78
from typing_extensions import Annotated
89
import typer
910
from rich import print as print
@@ -329,7 +330,9 @@ async def run_list_models(
329330
else:
330331
print(f"Model identifiers: ({len(result)}): {result}")
331332
except Exception as e:
332-
logger.error(f"Error in listing models. {e}")
333+
logger.error(
334+
f"Error in listing models. {e}{'\n' + e.response.text if isinstance(e, HTTPStatusError) else ''}"
335+
)
333336
finally:
334337
self._cleanup()
335338

@@ -342,7 +345,9 @@ async def run_list_tags(self, model_identifier: str):
342345
result = await self._list_tags(model_identifier=model_identifier)
343346
print(f"Model tags: ({len(result)}): {result}")
344347
except Exception as e:
345-
logger.error(f"Error in listing model tags. {e}")
348+
logger.error(
349+
f"Error in listing model tags. {e}{'\n' + e.response.text if isinstance(e, HTTPStatusError) else ''}"
350+
)
346351
finally:
347352
self._cleanup()
348353

@@ -354,7 +359,9 @@ async def run_model_download(self, model_tag: str):
354359
self._initialize()
355360
await self._model_download(model_tag=model_tag)
356361
except Exception as e:
357-
logger.error(f"Error in downloading model. {e}")
362+
logger.error(
363+
f"Error in downloading model. {e}{'\n' + e.response.text if isinstance(e, HTTPStatusError) else ''}"
364+
)
358365
finally:
359366
self._cleanup()
360367

@@ -376,7 +383,9 @@ async def run_hf_list_models(
376383
else:
377384
print(f"Model identifiers: ({len(result)}): {result}")
378385
except Exception as e:
379-
logger.error(f"Error in listing models. {e}")
386+
logger.error(
387+
f"Error in listing models. {e}{'\n' + e.response.text if isinstance(e, HTTPStatusError) else ''}"
388+
)
380389
finally:
381390
self._cleanup()
382391

@@ -391,7 +400,9 @@ async def run_hf_list_tags(self, model_identifier: str):
391400
result = await self._hf_list_tags(model_identifier=model_identifier)
392401
print(f"Model tags: ({len(result)}): {result}")
393402
except Exception as e:
394-
logger.error(f"Error in listing model tags. {e}")
403+
logger.error(
404+
f"Error in listing model tags. {e}{'\n' + e.response.text if isinstance(e, HTTPStatusError) else ''}"
405+
)
395406
finally:
396407
self._cleanup()
397408

@@ -403,7 +414,9 @@ async def run_hf_model_download(self, user_repo_quant: str):
403414
self._initialize()
404415
await self._hf_model_download(user_repo_quant=user_repo_quant)
405416
except Exception as e:
406-
logger.error(f"Error in downloading Hugging Face model. {e}")
417+
logger.error(
418+
f"Error in downloading Hugging Face model. {e}{'\n' + e.response.text if isinstance(e, HTTPStatusError) else ''}"
419+
)
407420
finally:
408421
self._cleanup()
409422

src/ollama_downloader/data/data_models.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ class OllamaLibrary(BaseModel):
4545
models_path: Annotated[
4646
str, AfterValidator(CustomValidators.validate_path_as_dir)
4747
] = Field(
48-
default="~/.ollama/models",
48+
# Windows environment variables: https://learn.microsoft.com/en-us/windows/deployment/usmt/usmt-recognized-environment-variables
49+
default=os.path.join(
50+
"~",
51+
".ollama",
52+
"models",
53+
),
4954
description="Path to the Ollama models on the filesystem. This should be a directory where model BLOBs and manifest metadata are stored.",
5055
)
5156
registry_base_url: Annotated[str, AfterValidator(CustomValidators.validate_url)] = (

src/ollama_downloader/downloader/hf_model_downloader.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,12 @@
2121
class HuggingFaceModelDownloader(ModelDownloader):
2222
def __init__(self):
2323
super().__init__()
24-
# if not self.settings.ollama_library.verify_ssl:
25-
# logger.warning(
26-
# "Disabling SSL verification for HTTP requests. This is not recommended for production use."
27-
# )
28-
# session = requests.Session()
29-
# session.verify = False
30-
# configure_http_backend(backend_factory=lambda: session)
3124

3225
def download_model(self, model_identifier: str) -> bool:
3326
# Validate the response as an ImageManifest but don't enforce strict validation
3427
(user, model_repo), quant = (
3528
model_identifier.split(":")[0].split("/"),
36-
model_identifier.split(":")[1],
29+
model_identifier.split(":")[1] if ":" in model_identifier else "latest",
3730
)
3831
print(
3932
f"Downloading Hugging Face model {model_repo} from {user} with {quant} quantisation"

src/ollama_downloader/downloader/model_downloader.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,22 +139,25 @@ def _make_manifest_url(
139139
Returns:
140140
httpx.URL: The constructed manifest URL.
141141
"""
142+
model, tag = (
143+
model_identifier.split(":")
144+
if ":" in model_identifier
145+
else (model_identifier, "latest")
146+
)
142147
match model_source:
143148
case ModelSource.OLLAMA:
144-
model, tag = (
145-
model_identifier.split(":")
146-
if ":" in model_identifier
147-
else (model_identifier, "latest")
148-
)
149149
logger.debug(f"Constructing manifest URL for {model}:{tag}")
150150
return httpx.URL(self.settings.ollama_library.registry_base_url).join(
151151
f"{model}/manifests/{tag}"
152152
)
153153
case ModelSource.HUGGINGFACE:
154154
logger.debug(f"Constructing manifest URL for {model_identifier}")
155-
return httpx.URL(
156-
f"{ModelDownloader.HF_BASE_URL}{model_identifier.replace(':', '/manifests/')}"
155+
hf_model_identifier = (
156+
model_identifier.replace(":", "/manifests/")
157+
if ":" in model_identifier
158+
else f"{model_identifier}/manifests/{tag}"
157159
)
160+
return httpx.URL(f"{ModelDownloader.HF_BASE_URL}{hf_model_identifier}")
158161
case _:
159162
raise ValueError(f"Unsupported model source: {model_source}")
160163

src/ollama_downloader/sysinfo.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
import grp
21
import logging
32
import os
43
import re
54
from typing import ClassVar
65
import platform
76
import psutil
87

8+
try:
9+
import grp # Unix, macOS and Linux only
10+
except ImportError:
11+
# Windows does not have the grp module
12+
grp = None # type: ignore[assignment]
13+
914
from ollama import Client as OllamaClient
1015

1116
logger = logging.getLogger(__name__)
@@ -118,7 +123,9 @@ def get_process_owner(self) -> tuple[str, int, str, int] | None:
118123
username = proc.username()
119124
uid = proc.uids().real if hasattr(proc, "uids") else -1
120125
gid = proc.gids().real if hasattr(proc, "gids") else -1
121-
groupname = grp.getgrgid(gid).gr_name if gid != -1 else ""
126+
groupname = (
127+
grp.getgrgid(gid).gr_name if grp is not None and gid != -1 else ""
128+
)
122129
self.process_owner = (username, uid, groupname, gid)
123130
logger.debug(
124131
f"Owner of process {proc.name()} ({self.process_id}, {proc.status()}): {self.process_owner}"

tests_on_windows.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
tests/test_data_models.py
2+
tests/test_typer.py::TestTyperCalls::test_version
3+
tests/test_typer.py::TestTyperCalls::test_show_config
4+
tests/test_typer.py::TestTyperCalls::test_list_models
5+
tests/test_typer.py::TestTyperCalls::test_list_tags
6+
tests/test_typer.py::TestTyperCalls::test_model_download
7+
tests/test_typer.py::TestTyperCalls::test_hf_list_models
8+
tests/test_typer.py::TestTyperCalls::test_hf_list_tags
9+
tests/test_typer.py::TestTyperCalls::test_hf_model_download

0 commit comments

Comments
 (0)