llmt-cli 0.2.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.
- llmt/__init__.py +7 -0
- llmt/api.py +138 -0
- llmt/cli.py +562 -0
- llmt/clients.py +1316 -0
- llmt/data/manifest-pubkey.json +7 -0
- llmt/doctor.py +970 -0
- llmt/doctorcmd.py +142 -0
- llmt/download.py +321 -0
- llmt/errors.py +132 -0
- llmt/layout.py +676 -0
- llmt/manifest.py +314 -0
- llmt/matcher.py +358 -0
- llmt/models.py +130 -0
- llmt/output.py +51 -0
- llmt/permissions.py +154 -0
- llmt/scancmd.py +556 -0
- llmt/scanner.py +328 -0
- llmt/telemetry/__init__.py +15 -0
- llmt/telemetry/artifacts.py +217 -0
- llmt/telemetry/beat.py +221 -0
- llmt/telemetry/cmd.py +358 -0
- llmt/telemetry/collect.py +252 -0
- llmt/telemetry/config.py +237 -0
- llmt/telemetry/consent.py +57 -0
- llmt/telemetry/errors.py +41 -0
- llmt/telemetry/wire.py +194 -0
- llmt/verify.py +129 -0
- llmt_cli-0.2.0.dist-info/METADATA +307 -0
- llmt_cli-0.2.0.dist-info/RECORD +33 -0
- llmt_cli-0.2.0.dist-info/WHEEL +5 -0
- llmt_cli-0.2.0.dist-info/entry_points.txt +2 -0
- llmt_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- llmt_cli-0.2.0.dist-info/top_level.txt +1 -0
llmt/__init__.py
ADDED
llmt/api.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""HTTP client for the FROZEN WP-8.1 public API.
|
|
2
|
+
|
|
3
|
+
Envelope contract:
|
|
4
|
+
success : {"data": ..., "meta"?: ...}
|
|
5
|
+
error : {"error": {"code", "message", "details"?}}
|
|
6
|
+
|
|
7
|
+
Base URL: default https://llmtorrents.com/api, overridable via the
|
|
8
|
+
``LLMT_API_BASE`` env var or ``--api-base``. An optional bearer token
|
|
9
|
+
(``LLMT_API_KEY`` / ``--api-key``) only raises rate limits (reads are public).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
import requests
|
|
18
|
+
|
|
19
|
+
from .errors import (
|
|
20
|
+
ApiError,
|
|
21
|
+
NotFoundError,
|
|
22
|
+
RateLimitedError,
|
|
23
|
+
TimeoutErrorLlmt,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
DEFAULT_API_BASE = "https://llmtorrents.com/api"
|
|
27
|
+
DEFAULT_TIMEOUT = 30
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def resolve_base(explicit: Optional[str] = None) -> str:
|
|
31
|
+
base = explicit or os.environ.get("LLMT_API_BASE") or DEFAULT_API_BASE
|
|
32
|
+
return base.rstrip("/")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def resolve_key(explicit: Optional[str] = None) -> Optional[str]:
|
|
36
|
+
return explicit or os.environ.get("LLMT_API_KEY") or None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ApiClient:
|
|
40
|
+
"""Thin, dependency-light wrapper over the catalog's JSON API."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, base: Optional[str] = None, key: Optional[str] = None,
|
|
43
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
44
|
+
session: Optional[requests.Session] = None):
|
|
45
|
+
self.base = resolve_base(base)
|
|
46
|
+
self.key = resolve_key(key)
|
|
47
|
+
self.timeout = timeout
|
|
48
|
+
self.session = session or requests.Session()
|
|
49
|
+
|
|
50
|
+
# ---- low-level ------------------------------------------------------
|
|
51
|
+
def _headers(self) -> dict[str, str]:
|
|
52
|
+
h = {"Accept": "application/json", "User-Agent": "llmt-cli"}
|
|
53
|
+
if self.key:
|
|
54
|
+
h["Authorization"] = f"Bearer {self.key}"
|
|
55
|
+
return h
|
|
56
|
+
|
|
57
|
+
def _get(self, path: str, params: Optional[dict] = None) -> dict[str, Any]:
|
|
58
|
+
url = f"{self.base}/{path.lstrip('/')}"
|
|
59
|
+
try:
|
|
60
|
+
resp = self.session.get(
|
|
61
|
+
url, params=params, headers=self._headers(), timeout=self.timeout
|
|
62
|
+
)
|
|
63
|
+
except requests.Timeout as exc:
|
|
64
|
+
raise TimeoutErrorLlmt(
|
|
65
|
+
f"API request timed out after {self.timeout}s: {url}"
|
|
66
|
+
) from exc
|
|
67
|
+
except requests.RequestException as exc:
|
|
68
|
+
raise ApiError(f"API request failed: {exc}") from exc
|
|
69
|
+
return self._handle(resp, url)
|
|
70
|
+
|
|
71
|
+
def _handle(self, resp: "requests.Response", url: str) -> dict[str, Any]:
|
|
72
|
+
# Try to decode an error envelope regardless of status.
|
|
73
|
+
payload: Any = None
|
|
74
|
+
try:
|
|
75
|
+
payload = resp.json()
|
|
76
|
+
except ValueError:
|
|
77
|
+
payload = None
|
|
78
|
+
|
|
79
|
+
err = payload.get("error") if isinstance(payload, dict) else None
|
|
80
|
+
code = err.get("code") if isinstance(err, dict) else None
|
|
81
|
+
message = err.get("message") if isinstance(err, dict) else None
|
|
82
|
+
details = err.get("details") if isinstance(err, dict) else None
|
|
83
|
+
|
|
84
|
+
if resp.status_code == 200 and isinstance(payload, dict) and "error" not in payload:
|
|
85
|
+
return payload
|
|
86
|
+
|
|
87
|
+
if resp.status_code == 429:
|
|
88
|
+
ra = resp.headers.get("Retry-After")
|
|
89
|
+
retry_after = None
|
|
90
|
+
if ra is not None:
|
|
91
|
+
try:
|
|
92
|
+
retry_after = int(ra)
|
|
93
|
+
except ValueError:
|
|
94
|
+
retry_after = None
|
|
95
|
+
raise RateLimitedError(
|
|
96
|
+
message or "Rate limited by the API.",
|
|
97
|
+
retry_after=retry_after, code=code or "rate_limited",
|
|
98
|
+
status=429, details=details,
|
|
99
|
+
)
|
|
100
|
+
if resp.status_code == 404:
|
|
101
|
+
raise NotFoundError(
|
|
102
|
+
message or "Not found.", code=code or "not_found",
|
|
103
|
+
status=404, details=details,
|
|
104
|
+
)
|
|
105
|
+
if resp.status_code == 403:
|
|
106
|
+
raise ApiError(
|
|
107
|
+
message or "Forbidden (the resource may be license-gated).",
|
|
108
|
+
code=code or "forbidden", status=403, details=details,
|
|
109
|
+
)
|
|
110
|
+
# Any other non-200, or a 200 that is actually an error envelope.
|
|
111
|
+
raise ApiError(
|
|
112
|
+
message or f"Unexpected API response (HTTP {resp.status_code}).",
|
|
113
|
+
code=code, status=resp.status_code, details=details,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# ---- endpoints ------------------------------------------------------
|
|
117
|
+
def list_models(self, q: Optional[str] = None, fit: Optional[str] = None,
|
|
118
|
+
per_page: int = 24, sort: Optional[str] = None,
|
|
119
|
+
extra: Optional[dict] = None) -> dict[str, Any]:
|
|
120
|
+
params: dict[str, Any] = {"per_page": per_page}
|
|
121
|
+
if q:
|
|
122
|
+
params["q"] = q
|
|
123
|
+
if fit:
|
|
124
|
+
params["fit"] = fit
|
|
125
|
+
if sort:
|
|
126
|
+
params["sort"] = sort
|
|
127
|
+
if extra:
|
|
128
|
+
params.update(extra)
|
|
129
|
+
return self._get("/models", params=params)
|
|
130
|
+
|
|
131
|
+
def get_model(self, slug: str) -> dict[str, Any]:
|
|
132
|
+
return self._get(f"/models/{slug}")
|
|
133
|
+
|
|
134
|
+
def get_manifest(self, info_hash: str) -> dict[str, Any]:
|
|
135
|
+
return self._get(f"/manifests/{info_hash}")
|
|
136
|
+
|
|
137
|
+
def get_fit(self, params: dict) -> dict[str, Any]:
|
|
138
|
+
return self._get("/fit", params=params)
|