Skip to content

Commit 9d26a5d

Browse files
authored
Merge pull request #1111 from transformerlab/add/fsspec-async
Fsspec async
2 parents b1ff7f6 + aeaa31a commit 9d26a5d

169 files changed

Lines changed: 3771 additions & 2997 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/pytest-sdk.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,5 @@ jobs:
4141
uv pip install -e .
4242
4343
- name: Run SDK tests (uv)
44-
env:
45-
PYTEST_DISABLE_PLUGIN_AUTOLOAD: "1"
4644
run: |
4745
uv run pytest

.github/workflows/pytest.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ jobs:
5151
- name: Test with pytest
5252
run: |
5353
pytest --cov=transformerlab --cov-branch --cov-report=xml -k 'not test_teams'
54-
5554
- name: Upload results to Codecov
5655
uses: codecov/codecov-action@v5
5756
with:

api/api.py

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -104,28 +104,33 @@
104104
# used internally to set constants that are shared between separate processes. They are not meant to be
105105
# to be overriden by the user.
106106
os.environ["_TFL_SOURCE_CODE_DIR"] = dirs.TFL_SOURCE_CODE_DIR
107-
# The temporary image directory for transformerlab (default; per-request overrides computed in routes)
108-
temp_image_dir = storage.join(get_workspace_dir(), "temp", "images")
109-
os.environ["TLAB_TEMP_IMAGE_DIR"] = str(temp_image_dir)
110107

111108

112109
@asynccontextmanager
113110
async def lifespan(app: FastAPI):
114111
"""Docs on lifespan events: https://fastapi.tiangolo.com/advanced/events/"""
115112
# Do the following at API Startup:
116113
print_launch_message()
114+
# Initialize directories early
115+
from transformerlab.shared import dirs as shared_dirs
116+
117+
await shared_dirs.initialize_dirs()
118+
119+
# Set the temporary image directory for transformerlab (computed async)
120+
temp_image_dir = storage.join(await get_workspace_dir(), "temp", "images")
121+
os.environ["TLAB_TEMP_IMAGE_DIR"] = str(temp_image_dir)
117122
# Validate cloud credentials early - fail fast if missing
118123
validate_cloud_credentials()
119-
galleries.update_gallery_cache()
124+
await galleries.update_gallery_cache()
120125
spawn_fastchat_controller_subprocess()
121126
await db.init() # This now runs Alembic migrations internally
122127
print("✅ SEED DATA")
123128
# Initialize experiments
124-
seed_default_experiments()
129+
await seed_default_experiments()
125130
# Seed default admin user
126131
await seed_default_admin_user()
127132
# Cancel any running jobs
128-
cancel_in_progress_jobs()
133+
await cancel_in_progress_jobs()
129134

