Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/backend/base/langflow/api/utils/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def remove_api_keys(flow: dict):
node_data = node.get("data").get("node")
template = node_data.get("template")
for value in template.values():
if isinstance(value, dict) and has_api_terms(value["name"]) and value.get("password"):
if isinstance(value, dict) and "name" in value and has_api_terms(value["name"]) and value.get("password"):
value["value"] = None

return flow
Expand Down
8 changes: 5 additions & 3 deletions src/backend/base/langflow/services/deps.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, Union
from typing import TYPE_CHECKING

from langflow.services.schema import ServiceType

Expand All @@ -23,11 +23,11 @@
# These imports MUST be outside TYPE_CHECKING because FastAPI uses eval_str=True
# to evaluate type annotations, and these types are used as return types for
# dependency functions that FastAPI evaluates at module load time.
from lfx.services.settings.service import SettingsService

Check failure on line 26 in src/backend/base/langflow/services/deps.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (TC002)

src/backend/base/langflow/services/deps.py:26:43: TC002 Move third-party import `lfx.services.settings.service.SettingsService` into a type-checking block

from langflow.services.job_queue.service import JobQueueService

Check failure on line 28 in src/backend/base/langflow/services/deps.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (TC001)

src/backend/base/langflow/services/deps.py:28:49: TC001 Move application import `langflow.services.job_queue.service.JobQueueService` into a type-checking block
from langflow.services.storage.service import StorageService

Check failure on line 29 in src/backend/base/langflow/services/deps.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (TC001)

src/backend/base/langflow/services/deps.py:29:47: TC001 Move application import `langflow.services.storage.service.StorageService` into a type-checking block
from langflow.services.telemetry.service import TelemetryService

Check failure on line 30 in src/backend/base/langflow/services/deps.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (TC001)

src/backend/base/langflow/services/deps.py:30:49: TC001 Move application import `langflow.services.telemetry.service.TelemetryService` into a type-checking block


def get_service(service_type: ServiceType, default=None):
Expand Down Expand Up @@ -176,15 +176,17 @@
yield session


def get_cache_service() -> Union[CacheService, AsyncBaseCacheService]:
def get_cache_service() -> CacheService | AsyncBaseCacheService:
"""Retrieves the cache service from the service manager.

Returns:
The cache service instance.
"""
from langflow.services.cache.factory import CacheServiceFactory

return get_service(ServiceType.CACHE_SERVICE, CacheServiceFactory())
if not hasattr(get_cache_service, "_cache_service_factory"):
get_cache_service._cache_service_factory = CacheServiceFactory()

Check failure on line 188 in src/backend/base/langflow/services/deps.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (SLF001)

src/backend/base/langflow/services/deps.py:188:9: SLF001 Private member accessed: `_cache_service_factory`
return get_service(ServiceType.CACHE_SERVICE, get_cache_service._cache_service_factory)

Check failure on line 189 in src/backend/base/langflow/services/deps.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (SLF001)

src/backend/base/langflow/services/deps.py:189:51: SLF001 Private member accessed: `_cache_service_factory`


def get_shared_component_cache_service() -> CacheService:
Expand Down
83 changes: 83 additions & 0 deletions src/backend/tests/unit/api/v1/test_projects.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import io
import json
import zipfile
from unittest.mock import MagicMock, patch
from uuid import uuid4

import pytest
from fastapi import status
from httpx import AsyncClient
from langflow.initial_setup.constants import STARTER_FOLDER_NAME
from langflow.services.database.models.flow.model import Flow, FlowCreate
from langflow.services.deps import session_scope

CYRILLIC_NAME = "Новый проект"
CYRILLIC_DESC = "Описание проекта с кириллицей" # noqa: RUF001
Expand Down Expand Up @@ -1538,3 +1544,80 @@ async def test_read_project_error_handling_consistency(self, client: AsyncClient
assert "not found" in result["detail"].lower(), (
f"Error message should mention 'not found' for params: {params}"
)


async def test_download_file_starter_project(client: AsyncClient, logged_in_headers, active_user, json_flow):
"""Test downloading a starter project with 3 starter flows."""
# Create a starter project for the user (since download_file requires user ownership)
project_payload = {
"name": STARTER_FOLDER_NAME,
"description": "Starter projects to help you get started in Langflow.",
"flows_list": [],
"components_list": [],
}
create_response = await client.post("api/v1/projects/", json=project_payload, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
starter_project = create_response.json()
starter_project_id = starter_project["id"]

# Create 3 starter flows in the starter project
flow_data = json.loads(json_flow)
flows_created = []
async with session_scope() as session:
for i in range(3):
flow_create = FlowCreate(
name=f"Starter Flow {i + 1}",
description=f"Test starter flow {i + 1}",
data=flow_data.get("data", {}),
folder_id=starter_project_id,
user_id=active_user.id,
)
flow = Flow.model_validate(flow_create, from_attributes=True)
session.add(flow)
flows_created.append(flow)

await session.flush()
# Refresh to get IDs
for flow in flows_created:
await session.refresh(flow)
await session.commit()

# Download the starter project
response = await client.get(
f"api/v1/projects/download/{starter_project_id}",
headers=logged_in_headers,
)

# Verify response
assert response.status_code == status.HTTP_200_OK, response.text
assert response.headers["Content-Type"] == "application/x-zip-compressed"
assert "attachment" in response.headers["Content-Disposition"]
assert "filename" in response.headers["Content-Disposition"]
# The filename is URL-encoded in the header, so check for the project name
content_disposition = response.headers["Content-Disposition"]
assert (
STARTER_FOLDER_NAME.replace(" ", "%20") in content_disposition
or STARTER_FOLDER_NAME.replace(" ", "_") in content_disposition
)

# Verify zip file contents
zip_content = response.content
with zipfile.ZipFile(io.BytesIO(zip_content), "r") as zip_file:
file_names = zip_file.namelist()
# Should have 3 flow files
assert len(file_names) == 3, f"Expected 3 files in zip, got {len(file_names)}: {file_names}"

# Verify each flow file exists and contains valid JSON
for i in range(3):
expected_filename = f"Starter Flow {i + 1}.json"
assert expected_filename in file_names, f"Expected {expected_filename} in zip file"

# Read and verify flow content
flow_content = zip_file.read(expected_filename)
flow_json = json.loads(flow_content)
assert flow_json["name"] == f"Starter Flow {i + 1}"
assert flow_json["description"] == f"Test starter flow {i + 1}"

# Clean up: delete the project (which will cascade delete flows)
delete_response = await client.delete(f"api/v1/projects/{starter_project_id}", headers=logged_in_headers)
assert delete_response.status_code == status.HTTP_204_NO_CONTENT
2 changes: 1 addition & 1 deletion src/lfx/src/lfx/_assets/component_index.json

Large diffs are not rendered by default.

Loading