Skip to content

Commit 0c4bf58

Browse files
authored
Alow upload of datasets and graphs to the cloud service (#544)
* Alow upload of datasets and graphs to the cloud service Signed-off-by: Luke Hinds <lukehinds@gmail.com> * Fix tests for upload url missing dataset path Signed-off-by: Luke Hinds <lukehinds@gmail.com> --------- Signed-off-by: Luke Hinds <lukehinds@gmail.com>
1 parent 819b430 commit 0c4bf58

4 files changed

Lines changed: 250 additions & 3 deletions

File tree

deepfabric/cli.py

Lines changed: 220 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,224 @@ def upload_kaggle(
745745
sys.exit(1)
746746

747747

748+
# DeepFabric Cloud upload command group
749+
@click.group()
750+
def upload() -> None:
751+
"""Upload datasets and graphs to DeepFabric Cloud."""
752+
pass
753+
754+
755+
def _upload_to_cloud(
756+
file: str,
757+
resource_type: Literal["dataset", "graph"],
758+
handle: str | None,
759+
name: str | None,
760+
description: str | None,
761+
tags: list[str] | None,
762+
config_file: str | None,
763+
) -> None:
764+
"""Shared helper for uploading datasets and graphs to DeepFabric Cloud.
765+
766+
Args:
767+
file: Path to the file to upload
768+
resource_type: Either "dataset" or "graph"
769+
handle: Resource handle (e.g., username/resource-name)
770+
name: Display name for the resource
771+
description: Description for the resource
772+
tags: Tags for the resource (only used for datasets)
773+
config_file: Path to config file with upload settings
774+
"""
775+
# Lazy imports to avoid slow startup
776+
import httpx # noqa: PLC0415
777+
778+
from .auth import DEFAULT_API_URL # noqa: PLC0415
779+
from .cloud_upload import ( # noqa: PLC0415
780+
_get_user_friendly_error,
781+
build_urls,
782+
derive_frontend_url,
783+
derive_name_and_slug,
784+
ensure_authenticated,
785+
get_current_user,
786+
upload_dataset,
787+
upload_topic_graph,
788+
)
789+
790+
tui = get_tui()
791+
config_key = resource_type # "dataset" or "graph"
792+
url_resource_type = "datasets" if resource_type == "dataset" else "graphs"
793+
794+
# Load handle from config if not provided via CLI
795+
final_handle = handle
796+
final_description = description or ""
797+
final_tags = list(tags) if tags else []
798+
799+
if config_file:
800+
config = DeepFabricConfig.from_yaml(config_file)
801+
cloud_config = config.get_deepfabric_cloud_config()
802+
if not final_handle:
803+
final_handle = cloud_config.get(config_key)
804+
if not description and cloud_config.get("description"):
805+
final_description = cloud_config.get("description", "")
806+
if resource_type == "dataset" and not tags and cloud_config.get("tags"):
807+
final_tags = cloud_config.get("tags", [])
808+
809+
# Ensure authenticated
810+
if not ensure_authenticated(DEFAULT_API_URL, headless=False):
811+
tui.error("Authentication required. Run 'deepfabric auth login' first.")
812+
sys.exit(1)
813+
814+
# Derive name and slug from filename if not provided
815+
default_name, default_slug = derive_name_and_slug(file)
816+
final_name = name or default_name
817+
818+
# Use slug from handle if provided, otherwise use derived slug
819+
if final_handle and "/" in final_handle:
820+
final_slug = final_handle.split("/")[-1]
821+
else:
822+
final_slug = final_handle or default_slug
823+
824+
tui.info(f"Uploading {resource_type} '{final_name}'...")
825+
826+
try:
827+
# Call the appropriate upload function
828+
if resource_type == "dataset":
829+
result = upload_dataset(
830+
dataset_path=file,
831+
name=final_name,
832+
slug=final_slug,
833+
description=final_description,
834+
tags=final_tags,
835+
api_url=DEFAULT_API_URL,
836+
)
837+
resource_id = result.get("dataset_id") or result.get("id")
838+
else:
839+
result = upload_topic_graph(
840+
graph_path=file,
841+
name=final_name,
842+
description=final_description,
843+
slug=final_slug,
844+
api_url=DEFAULT_API_URL,
845+
)
846+
resource_id = result.get("id")
847+
848+
# Display success message
849+
tui.success(f"{resource_type.capitalize()} '{final_name}' uploaded successfully!")
850+
851+
# Display URL if available
852+
if resource_id:
853+
user_info = get_current_user(DEFAULT_API_URL)
854+
username = user_info.get("username") if user_info else None
855+
frontend_url = derive_frontend_url(DEFAULT_API_URL)
856+
public_url, internal_url = build_urls(
857+
url_resource_type, resource_id, final_slug, username, frontend_url
858+
)
859+
tui.info(f"View at: {public_url or internal_url}")
860+
861+
except httpx.HTTPStatusError as e:
862+
error_msg = _get_user_friendly_error(e)
863+
if "already exists" in error_msg.lower():
864+
tui.error(
865+
f"A {resource_type} with slug '{final_slug}' already exists. "
866+
"Use a different --handle value."
867+
)
868+
else:
869+
tui.error(f"Error uploading {resource_type}: {error_msg}")
870+
sys.exit(1)
871+
except Exception as e:
872+
tui.error(f"Error uploading {resource_type}: {str(e)}")
873+
sys.exit(1)
874+
875+
876+
@upload.command("dataset")
877+
@click.argument("file", type=click.Path(exists=True))
878+
@click.option("--handle", help="Dataset handle (e.g., username/dataset-name)")
879+
@click.option("--name", help="Display name for the dataset")
880+
@click.option("--description", help="Description for the dataset")
881+
@click.option(
882+
"--tags", multiple=True, help="Tags for the dataset (can be specified multiple times)"
883+
)
884+
@click.option(
885+
"--config",
886+
"config_file",
887+
type=click.Path(exists=True),
888+
help="Config file with upload settings",
889+
)
890+
def upload_dataset_cmd(
891+
file: str,
892+
handle: str | None,
893+
name: str | None,
894+
description: str | None,
895+
tags: tuple[str, ...],
896+
config_file: str | None,
897+
) -> None:
898+
"""Upload a dataset to DeepFabric Cloud.
899+
900+
FILE is the path to the JSONL dataset file.
901+
902+
Examples:
903+
904+
deepfabric upload dataset my-dataset.jsonl --handle myuser/my-dataset
905+
906+
deepfabric upload dataset output.jsonl --config config.yaml
907+
"""
908+
trace(
909+
"cli_upload_dataset",
910+
{"has_config": config_file is not None, "has_handle": handle is not None},
911+
)
912+
_upload_to_cloud(
913+
file=file,
914+
resource_type="dataset",
915+
handle=handle,
916+
name=name,
917+
description=description,
918+
tags=list(tags) if tags else None,
919+
config_file=config_file,
920+
)
921+
922+
923+
@upload.command("graph")
924+
@click.argument("file", type=click.Path(exists=True))
925+
@click.option("--handle", help="Graph handle (e.g., username/graph-name)")
926+
@click.option("--name", help="Display name for the graph")
927+
@click.option("--description", help="Description for the graph")
928+
@click.option(
929+
"--config",
930+
"config_file",
931+
type=click.Path(exists=True),
932+
help="Config file with upload settings",
933+
)
934+
def upload_graph_cmd(
935+
file: str,
936+
handle: str | None,
937+
name: str | None,
938+
description: str | None,
939+
config_file: str | None,
940+
) -> None:
941+
"""Upload a topic graph to DeepFabric Cloud.
942+
943+
FILE is the path to the JSON graph file.
944+
945+
Examples:
946+
947+
deepfabric upload graph topic_graph.json --handle myuser/my-graph
948+
949+
deepfabric upload graph graph.json --config config.yaml
950+
"""
951+
trace(
952+
"cli_upload_graph",
953+
{"has_config": config_file is not None, "has_handle": handle is not None},
954+
)
955+
_upload_to_cloud(
956+
file=file,
957+
resource_type="graph",
958+
handle=handle,
959+
name=name,
960+
description=description,
961+
tags=None,
962+
config_file=config_file,
963+
)
964+
965+
748966
@cli.command()
749967
@click.argument("graph_file", type=click.Path(exists=True))
750968
@click.option(
@@ -1152,10 +1370,11 @@ def evaluate(
11521370
handle_error(click.get_current_context(), e)
11531371

11541372

1155-
# Register the auth command group
1373+
# Register the auth and upload command groups
11561374
# EXPERIMENTAL: Only enable cloud features if explicitly opted in
11571375
if get_bool_env("EXPERIMENTAL_DF"):
11581376
cli.add_command(auth_group)
1377+
cli.add_command(upload)
11591378

11601379

11611380
@cli.command("import-tools")

deepfabric/cloud_upload.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ def build_urls(
359359

360360
if username:
361361
if resource_type == "datasets":
362-
public_url = f"{frontend_url}/{username}/{slug}"
362+
public_url = f"{frontend_url}/dataset/{username}/{slug}"
363363
else:
364364
public_url = f"{frontend_url}/graphs/{username}/{slug}"
365365
else:

deepfabric/config.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,27 @@ class KaggleConfig(BaseModel):
345345
version_notes: str | None = Field(None, description="Version notes for dataset update")
346346

347347

348+
class DeepFabricCloudConfig(BaseModel):
349+
"""Configuration for DeepFabric Cloud integration."""
350+
351+
graph: str | None = Field(
352+
default=None,
353+
description="DeepFabric Cloud graph handle (e.g., username/graph-name)",
354+
)
355+
dataset: str | None = Field(
356+
default=None,
357+
description="DeepFabric Cloud dataset handle (e.g., username/dataset-name)",
358+
)
359+
description: str | None = Field(
360+
default=None,
361+
description="Description for uploaded resources",
362+
)
363+
tags: list[str] = Field(
364+
default_factory=list,
365+
description="Tags for uploaded resources",
366+
)
367+
368+
348369
class EvaluationConfig(BaseModel):
349370
"""Configuration for model evaluation."""
350371

@@ -456,6 +477,9 @@ class DeepFabricConfig(BaseModel):
456477
evaluation: EvaluationConfig | None = Field(None, description="Evaluation configuration")
457478
huggingface: HuggingFaceConfig | None = Field(None, description="Hugging Face configuration")
458479
kaggle: KaggleConfig | None = Field(None, description="Kaggle configuration")
480+
deepfabric_cloud: DeepFabricCloudConfig | None = Field(
481+
None, description="DeepFabric Cloud configuration"
482+
)
459483

460484
@classmethod
461485
def _detect_old_format(cls, config_dict: dict) -> bool:
@@ -662,6 +686,10 @@ def get_kaggle_config(self) -> dict:
662686
"""Get Kaggle configuration."""
663687
return self.kaggle.model_dump() if self.kaggle else {}
664688

689+
def get_deepfabric_cloud_config(self) -> dict:
690+
"""Get DeepFabric Cloud configuration."""
691+
return self.deepfabric_cloud.model_dump() if self.deepfabric_cloud else {}
692+
665693
def get_configured_providers(self) -> set[str]:
666694
"""Get the set of LLM providers configured in this config."""
667695
providers = set()

tests/unit/test_cloud_upload.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def test_dataset_with_username(self):
6969
username="testuser",
7070
frontend_url="https://example.deepfabric.null",
7171
)
72-
assert public_url == "https://example.deepfabric.null/testuser/my-dataset"
72+
assert public_url == "https://example.deepfabric.null/dataset/testuser/my-dataset"
7373
assert internal_url == "https://example.deepfabric.null/datasets/abc123"
7474

7575
def test_dataset_without_username(self):

0 commit comments

Comments
 (0)