130135
# Create buckets for all existing teams if TFL_API_STORAGE_URI is enabled
131136
if os.getenv("TFL_API_STORAGE_URI"):
@@ -362,7 +367,7 @@ async def server_worker_start(
362367
# then we check to see if we are an experiment
363368
elif experiment_id is not None:
364369
try:
365-
experiment = experiment_get(experiment_id)
370+
experiment = await experiment_get(experiment_id)
366371
experiment_config = (
367372
experiment["config"]
368373
if isinstance(experiment["config"], dict)
@@ -394,15 +399,15 @@ async def server_worker_start(
394399
model_architecture = model_architecture
395400

396401
plugin_name = inference_engine
397-
plugin_location = lab_dirs.plugin_dir_by_name(plugin_name)
402+
plugin_location = await lab_dirs.plugin_dir_by_name(plugin_name)
398403

399404
model = model_name
400405
if model_filename is not None and model_filename != "":
401406
model = model_filename
402407

403408
if adaptor != "":
404409
# Resolve per-request workspace if multitenant
405-
workspace_dir = get_workspace_dir()
410+
workspace_dir = await get_workspace_dir()
406411
adaptor = f"{workspace_dir}/adaptors/{secure_filename(model)}/{adaptor}"
407412

408413
params = [
@@ -419,14 +424,14 @@ async def server_worker_start(
419424
json.dumps(inference_params),
420425
]
421426

422-
job_id = job_create(type="LOAD_MODEL", status="STARTED", job_data="{}", experiment_id=experiment_id)
427+
job_id = await job_create(type="LOAD_MODEL", status="STARTED", job_data="{}", experiment_id=experiment_id)
423428

424429
print("Loading plugin loader instead of default worker")
425430

426431
from lab.dirs import get_global_log_path
427432

428-
with storage.open(get_global_log_path(), "a") as global_log:
429-
global_log.write(f"🏃 Loading Inference Server for {model_name} with {inference_params}\n")
433+
async with await storage.open(await get_global_log_path(), "a") as global_log:
434+
await global_log.write(f"🏃 Loading Inference Server for {model_name} with {inference_params}\n")
430435

431436
# Pass organization_id as environment variable to subprocess
432437
from transformerlab.shared.request_context import get_current_org_id
@@ -447,8 +452,8 @@ async def server_worker_start(
447452
if exitcode == 99:
448453
from lab.dirs import get_global_log_path
449454

450-
with storage.open(get_global_log_path(), "a") as global_log:
451-
global_log.write(
455+
async with await storage.open(await get_global_log_path(), "a") as global_log:
456+
await global_log.write(
452457
"GPU (CUDA) Out of Memory: Please try a smaller model or a different inference engine. Restarting the server may free up resources.\n"
453458
)
454459
return {
@@ -458,20 +463,20 @@ async def server_worker_start(
458463
if exitcode is not None and exitcode != 0:
459464
from lab.dirs import get_global_log_path
460465

461-
with storage.open(get_global_log_path(), "a") as global_log:
462-
global_log.write(f"Error loading model: {model_name} with exit code {exitcode}\n")
463-
job = job_get(job_id)
466+
async with await storage.open(await get_global_log_path(), "a") as global_log:
467+
await global_log.write(f"Error loading model: {model_name} with exit code {exitcode}\n")
468+
job = await job_get(job_id)
464469
error_msg = None
465470
if job and job.get("job_data"):
466471
error_msg = job["job_data"].get("error_msg")
467472
if not error_msg:
468473
error_msg = f"Exit code {exitcode}"
469-
job_update_status(job_id, "FAILED", experiment_id=experiment_id, error_msg=error_msg)
474+
await job_update_status(job_id, "FAILED", experiment_id=experiment_id, error_msg=error_msg)
470475
return {"status": "error", "message": error_msg}
471476
from lab.dirs import get_global_log_path
472477

473-
with storage.open(get_global_log_path(), "a") as global_log:
474-
global_log.write(f"Model loaded successfully: {model_name}\n")
478+
async with await storage.open(await get_global_log_path(), "a") as global_log:
479+
await global_log.write(f"Model loaded successfully: {model_name}\n")
475480
return {"status": "success", "job_id": job_id}
476481

477482

@@ -630,7 +635,9 @@ def run():
630635
)
631636

632637
if args.https:
633-
cert_path, key_path = ensure_persistent_self_signed_cert()
638+
import asyncio
639+
640+
cert_path, key_path = asyncio.run(ensure_persistent_self_signed_cert())
634641
uvicorn.run(
635642
"api:app", host=args.host, port=args.port, log_level="warning", ssl_certfile=cert_path, ssl_keyfile=key_path
636643
)

api/pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ dependencies = [
3333
"soundfile==0.13.1",
3434
"tensorboardX==2.6.2.2",
3535
"timm==1.0.15",
36-
"transformerlab==0.0.58",
37-
"transformerlab-inference==0.2.51",
36+
"transformerlab==0.0.62",
37+
"transformerlab-inference==0.2.52",
3838
"transformers==4.57.1",
3939
"wandb==0.19.10",
4040
"werkzeug==3.1.3",
@@ -109,4 +109,4 @@ cpu = [
109109
"tensorboard==2.18.0",
110110
"tiktoken==0.8.0",
111111
"watchfiles==1.0.4",
112-
]
112+
]

api/test/api/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
# Create test directories before setting environment variables
77
os.makedirs("test/tmp/", exist_ok=True)
8+
os.makedirs("test/tmp/webapp", exist_ok=True) # Create webapp directory for static files
89

910
os.environ["TFL_HOME_DIR"] = "test/tmp/"
1011
# Note: TFL_WORKSPACE_DIR is not set so that get_workspace_dir() will use the org-based

api/test/api/test_data.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22
import os
33
import json
4+
import asyncio
45
from io import BytesIO
56
from PIL import Image
67
from pathlib import Path
@@ -13,7 +14,7 @@ def cleanup_dataset(dataset_id, client):
1314
from transformerlab.shared.shared import slugify
1415
import shutil
1516

16-
dataset_dir = dirs.dataset_dir_by_id(slugify(dataset_id))
17+
dataset_dir = asyncio.run(dirs.dataset_dir_by_id(slugify(dataset_id)))
1718
shutil.rmtree(dataset_dir, ignore_errors=True)
1819
client.get(f"/data/delete?dataset_id={dataset_id}")
1920

@@ -54,7 +55,7 @@ def test_data_info(client):
5455
def test_save_metadata(client):
5556
source_dataset_id = "source_dataset"
5657
new_dataset_id = "destination_dataset"
57-
dataset_dir = dirs.dataset_dir_by_id(slugify(source_dataset_id))
58+
dataset_dir = asyncio.run(dirs.dataset_dir_by_id(slugify(source_dataset_id)))
5859
os.makedirs(dataset_dir, exist_ok=True)
5960

6061
# Create dummy JPEG image
@@ -96,7 +97,7 @@ def test_save_metadata(client):
9697
data = response.json()
9798
assert data["status"] == "success"
9899

99-
new_dataset_dir = Path(dirs.dataset_dir_by_id(slugify(new_dataset_id)))
100+
new_dataset_dir = Path(asyncio.run(dirs.dataset_dir_by_id(slugify(new_dataset_id))))
100101
assert new_dataset_dir.exists()
101102

102103
cleanup_dataset(source_dataset_id, client)
@@ -106,7 +107,7 @@ def test_save_metadata(client):
106107
@pytest.mark.skip(reason="Skipping as it contains application-specific logic")
107108
def test_edit_with_template(client):
108109
dataset_id = "test_dataset"
109-
dataset_dir = dirs.dataset_dir_by_id(slugify(dataset_id))
110+
dataset_dir = asyncio.run(dirs.dataset_dir_by_id(slugify(dataset_id)))
110111
os.makedirs(dataset_dir, exist_ok=True)
111112

112113
image_path = os.path.join(dataset_dir, "image.jpg")

api/test/api/test_dataset_service.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ def tmp_dataset_dir(tmp_path: Path) -> Path:
1616
return tmp_path
1717

1818

19-
def test_load_local_dataset_filters_index_and_hidden(tmp_dataset_dir: Path, monkeypatch):
19+
@pytest.mark.asyncio
20+
async def test_load_local_dataset_filters_index_and_hidden(tmp_dataset_dir: Path, monkeypatch):
2021
# Import inside test to ensure module path resolution for monkeypatching
2122
from transformerlab.services import dataset_service
2223

@@ -30,7 +31,7 @@ def fake_load_dataset(path=None, data_files=None, streaming=False):
3031

3132
monkeypatch.setattr(dataset_service, "load_dataset", fake_load_dataset)
3233

33-
result = dataset_service.load_local_dataset(str(tmp_dataset_dir))
34+
result = await dataset_service.load_local_dataset(str(tmp_dataset_dir))
3435

3536
assert result == {"ok": True}
3637
assert captured["path"] == str(tmp_dataset_dir)
@@ -43,7 +44,8 @@ def fake_load_dataset(path=None, data_files=None, streaming=False):
4344
assert captured["streaming"] is False
4445

4546

46-
def test_load_local_dataset_uses_explicit_data_files(tmp_path: Path, monkeypatch):
47+
@pytest.mark.asyncio
48+
async def test_load_local_dataset_uses_explicit_data_files(tmp_path: Path, monkeypatch):
4749
from transformerlab.services import dataset_service
4850

4951
# Explicit files provided (note: function should not re-filter these)
@@ -60,7 +62,9 @@ def fake_load_dataset(path=None, data_files=None, streaming=False):
6062

6163
monkeypatch.setattr(dataset_service, "load_dataset", fake_load_dataset)
6264

63-
result = dataset_service.load_local_dataset(str(tmp_path), data_files=["keep.me", "index.json"], streaming=True)
65+
result = await dataset_service.load_local_dataset(
66+
str(tmp_path), data_files=["keep.me", "index.json"], streaming=True
67+
)
6468

6569
assert result == {"ok": True}
6670
assert captured["path"] == str(tmp_path)
@@ -72,7 +76,8 @@ def fake_load_dataset(path=None, data_files=None, streaming=False):
7276
assert captured["streaming"] is True
7377

7478

75-
def test_load_local_dataset_fallback_when_no_valid_files(tmp_path: Path, monkeypatch):
79+
@pytest.mark.asyncio
80+
async def test_load_local_dataset_fallback_when_no_valid_files(tmp_path: Path, monkeypatch):
7681
from transformerlab.services import dataset_service
7782

7883
# Only metadata/hidden files present
@@ -89,7 +94,7 @@ def fake_load_dataset(path=None, data_files=None, streaming=False):
8994

9095
monkeypatch.setattr(dataset_service, "load_dataset", fake_load_dataset)
9196

92-
result = dataset_service.load_local_dataset(str(tmp_path))
97+
result = await dataset_service.load_local_dataset(str(tmp_path))
9398

9499
assert result == {"ok": True}
95100
assert captured["path"] == str(tmp_path)

0 commit comments

Comments
 (0)