Skip to content

Commit 526248a

Browse files
authored
set_handlers: models_to_fetch direct links support (#218)
Closes: #217 --------- Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
1 parent 6d88c6e commit 526248a

File tree

5 files changed

+112
-20
lines changed

5 files changed

+112
-20
lines changed

.github/workflows/analysis-coverage.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ jobs:
4949
- nextcloud: "27.1.4"
5050
python: "3.10"
5151
php-version: "8.1"
52+
timeout-minutes: 60
5253

5354
services:
5455
mariadb:
@@ -209,6 +210,7 @@ jobs:
209210
php-version: "8.2"
210211
env:
211212
NC_dbname: nextcloud_abz
213+
timeout-minutes: 60
212214

213215
services:
214216
postgres:
@@ -361,6 +363,7 @@ jobs:
361363
needs: [analysis]
362364
runs-on: ubuntu-22.04
363365
name: stable27 • 🐘8.1 • 🐍3.11 • OCI
366+
timeout-minutes: 60
364367

365368
services:
366369
oracle:
@@ -483,6 +486,7 @@ jobs:
483486
fail-fast: false
484487
matrix:
485488
nextcloud: [ 'stable27', 'stable28', 'master' ]
489+
timeout-minutes: 60
486490

487491
services:
488492
mariadb:
@@ -661,6 +665,7 @@ jobs:
661665
nextcloud: [ 'stable27', 'stable28', 'master' ]
662666
env:
663667
NC_dbname: nextcloud_abz
668+
timeout-minutes: 60
664669

665670
services:
666671
postgres:
@@ -841,6 +846,7 @@ jobs:
841846
nextcloud: [ 'stable26', 'stable27', 'stable28', 'master' ]
842847
env:
843848
NEXTCLOUD_URL: "http://localhost:8080/index.php"
849+
timeout-minutes: 60
844850

845851
steps:
846852
- name: Set up php

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.10.0 - 2024-02-0x]
6+
7+
### Added
8+
9+
- set_handlers: `models_to_fetch` can now accept direct links to a files to download. #217
10+
11+
### Changed
12+
13+
- adjusted code related to changes in AppAPI `2.0.3` #216
14+
515
## [0.9.0 - 2024-01-25]
616

717
### Added

nc_py_api/ex_app/integration_fastapi.py

Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
"""FastAPI directly related stuff."""
22

33
import asyncio
4+
import builtins
5+
import hashlib
46
import json
57
import os
68
import typing
9+
from urllib.parse import urlparse
710

