ollama-models 0.1.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,375 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ lerna-debug.log*
8
+
9
+ # Diagnostic reports (https://nodejs.org/api/report.html)
10
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11
+
12
+ # Runtime data
13
+ pids
14
+ *.pid
15
+ *.seed
16
+ *.pid.lock
17
+
18
+ # Directory for instrumented libs generated by jscoverage/JSCover
19
+ lib-cov
20
+
21
+ # Coverage directory used by tools like istanbul
22
+ coverage
23
+ *.lcov
24
+
25
+ # nyc test coverage
26
+ .nyc_output
27
+
28
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29
+ .grunt
30
+
31
+ # Bower dependency directory (https://bower.io/)
32
+ bower_components
33
+
34
+ # node-waf configuration
35
+ .lock-wscript
36
+
37
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
38
+ build/Release
39
+
40
+ # Dependency directories
41
+ node_modules/
42
+ jspm_packages/
43
+
44
+ # Snowpack dependency directory (https://snowpack.dev/)
45
+ web_modules/
46
+
47
+ # TypeScript cache
48
+ *.tsbuildinfo
49
+
50
+ # Optional npm cache directory
51
+ .npm
52
+
53
+ # Optional eslint cache
54
+ .eslintcache
55
+
56
+ # Optional stylelint cache
57
+ .stylelintcache
58
+
59
+ # Optional REPL history
60
+ .node_repl_history
61
+
62
+ # Output of 'npm pack'
63
+ *.tgz
64
+
65
+ # Yarn Integrity file
66
+ .yarn-integrity
67
+
68
+ # dotenv environment variable files
69
+ .env
70
+ .env.*
71
+ !.env.example
72
+
73
+ # parcel-bundler cache (https://parceljs.org/)
74
+ .cache
75
+ .parcel-cache
76
+
77
+ # Next.js build output
78
+ .next
79
+ out
80
+
81
+ # Nuxt.js build / generate output
82
+ .nuxt
83
+ dist
84
+ .output
85
+
86
+ # Gatsby files
87
+ .cache/
88
+ # Comment in the public line in if your project uses Gatsby and not Next.js
89
+ # https://nextjs.org/blog/next-9-1#public-directory-support
90
+ # public
91
+
92
+ # vuepress build output
93
+ .vuepress/dist
94
+
95
+ # vuepress v2.x temp and cache directory
96
+ .temp
97
+ .cache
98
+
99
+ # Sveltekit cache directory
100
+ .svelte-kit/
101
+
102
+ # vitepress build output
103
+ **/.vitepress/dist
104
+
105
+ # vitepress cache directory
106
+ **/.vitepress/cache
107
+
108
+ # Docusaurus cache and generated files
109
+ .docusaurus
110
+
111
+ # Serverless directories
112
+ .serverless/
113
+
114
+ # FuseBox cache
115
+ .fusebox/
116
+
117
+ # DynamoDB Local files
118
+ .dynamodb/
119
+
120
+ # Firebase cache directory
121
+ .firebase/
122
+
123
+ # TernJS port file
124
+ .tern-port
125
+
126
+ # Stores VSCode versions used for testing VSCode extensions
127
+ .vscode-test
128
+
129
+ # pnpm
130
+ .pnpm-store
131
+
132
+ # yarn v3
133
+ .pnp.*
134
+ .yarn/*
135
+ !.yarn/patches
136
+ !.yarn/plugins
137
+ !.yarn/releases
138
+ !.yarn/sdks
139
+ !.yarn/versions
140
+
141
+ # Vite files
142
+ vite.config.js.timestamp-*
143
+ vite.config.ts.timestamp-*
144
+ .vite/
145
+
146
+ # Byte-compiled / optimized / DLL files
147
+ __pycache__/
148
+ *.py[codz]
149
+ *$py.class
150
+
151
+ # C extensions
152
+ *.so
153
+
154
+ # Distribution / packaging
155
+ .Python
156
+ build/
157
+ develop-eggs/
158
+ dist/
159
+ downloads/
160
+ eggs/
161
+ .eggs/
162
+ lib/
163
+ lib64/
164
+ parts/
165
+ sdist/
166
+ var/
167
+ wheels/
168
+ share/python-wheels/
169
+ *.egg-info/
170
+ .installed.cfg
171
+ *.egg
172
+ MANIFEST
173
+
174
+ # PyInstaller
175
+ # Usually these files are written by a python script from a template
176
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
177
+ *.manifest
178
+ *.spec
179
+
180
+ # Installer logs
181
+ pip-log.txt
182
+ pip-delete-this-directory.txt
183
+
184
+ # Unit test / coverage reports
185
+ htmlcov/
186
+ .tox/
187
+ .nox/
188
+ .coverage
189
+ .coverage.*
190
+ .cache
191
+ nosetests.xml
192
+ coverage.xml
193
+ *.cover
194
+ *.py.cover
195
+ .hypothesis/
196
+ .pytest_cache/
197
+ cover/
198
+
199
+ # Translations
200
+ *.mo
201
+ *.pot
202
+
203
+ # Django stuff:
204
+ *.log
205
+ local_settings.py
206
+ db.sqlite3
207
+ db.sqlite3-journal
208
+
209
+ # Flask stuff:
210
+ instance/
211
+ .webassets-cache
212
+
213
+ # Scrapy stuff:
214
+ .scrapy
215
+
216
+ # Sphinx documentation
217
+ docs/_build/
218
+
219
+ # PyBuilder
220
+ .pybuilder/
221
+ target/
222
+
223
+ # Jupyter Notebook
224
+ .ipynb_checkpoints
225
+
226
+ # IPython
227
+ profile_default/
228
+ ipython_config.py
229
+
230
+ # pyenv
231
+ # For a library or package, you might want to ignore these files since the code is
232
+ # intended to run in multiple environments; otherwise, check them in:
233
+ # .python-version
234
+
235
+ # pipenv
236
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
237
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
238
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
239
+ # install all needed dependencies.
240
+ # Pipfile.lock
241
+
242
+ # UV
243
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
244
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
245
+ # commonly ignored for libraries.
246
+ # uv.lock
247
+
248
+ # poetry
249
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
250
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
251
+ # commonly ignored for libraries.
252
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
253
+ # poetry.lock
254
+ # poetry.toml
255
+
256
+ # pdm
257
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
258
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
259
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
260
+ # pdm.lock
261
+ # pdm.toml
262
+ .pdm-python
263
+ .pdm-build/
264
+
265
+ # pixi
266
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
267
+ # pixi.lock
268
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
269
+ # in the .venv directory. It is recommended not to include this directory in version control.
270
+ .pixi
271
+
272
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
273
+ __pypackages__/
274
+
275
+ # Celery stuff
276
+ celerybeat-schedule
277
+ celerybeat.pid
278
+
279
+ # Redis
280
+ *.rdb
281
+ *.aof
282
+ *.pid
283
+
284
+ # RabbitMQ
285
+ mnesia/
286
+ rabbitmq/
287
+ rabbitmq-data/
288
+
289
+ # ActiveMQ
290
+ activemq-data/
291
+
292
+ # SageMath parsed files
293
+ *.sage.py
294
+
295
+ # Environments
296
+ .env
297
+ .envrc
298
+ .venv
299
+ env/
300
+ venv/
301
+ ENV/
302
+ env.bak/
303
+ venv.bak/
304
+
305
+ # Spyder project settings
306
+ .spyderproject
307
+ .spyproject
308
+
309
+ # Rope project settings
310
+ .ropeproject
311
+
312
+ # mkdocs documentation
313
+ /site
314
+
315
+ # mypy
316
+ .mypy_cache/
317
+ .dmypy.json
318
+ dmypy.json
319
+
320
+ # Pyre type checker
321
+ .pyre/
322
+
323
+ # pytype static type analyzer
324
+ .pytype/
325
+
326
+ # Cython debug symbols
327
+ cython_debug/
328
+
329
+ # PyCharm
330
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
331
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
332
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
333
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
334
+ # .idea/
335
+
336
+ # Abstra
337
+ # Abstra is an AI-powered process automation framework.
338
+ # Ignore directories containing user credentials, local state, and settings.
339
+ # Learn more at https://abstra.io/docs
340
+ .abstra/
341
+
342
+ # Visual Studio Code
343
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
344
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
345
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
346
+ # you could uncomment the following to ignore the entire vscode folder
347
+ # .vscode/
348
+
349
+ # Ruff stuff:
350
+ .ruff_cache/
351
+
352
+ # PyPI configuration file
353
+ .pypirc
354
+
355
+ # Marimo
356
+ marimo/_static/
357
+ marimo/_lsp/
358
+ __marimo__/
359
+
360
+ # Streamlit
361
+ .streamlit/secrets.toml
362
+
363
+ # Cloudflare Wrangler
364
+ .wrangler/
365
+
366
+ # Rye virtual envs
367
+ .venv/
368
+ .python-version
369
+
370
+ # tsup / tsc build output
371
+ dist/
372
+
373
+ # Temp (system)
374
+ .temp/
375
+
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: ollama-models
3
+ Version: 0.1.0
4
+ Summary: Python client for the ollama-models API — search and list Ollama model weights
5
+ Project-URL: Repository, https://github.com/devcomfort/ollama-models
6
+ Author-email: devcomfort <im@devcomfort.me>
7
+ License: MIT
8
+ Keywords: llm,models,ollama,registry,search
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.8
20
+ Requires-Dist: httpx>=0.27.0
21
+ Description-Content-Type: text/markdown
22
+
23
+ # ollama-models (Python)
24
+
25
+ Python client for searching and listing models from the [Ollama](https://ollama.com) registry.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install ollama-models
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```python
36
+ from ollama_models import OllamaModelsClient
37
+
38
+ # No base URL needed — defaults to the official hosted instance
39
+ client = OllamaModelsClient()
40
+
41
+ # Pass a base URL only if you self-host the API
42
+ # client = OllamaModelsClient("https://your-own-instance.workers.dev")
43
+
44
+ # Search models
45
+ result = client.search("qwen3", page=1)
46
+ for page in result.pages:
47
+ print(page.http_url)
48
+
49
+ # Get all tags for a model
50
+ model = client.get_model("qwen3")
51
+ print(model.default_model_id) # qwen3:latest
52
+ for w in model.model_list:
53
+ print(w.id) # qwen3:latest, qwen3:4b, ...
54
+
55
+ # Async usage
56
+ import asyncio
57
+
58
+ async def main():
59
+ result = await client.search_async("qwen3")
60
+ model = await client.get_model_async("qwen3")
61
+
62
+ asyncio.run(main())
63
+ ```
@@ -0,0 +1,41 @@
1
+ # ollama-models (Python)
2
+
3
+ Python client for searching and listing models from the [Ollama](https://ollama.com) registry.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install ollama-models
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ from ollama_models import OllamaModelsClient
15
+
16
+ # No base URL needed — defaults to the official hosted instance
17
+ client = OllamaModelsClient()
18
+
19
+ # Pass a base URL only if you self-host the API
20
+ # client = OllamaModelsClient("https://your-own-instance.workers.dev")
21
+
22
+ # Search models
23
+ result = client.search("qwen3", page=1)
24
+ for page in result.pages:
25
+ print(page.http_url)
26
+
27
+ # Get all tags for a model
28
+ model = client.get_model("qwen3")
29
+ print(model.default_model_id) # qwen3:latest
30
+ for w in model.model_list:
31
+ print(w.id) # qwen3:latest, qwen3:4b, ...
32
+
33
+ # Async usage
34
+ import asyncio
35
+
36
+ async def main():
37
+ result = await client.search_async("qwen3")
38
+ model = await client.get_model_async("qwen3")
39
+
40
+ asyncio.run(main())
41
+ ```
@@ -0,0 +1,51 @@
1
+ [project]
2
+ name = "ollama-models"
3
+ version = "0.1.0"
4
+ description = "Python client for the ollama-models API — search and list Ollama model weights"
5
+ authors = [
6
+ { name = "devcomfort", email = "im@devcomfort.me" }
7
+ ]
8
+ license = { text = "MIT" }
9
+ readme = "README.md"
10
+ requires-python = ">= 3.8"
11
+ keywords = ["ollama", "llm", "models", "search", "registry"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.8",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ ]
24
+ dependencies = [
25
+ "httpx>=0.27.0",
26
+ ]
27
+
28
+ [project.urls]
29
+ Repository = "https://github.com/devcomfort/ollama-models"
30
+
31
+ [build-system]
32
+ requires = ["hatchling"]
33
+ build-backend = "hatchling.build"
34
+
35
+ [tool.rye]
36
+ managed = true
37
+ dev-dependencies = [
38
+ "pytest>=8.0.0",
39
+ "pytest-asyncio>=0.23.0",
40
+ "pytest-httpx>=0.21.0",
41
+ "build>=1.0.0",
42
+ ]
43
+
44
+ [tool.hatch.metadata]
45
+ allow-direct-references = true
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/ollama_models"]
49
+
50
+ [tool.pytest.ini_options]
51
+ asyncio_mode = "auto"
@@ -0,0 +1,47 @@
1
+ # generated by rye
2
+ # use `rye lock` or `rye sync` to update this lockfile
3
+ #
4
+ # last locked with the following flags:
5
+ # pre: false
6
+ # features: []
7
+ # all-features: false
8
+ # with-sources: false
9
+ # generate-hashes: false
10
+ # universal: false
11
+
12
+ -e file:.
13
+ anyio==4.13.0
14
+ # via httpx
15
+ build==1.4.2
16
+ certifi==2026.2.25
17
+ # via httpcore
18
+ # via httpx
19
+ h11==0.16.0
20
+ # via httpcore
21
+ httpcore==1.0.9
22
+ # via httpx
23
+ httpx==0.28.1
24
+ # via ollama-models
25
+ # via pytest-httpx
26
+ idna==3.11
27
+ # via anyio
28
+ # via httpx
29
+ iniconfig==2.3.0
30
+ # via pytest
31
+ packaging==26.0
32
+ # via build
33
+ # via pytest
34
+ pluggy==1.6.0
35
+ # via pytest
36
+ pygments==2.19.2
37
+ # via pytest
38
+ pyproject-hooks==1.2.0
39
+ # via build
40
+ pytest==9.0.2
41
+ # via pytest-asyncio
42
+ # via pytest-httpx
43
+ pytest-asyncio==1.3.0
44
+ pytest-httpx==0.36.0
45
+ typing-extensions==4.15.0
46
+ # via anyio
47
+ # via pytest-asyncio
@@ -0,0 +1,28 @@
1
+ # generated by rye
2
+ # use `rye lock` or `rye sync` to update this lockfile
3
+ #
4
+ # last locked with the following flags:
5
+ # pre: false
6
+ # features: []
7
+ # all-features: false
8
+ # with-sources: false
9
+ # generate-hashes: false
10
+ # universal: false
11
+
12
+ -e file:.
13
+ anyio==4.13.0
14
+ # via httpx
15
+ certifi==2026.2.25
16
+ # via httpcore
17
+ # via httpx
18
+ h11==0.16.0
19
+ # via httpcore
20
+ httpcore==1.0.9
21
+ # via httpx
22
+ httpx==0.28.1
23
+ # via ollama-models
24
+ idna==3.11
25
+ # via anyio
26
+ # via httpx
27
+ typing-extensions==4.15.0
28
+ # via anyio
@@ -0,0 +1,10 @@
1
+ from .client import OllamaModelsClient
2
+ from .types import ModelList, ModelPage, ModelWeight, SearchResult
3
+
4
+ __all__ = [
5
+ "OllamaModelsClient",
6
+ "SearchResult",
7
+ "ModelPage",
8
+ "ModelList",
9
+ "ModelWeight",
10
+ ]
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict
4
+
5
+ import httpx
6
+
7
+ from .types import ModelList, ModelPage, ModelWeight, SearchResult
8
+
9
+ DEFAULT_BASE_URL = "https://ollama-models-api.devcomfort.workers.dev"
10
+
11
+
12
+ class OllamaModelsClient:
13
+ """Sync/async client for the ollama-models Cloudflare Workers API.
14
+
15
+ Usage (sync)::
16
+
17
+ client = OllamaModelsClient()
18
+ result = client.search("qwen3", page=1)
19
+ model = client.get_model("qwen3")
20
+
21
+ Usage (async)::
22
+
23
+ result = await client.search_async("qwen3", page=1)
24
+ model = await client.get_model_async("qwen3")
25
+ """
26
+
27
+ def __init__(self, base_url: str = DEFAULT_BASE_URL) -> None:
28
+ self._base_url = base_url.rstrip("/")
29
+
30
+ # ------------------------------------------------------------------
31
+ # Search
32
+ # ------------------------------------------------------------------
33
+
34
+ def search(self, keyword: str = "", page: int = 1) -> SearchResult:
35
+ params: Dict[str, str] = {"page": str(page)}
36
+ if keyword:
37
+ params["q"] = keyword
38
+ with httpx.Client() as client:
39
+ res = client.get(f"{self._base_url}/search", params=params)
40
+ res.raise_for_status()
41
+ data = res.json()
42
+ return _parse_search_result(data)
43
+
44
+ async def search_async(self, keyword: str = "", page: int = 1) -> SearchResult:
45
+ params: Dict[str, str] = {"page": str(page)}
46
+ if keyword:
47
+ params["q"] = keyword
48
+ async with httpx.AsyncClient() as client:
49
+ res = await client.get(f"{self._base_url}/search", params=params)
50
+ res.raise_for_status()
51
+ data = res.json()
52
+ return _parse_search_result(data)
53
+
54
+ # ------------------------------------------------------------------
55
+ # Model
56
+ # ------------------------------------------------------------------
57
+
58
+ def get_model(self, name: str) -> ModelList:
59
+ with httpx.Client() as client:
60
+ res = client.get(f"{self._base_url}/model", params={"name": name})
61
+ res.raise_for_status()
62
+ data = res.json()
63
+ return _parse_model_list(data)
64
+
65
+ async def get_model_async(self, name: str) -> ModelList:
66
+ async with httpx.AsyncClient() as client:
67
+ res = await client.get(f"{self._base_url}/model", params={"name": name})
68
+ res.raise_for_status()
69
+ data = res.json()
70
+ return _parse_model_list(data)
71
+
72
+
73
+ # ------------------------------------------------------------------
74
+ # Internal parsers
75
+ # ------------------------------------------------------------------
76
+
77
+
78
+ def _parse_search_result(data: dict) -> SearchResult:
79
+ return SearchResult(
80
+ pages=[ModelPage(http_url=p["http_url"]) for p in data["pages"]],
81
+ page_id=int(data["page_id"]),
82
+ keyword=str(data["keyword"]),
83
+ )
84
+
85
+
86
+ def _parse_model_list(data: dict) -> ModelList:
87
+ return ModelList(
88
+ model_list=[
89
+ ModelWeight(http_url=w["http_url"], id=w["id"]) for w in data["model_list"]
90
+ ],
91
+ default_model_id=str(data["default_model_id"]),
92
+ )
@@ -0,0 +1,26 @@
1
+ from dataclasses import dataclass
2
+ from typing import List
3
+
4
+
5
+ @dataclass
6
+ class ModelPage:
7
+ http_url: str
8
+
9
+
10
+ @dataclass
11
+ class SearchResult:
12
+ pages: List[ModelPage]
13
+ page_id: int
14
+ keyword: str
15
+
16
+
17
+ @dataclass
18
+ class ModelWeight:
19
+ http_url: str
20
+ id: str
21
+
22
+
23
+ @dataclass
24
+ class ModelList:
25
+ model_list: List[ModelWeight]
26
+ default_model_id: str
File without changes
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+ import httpx
5
+ from pytest_httpx import HTTPXMock
6
+
7
+ from ollama_models import OllamaModelsClient
8
+ from ollama_models.client import DEFAULT_BASE_URL
9
+
10
+ # ─── fixtures ─────────────────────────────────────────────────────────────────
11
+
12
+ MOCK_SEARCH = {
13
+ "pages": [
14
+ {"http_url": "https://ollama.com/library/qwen3"},
15
+ {"http_url": "https://ollama.com/library/mistral"},
16
+ ],
17
+ "page_id": 1,
18
+ "keyword": "qwen3",
19
+ }
20
+
21
+ MOCK_MODEL = {
22
+ "model_list": [
23
+ {"http_url": "https://ollama.com/library/qwen3", "id": "qwen3:latest"},
24
+ {"http_url": "https://ollama.com/library/qwen3", "id": "qwen3:4b"},
25
+ ],
26
+ "default_model_id": "qwen3:latest",
27
+ }
28
+
29
+
30
+ # ─── DEFAULT_BASE_URL ─────────────────────────────────────────────────────────
31
+
32
+
33
+ def test_default_base_url_is_official_instance():
34
+ assert DEFAULT_BASE_URL == "https://ollama-models-api.devcomfort.workers.dev"
35
+
36
+
37
+ def test_client_uses_default_base_url(httpx_mock: HTTPXMock):
38
+ httpx_mock.add_response(json=MOCK_SEARCH)
39
+ OllamaModelsClient().search("qwen3")
40
+ request = httpx_mock.get_requests()[0]
41
+ assert DEFAULT_BASE_URL in str(request.url)
42
+
43
+
44
+ # ─── search() (sync) ──────────────────────────────────────────────────────────
45
+
46
+
47
+ def test_search_returns_search_result(httpx_mock: HTTPXMock):
48
+ httpx_mock.add_response(json=MOCK_SEARCH)
49
+ result = OllamaModelsClient().search("qwen3", page=1)
50
+ assert result.keyword == "qwen3"
51
+ assert result.page_id == 1
52
+ assert len(result.pages) == 2
53
+ assert result.pages[0].http_url == "https://ollama.com/library/qwen3"
54
+
55
+
56
+ def test_search_sends_keyword_and_page_params(httpx_mock: HTTPXMock):
57
+ httpx_mock.add_response(json=MOCK_SEARCH)
58
+ OllamaModelsClient().search("mistral", page=2)
59
+ request = httpx_mock.get_requests()[0]
60
+ assert "q=mistral" in str(request.url)
61
+ assert "page=2" in str(request.url)
62
+
63
+
64
+ def test_search_omits_q_param_when_keyword_is_empty(httpx_mock: HTTPXMock):
65
+ httpx_mock.add_response(json=MOCK_SEARCH)
66
+ OllamaModelsClient().search("", page=1)
67
+ request = httpx_mock.get_requests()[0]
68
+ assert "q=" not in str(request.url)
69
+
70
+
71
+ def test_search_hits_search_endpoint(httpx_mock: HTTPXMock):
72
+ httpx_mock.add_response(json=MOCK_SEARCH)
73
+ OllamaModelsClient().search("qwen3")
74
+ request = httpx_mock.get_requests()[0]
75
+ assert "/search" in str(request.url)
76
+
77
+
78
+ def test_search_raises_on_http_error(httpx_mock: HTTPXMock):
79
+ httpx_mock.add_response(status_code=500)
80
+ with pytest.raises(httpx.HTTPStatusError):
81
+ OllamaModelsClient().search("qwen3")
82
+
83
+
84
+ # ─── search_async() ───────────────────────────────────────────────────────────
85
+
86
+
87
+ async def test_search_async_returns_search_result(httpx_mock: HTTPXMock):
88
+ httpx_mock.add_response(json=MOCK_SEARCH)
89
+ result = await OllamaModelsClient().search_async("qwen3", page=1)
90
+ assert result.keyword == "qwen3"
91
+ assert len(result.pages) == 2
92
+
93
+
94
+ async def test_search_async_sends_keyword_and_page_params(httpx_mock: HTTPXMock):
95
+ httpx_mock.add_response(json=MOCK_SEARCH)
96
+ await OllamaModelsClient().search_async("qwen3", page=3)
97
+ request = httpx_mock.get_requests()[0]
98
+ assert "q=qwen3" in str(request.url)
99
+ assert "page=3" in str(request.url)
100
+
101
+
102
+ async def test_search_async_raises_on_http_error(httpx_mock: HTTPXMock):
103
+ httpx_mock.add_response(status_code=503)
104
+ with pytest.raises(httpx.HTTPStatusError):
105
+ await OllamaModelsClient().search_async("qwen3")
106
+
107
+
108
+ # ─── get_model() (sync) ───────────────────────────────────────────────────────
109
+
110
+
111
+ def test_get_model_returns_model_list(httpx_mock: HTTPXMock):
112
+ httpx_mock.add_response(json=MOCK_MODEL)
113
+ result = OllamaModelsClient().get_model("qwen3")
114
+ assert result.default_model_id == "qwen3:latest"
115
+ assert len(result.model_list) == 2
116
+ assert result.model_list[0].id == "qwen3:latest"
117
+ assert result.model_list[1].id == "qwen3:4b"
118
+
119
+
120
+ def test_get_model_sends_name_param(httpx_mock: HTTPXMock):
121
+ httpx_mock.add_response(json=MOCK_MODEL)
122
+ OllamaModelsClient().get_model("qwen3")
123
+ request = httpx_mock.get_requests()[0]
124
+ assert "name=qwen3" in str(request.url)
125
+
126
+
127
+ def test_get_model_hits_model_endpoint(httpx_mock: HTTPXMock):
128
+ httpx_mock.add_response(json=MOCK_MODEL)
129
+ OllamaModelsClient().get_model("qwen3")
130
+ request = httpx_mock.get_requests()[0]
131
+ assert "/model" in str(request.url)
132
+
133
+
134
+ def test_get_model_raises_on_http_error(httpx_mock: HTTPXMock):
135
+ httpx_mock.add_response(status_code=404)
136
+ with pytest.raises(httpx.HTTPStatusError):
137
+ OllamaModelsClient().get_model("nonexistent")
138
+
139
+
140
+ # ─── get_model_async() ────────────────────────────────────────────────────────
141
+
142
+
143
+ async def test_get_model_async_returns_model_list(httpx_mock: HTTPXMock):
144
+ httpx_mock.add_response(json=MOCK_MODEL)
145
+ result = await OllamaModelsClient().get_model_async("qwen3")
146
+ assert result.default_model_id == "qwen3:latest"
147
+ assert len(result.model_list) == 2
148
+
149
+
150
+ async def test_get_model_async_sends_name_param(httpx_mock: HTTPXMock):
151
+ httpx_mock.add_response(json=MOCK_MODEL)
152
+ await OllamaModelsClient().get_model_async("mistral")
153
+ request = httpx_mock.get_requests()[0]
154
+ assert "name=mistral" in str(request.url)
155
+
156
+
157
+ async def test_get_model_async_raises_on_http_error(httpx_mock: HTTPXMock):
158
+ httpx_mock.add_response(status_code=404)
159
+ with pytest.raises(httpx.HTTPStatusError):
160
+ await OllamaModelsClient().get_model_async("nonexistent")
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from ollama_models.types import ModelList, ModelPage, ModelWeight, SearchResult
4
+
5
+
6
+ def test_model_page_stores_url():
7
+ page = ModelPage(http_url="https://ollama.com/library/qwen3")
8
+ assert page.http_url == "https://ollama.com/library/qwen3"
9
+
10
+
11
+ def test_search_result_stores_fields():
12
+ result = SearchResult(
13
+ pages=[ModelPage(http_url="https://ollama.com/library/qwen3")],
14
+ page_id=2,
15
+ keyword="qwen3",
16
+ )
17
+ assert result.page_id == 2
18
+ assert result.keyword == "qwen3"
19
+ assert len(result.pages) == 1
20
+ assert result.pages[0].http_url == "https://ollama.com/library/qwen3"
21
+
22
+
23
+ def test_model_weight_stores_fields():
24
+ weight = ModelWeight(
25
+ http_url="https://ollama.com/library/qwen3",
26
+ id="qwen3:4b",
27
+ )
28
+ assert weight.http_url == "https://ollama.com/library/qwen3"
29
+ assert weight.id == "qwen3:4b"
30
+
31
+
32
+ def test_model_list_stores_fields():
33
+ model_list = ModelList(
34
+ model_list=[
35
+ ModelWeight(http_url="https://ollama.com/library/qwen3", id="qwen3:latest"),
36
+ ModelWeight(http_url="https://ollama.com/library/qwen3", id="qwen3:4b"),
37
+ ],
38
+ default_model_id="qwen3:latest",
39
+ )
40
+ assert model_list.default_model_id == "qwen3:latest"
41
+ assert len(model_list.model_list) == 2
42
+ assert model_list.model_list[0].id == "qwen3:latest"
43
+ assert model_list.model_list[1].id == "qwen3:4b"