11+
import httpx
812
from fastapi import (
913
BackgroundTasks,
1014
Depends,
@@ -20,6 +24,7 @@
2024
from .._misc import get_username_secret_from_headers
2125
from ..nextcloud import AsyncNextcloudApp, NextcloudApp
2226
from ..talk_bot import TalkBotMessage
27+
from .defs import LogLvl
2328
from .misc import persistent_storage
2429

2530

@@ -163,26 +168,79 @@ def __map_app_static_folders(fast_api_app: FastAPI):
163168
fast_api_app.mount(f"/{mnt_dir}", staticfiles.StaticFiles(directory=mnt_dir_path), name=mnt_dir)
164169

165170

166-
def __fetch_models_task(
167-
nc: NextcloudApp,
168-
models: dict[str, dict],
169-
) -> None:
171+
def __fetch_models_task(nc: NextcloudApp, models: dict[str, dict]) -> None:
170172
if models:
171-
from huggingface_hub import snapshot_download # noqa isort:skip pylint: disable=C0415 disable=E0401
172-
from tqdm import tqdm # noqa isort:skip pylint: disable=C0415 disable=E0401
173-
174-
class TqdmProgress(tqdm):
175-
def display(self, msg=None, pos=None):
176-
nc.set_init_status(min(int((self.n * 100 / self.total) / len(models)), 100))
177-
return super().display(msg, pos)
178-
173+
current_progress = 0
174+
percent_for_each = min(int(100 / len(models)), 99)
179175
for model in models:
180-
workers = models[model].pop("max_workers", 2)
181-
cache = models[model].pop("cache_dir", persistent_storage())
182-
snapshot_download(model, tqdm_class=TqdmProgress, **models[model], max_workers=workers, cache_dir=cache)
176+
if model.startswith(("http://", "https://")):
177+
__fetch_model_as_file(current_progress, percent_for_each, nc, model, models[model])
178+
else:
179+
__fetch_model_as_snapshot(current_progress, percent_for_each, nc, model, models[model])
180+
current_progress += percent_for_each
183181
nc.set_init_status(100)
184182

185183

184+
def __fetch_model_as_file(
185+
current_progress: int, progress_for_task: int, nc: NextcloudApp, model_path: str, download_options: dict
186+
) -> None:
187+
result_path = download_options.pop("save_path", urlparse(model_path).path.split("/")[-1])
188+
try:
189+
with httpx.stream("GET", model_path, follow_redirects=True) as response:
190+
if not response.is_success:
191+
nc.log(LogLvl.ERROR, f"Downloading of '{model_path}' returned {response.status_code} status.")
192+
return
193+
downloaded_size = 0
194+
linked_etag = ""
195+
for each_history in response.history:
196+
linked_etag = each_history.headers.get("X-Linked-ETag", "")
197+
if linked_etag:
198+
break
199+
if not linked_etag:
200+
linked_etag = response.headers.get("X-Linked-ETag", response.headers.get("ETag", ""))
201+
total_size = int(response.headers.get("Content-Length"))
202+
try:
203+
existing_size = os.path.getsize(result_path)
204+
except OSError:
205+
existing_size = 0
206+
if linked_etag and total_size == existing_size:
207+
with builtins.open(result_path, "rb") as file:
208+
sha256_hash = hashlib.sha256()
209+
for byte_block in iter(lambda: file.read(4096), b""):
210+
sha256_hash.update(byte_block)
211+
if f'"{sha256_hash.hexdigest()}"' == linked_etag:
212+
nc.set_init_status(min(current_progress + progress_for_task, 99))
213+
return
214+
215+
with builtins.open(result_path, "wb") as file:
216+
last_progress = current_progress
217+
for chunk in response.iter_bytes(5 * 1024 * 1024):
218+
downloaded_size += file.write(chunk)
219+
if total_size:
220+
new_progress = min(current_progress + int(progress_for_task * downloaded_size / total_size), 99)
221+
if new_progress != last_progress:
222+
nc.set_init_status(new_progress)
223+
last_progress = new_progress
224+
except Exception as e: # noqa pylint: disable=broad-exception-caught
225+
nc.log(LogLvl.ERROR, f"Downloading of '{model_path}' raised an exception: {e}")
226+
227+
228+
def __fetch_model_as_snapshot(
229+
current_progress: int, progress_for_task, nc: NextcloudApp, mode_name: str, download_options: dict
230+
) -> None:
231+
from huggingface_hub import snapshot_download # noqa isort:skip pylint: disable=C0415 disable=E0401
232+
from tqdm import tqdm # noqa isort:skip pylint: disable=C0415 disable=E0401
233+
234+
class TqdmProgress(tqdm):
235+
def display(self, msg=None, pos=None):
236+
nc.set_init_status(min(current_progress + int(progress_for_task * self.n / self.total), 99))
237+
return super().display(msg, pos)
238+
239+
workers = download_options.pop("max_workers", 2)
240+
cache = download_options.pop("cache_dir", persistent_storage())
241+
snapshot_download(mode_name, tqdm_class=TqdmProgress, **download_options, max_workers=workers, cache_dir=cache)
242+
243+
186244
class AppAPIAuthMiddleware:
187245
"""Pure ASGI AppAPIAuth Middleware."""
188246

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ profile = "black"
133133
master.py-version = "3.10"
134134
master.extension-pkg-allow-list = ["pydantic"]
135135
design.max-attributes = 8
136-
design.max-locals = 16
136+
design.max-locals = 20
137137
design.max-branches = 16
138138
design.max-returns = 8
139139
design.max-args = 7

tests/_install_init_handler_models.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
11
from contextlib import asynccontextmanager
2+
from pathlib import Path
23

34
from fastapi import FastAPI
45

56
from nc_py_api import NextcloudApp, ex_app
67

7-
MODEL_NAME = "MBZUAI/LaMini-T5-61M"
8+
INVALID_URL = "https://invalid_url"
9+
MODEL_NAME1 = "MBZUAI/LaMini-T5-61M"
10+
MODEL_NAME2 = "https://huggingface.co/MBZUAI/LaMini-T5-61M/resolve/main/pytorch_model.bin"
11+
MODEL_NAME2_http = "http://huggingface.co/MBZUAI/LaMini-T5-61M/resolve/main/pytorch_model.bin"
12+
INVALID_PATH = "https://huggingface.co/invalid_path"
13+
SOME_FILE = "https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/README.md"
814

915

1016
@asynccontextmanager
1117
async def lifespan(_app: FastAPI):
12-
ex_app.set_handlers(APP, enabled_handler, models_to_fetch={MODEL_NAME: {}})
18+
ex_app.set_handlers(
19+
APP,
20+
enabled_handler,
21+
models_to_fetch={
22+
INVALID_URL: {},
23+
MODEL_NAME1: {},
24+
MODEL_NAME2: {},
25+
MODEL_NAME2_http: {},
26+
INVALID_PATH: {},
27+
SOME_FILE: {},
28+
},
29+
)
1330
yield
1431

1532

@@ -19,9 +36,10 @@ async def lifespan(_app: FastAPI):
1936
def enabled_handler(enabled: bool, _nc: NextcloudApp) -> str:
2037
if enabled:
2138
try:
22-
assert ex_app.get_model_path(MODEL_NAME)
39+
assert ex_app.get_model_path(MODEL_NAME1)
2340
except Exception: # noqa
24-
return "model not found"
41+
return "model1 not found"
42+
assert Path("pytorch_model.bin").is_file()
2543
return ""
2644

2745

0 commit comments

Comments
 (0)