diff --git a/src/backend/base/langflow/api/v1/deployments.py b/src/backend/base/langflow/api/v1/deployments.py index 268368905f..c61c3cdd70 100644 --- a/src/backend/base/langflow/api/v1/deployments.py +++ b/src/backend/base/langflow/api/v1/deployments.py @@ -17,10 +17,11 @@ from langflow.api.v1.schemas.deployments import ( DeploymentDuplicateResponse, DeploymentGetResponse, DeploymentListResponse, - DeploymentProviderAccountCreate, + DeploymentProviderAccountCreateRequest, + DeploymentProviderAccountGetResponse, DeploymentProviderAccountListResponse, - DeploymentProviderAccountResponse, - DeploymentProviderAccountUpdate, + DeploymentProviderAccountUpdateRequest, + DeploymentRedeployResponse, DeploymentStatusResponse, DeploymentTypeListResponse, DeploymentUpdateRequest, @@ -29,7 +30,6 @@ from langflow.api.v1.schemas.deployments import ( ExecutionCreateResponse, ExecutionStatusResponse, FlowVersionIdsQuery, - RedeployResponse, ) router = APIRouter(prefix="/deployments", tags=["Deployments"]) @@ -63,13 +63,13 @@ DeploymentIdPath = Annotated[ @router.post( "/providers", - response_model=DeploymentProviderAccountResponse, + response_model=DeploymentProviderAccountGetResponse, status_code=status.HTTP_201_CREATED, tags=["Deployment Providers"], ) async def create_provider_account( session: DbSession, - payload: DeploymentProviderAccountCreate, + payload: DeploymentProviderAccountCreateRequest, current_user: CurrentActiveUser, ): """Register a new deployment provider account.""" @@ -89,7 +89,7 @@ async def list_provider_accounts( @router.get( "/providers/{provider_id}", - response_model=DeploymentProviderAccountResponse, + response_model=DeploymentProviderAccountGetResponse, tags=["Deployment Providers"], ) async def get_provider_account( @@ -117,13 +117,13 @@ async def delete_provider_account( @router.patch( "/providers/{provider_id}", - response_model=DeploymentProviderAccountResponse, + response_model=DeploymentProviderAccountGetResponse, tags=["Deployment Providers"], ) async def update_provider_account( provider_id: DeploymentProviderAccountIdPath, session: DbSession, - payload: DeploymentProviderAccountUpdate, + payload: DeploymentProviderAccountUpdateRequest, current_user: CurrentActiveUser, ): """Partially update a deployment provider account.""" @@ -246,7 +246,7 @@ async def delete_deployment( @router.post( "/{deployment_id}/redeploy", - response_model=RedeployResponse, + response_model=DeploymentRedeployResponse, ) async def redeploy_deployment( deployment_id: DeploymentIdPath, diff --git a/src/backend/base/langflow/api/v1/schemas/deployments.py b/src/backend/base/langflow/api/v1/schemas/deployments.py index ffc9d2bbb4..68b4f6d8a5 100644 --- a/src/backend/base/langflow/api/v1/schemas/deployments.py +++ b/src/backend/base/langflow/api/v1/schemas/deployments.py @@ -123,7 +123,7 @@ FlowVersionIdsQuery = Annotated[list[str] | None, AfterValidator(_validate_flow_ # --------------------------------------------------------------------------- -class DeploymentProviderAccountCreate(BaseModel): +class DeploymentProviderAccountCreateRequest(BaseModel): model_config = {"extra": "forbid"} provider_tenant_id: NonEmptyStr | None = Field( @@ -147,7 +147,7 @@ class DeploymentProviderAccountCreate(BaseModel): return _normalize_str(value, field_name=info.field_name) -class DeploymentProviderAccountUpdate(BaseModel): +class DeploymentProviderAccountUpdateRequest(BaseModel): model_config = {"extra": "forbid"} provider_tenant_id: NonEmptyStr | None = Field( @@ -176,7 +176,7 @@ class DeploymentProviderAccountUpdate(BaseModel): return _normalize_optional_str(value, field_name=info.field_name) @model_validator(mode="after") - def ensure_any_field_provided(self) -> DeploymentProviderAccountUpdate: + def ensure_any_field_provided(self) -> DeploymentProviderAccountUpdateRequest: if not self.model_fields_set: msg = "At least one field must be provided for update." raise ValueError(msg) @@ -189,7 +189,7 @@ class DeploymentProviderAccountUpdate(BaseModel): return self -class DeploymentProviderAccountResponse(BaseModel): +class DeploymentProviderAccountGetResponse(BaseModel): id: UUID = Field(description="Langflow DB provider-account UUID (`deployment_provider_account.id`).") provider_tenant_id: str | None = Field( default=None, @@ -264,7 +264,7 @@ class DeploymentListResponse(_PaginatedResponse): class DeploymentProviderAccountListResponse(_PaginatedResponse): - providers: list[DeploymentProviderAccountResponse] + providers: list[DeploymentProviderAccountGetResponse] class DeploymentCreateResponse(_DeploymentResponseBase): @@ -279,7 +279,7 @@ class DeploymentStatusResponse(_DeploymentResponseBase): """API response for deployment status/health.""" -class RedeployResponse(_DeploymentResponseBase): +class DeploymentRedeployResponse(_DeploymentResponseBase): """API response for redeployment.""" diff --git a/src/backend/base/langflow/services/database/models/deployment/__init__.py b/src/backend/base/langflow/services/database/models/deployment/__init__.py index 414be26088..28dfc8267b 100644 --- a/src/backend/base/langflow/services/database/models/deployment/__init__.py +++ b/src/backend/base/langflow/services/database/models/deployment/__init__.py @@ -1,3 +1,3 @@ -from .model import Deployment, DeploymentCreate, DeploymentRead, DeploymentUpdate +from .model import Deployment, DeploymentRead -__all__ = ["Deployment", "DeploymentCreate", "DeploymentRead", "DeploymentUpdate"] +__all__ = ["Deployment", "DeploymentRead"] diff --git a/src/backend/base/langflow/services/database/models/deployment/model.py b/src/backend/base/langflow/services/database/models/deployment/model.py index ace58b8de3..7c4fb3d0ae 100644 --- a/src/backend/base/langflow/services/database/models/deployment/model.py +++ b/src/backend/base/langflow/services/database/models/deployment/model.py @@ -8,7 +8,7 @@ from sqlalchemy import ForeignKey, UniqueConstraint from sqlmodel import Column, DateTime, Field, Relationship, SQLModel, func from langflow.schema.serialize import UUIDstr -from langflow.services.database.utils import validate_non_empty_string, validate_non_empty_string_optional +from langflow.services.database.utils import validate_non_empty_string if TYPE_CHECKING: from langflow.services.database.models.deployment_provider_account.model import DeploymentProviderAccount @@ -59,28 +59,6 @@ class Deployment(SQLModel, table=True): # type: ignore[call-arg] return validate_non_empty_string(v, info) -class DeploymentCreate(SQLModel): - resource_key: str - deployment_provider_account_id: UUID - project_id: UUID - name: str - - @field_validator("name", "resource_key") - @classmethod - def validate_non_empty(cls, v: str, info: object) -> str: - return validate_non_empty_string(v, info) - - -class DeploymentUpdate(SQLModel): - name: str | None = None - project_id: UUID | None = None - - @field_validator("name", mode="before") - @classmethod - def validate_non_empty_if_provided(cls, v: str | None, info: object) -> str | None: - return validate_non_empty_string_optional(v, info) - - class DeploymentRead(SQLModel): id: UUID resource_key: str diff --git a/src/backend/base/langflow/services/database/models/deployment_provider_account/__init__.py b/src/backend/base/langflow/services/database/models/deployment_provider_account/__init__.py index 5f0a9fcac7..6b8f4157eb 100644 --- a/src/backend/base/langflow/services/database/models/deployment_provider_account/__init__.py +++ b/src/backend/base/langflow/services/database/models/deployment_provider_account/__init__.py @@ -1,13 +1,9 @@ from .model import ( DeploymentProviderAccount, - DeploymentProviderAccountCreate, DeploymentProviderAccountRead, - DeploymentProviderAccountUpdate, ) __all__ = [ "DeploymentProviderAccount", - "DeploymentProviderAccountCreate", "DeploymentProviderAccountRead", - "DeploymentProviderAccountUpdate", ] diff --git a/src/backend/base/langflow/services/database/models/deployment_provider_account/model.py b/src/backend/base/langflow/services/database/models/deployment_provider_account/model.py index 826d684e78..b5e920a976 100644 --- a/src/backend/base/langflow/services/database/models/deployment_provider_account/model.py +++ b/src/backend/base/langflow/services/database/models/deployment_provider_account/model.py @@ -11,7 +11,6 @@ from langflow.schema.serialize import UUIDstr from langflow.services.database.utils import ( normalize_string_or_none, validate_non_empty_string, - validate_non_empty_string_optional, ) if TYPE_CHECKING: @@ -43,7 +42,7 @@ class DeploymentProviderAccount(SQLModel, table=True): # type: ignore[call-arg] provider_key: str = Field(index=True) provider_url: str = Field() # MUST be stored encrypted; the CRUD layer encrypts via auth_utils before writing - # and the Read schema MUST intentionally excludes this field. + # and the Read schema intentionally excludes this field. api_key: str = Field() created_at: datetime | None = Field( default=None, @@ -71,23 +70,6 @@ class DeploymentProviderAccount(SQLModel, table=True): # type: ignore[call-arg] return validate_non_empty_string(v, info) -class DeploymentProviderAccountCreate(SQLModel): - provider_tenant_id: str | None = None - provider_key: str - provider_url: str - api_key: str - - @field_validator("provider_tenant_id", mode="before") - @classmethod - def normalize_tenant_id(cls, v: str | None) -> str | None: - return normalize_string_or_none(v) - - @field_validator("provider_key", "provider_url", "api_key") - @classmethod - def validate_non_empty(cls, v: str, info: object) -> str: - return validate_non_empty_string(v, info) - - class DeploymentProviderAccountRead(SQLModel): id: UUID user_id: UUID @@ -97,25 +79,3 @@ class DeploymentProviderAccountRead(SQLModel): created_at: datetime updated_at: datetime # api_key intentionally omitted -- stored encrypted, never serialize credentials to API responses - - -class DeploymentProviderAccountUpdate(SQLModel): - # All fields default to None. API routes consuming this schema must check - # ``model_fields_set`` to distinguish "field omitted" (keep existing value) - # from "field explicitly set to null" (clear the value). The CRUD layer's - # ``update_provider_account`` uses an ``_UNSET`` sentinel for the same - # purpose on ``provider_tenant_id``. - provider_tenant_id: str | None = None - provider_key: str | None = None - provider_url: str | None = None - api_key: str | None = None - - @field_validator("provider_tenant_id", mode="before") - @classmethod - def normalize_tenant_id(cls, v: str | None) -> str | None: - return normalize_string_or_none(v) - - @field_validator("provider_key", "provider_url", "api_key", mode="before") - @classmethod - def validate_non_empty_if_provided(cls, v: str | None, info: object) -> str | None: - return validate_non_empty_string_optional(v, info) diff --git a/src/backend/base/langflow/tests/services/database/models/deployment/test_model.py b/src/backend/base/langflow/tests/services/database/models/deployment/test_model.py index 95a1dbf348..675178838b 100644 --- a/src/backend/base/langflow/tests/services/database/models/deployment/test_model.py +++ b/src/backend/base/langflow/tests/services/database/models/deployment/test_model.py @@ -1,7 +1,7 @@ from unittest.mock import MagicMock import pytest -from langflow.services.database.models.deployment.model import Deployment, DeploymentCreate, DeploymentRead +from langflow.services.database.models.deployment.model import Deployment, DeploymentRead class TestDeploymentValidation: @@ -52,50 +52,3 @@ class TestDeploymentRead: "updated_at", } assert set(DeploymentRead.model_fields.keys()) == expected - - -class TestDeploymentCreate: - """Tests for DeploymentCreate schema.""" - - def test_rejects_empty_name(self): - from uuid import uuid4 - - with pytest.raises(ValueError, match="name must not be empty"): - DeploymentCreate( - resource_key="rk-1", - deployment_provider_account_id=uuid4(), - project_id=uuid4(), - name="", - ) - - def test_rejects_empty_resource_key(self): - from uuid import uuid4 - - with pytest.raises(ValueError, match="resource_key must not be empty"): - DeploymentCreate( - resource_key=" ", - deployment_provider_account_id=uuid4(), - project_id=uuid4(), - name="my-deploy", - ) - - def test_valid_create(self): - from uuid import uuid4 - - obj = DeploymentCreate( - resource_key="rk-1", - deployment_provider_account_id=uuid4(), - project_id=uuid4(), - name="my-deploy", - ) - assert obj.name == "my-deploy" - assert obj.resource_key == "rk-1" - - def test_has_expected_fields(self): - expected = { - "resource_key", - "deployment_provider_account_id", - "project_id", - "name", - } - assert set(DeploymentCreate.model_fields.keys()) == expected diff --git a/src/backend/base/langflow/tests/services/database/models/deployment_provider_account/test_model.py b/src/backend/base/langflow/tests/services/database/models/deployment_provider_account/test_model.py index 80a26f5a29..0500197ad6 100644 --- a/src/backend/base/langflow/tests/services/database/models/deployment_provider_account/test_model.py +++ b/src/backend/base/langflow/tests/services/database/models/deployment_provider_account/test_model.py @@ -3,9 +3,7 @@ from unittest.mock import MagicMock import pytest from langflow.services.database.models.deployment_provider_account.model import ( DeploymentProviderAccount, - DeploymentProviderAccountCreate, DeploymentProviderAccountRead, - DeploymentProviderAccountUpdate, ) @@ -79,91 +77,3 @@ class TestDeploymentProviderAccountRead: "updated_at", } assert set(DeploymentProviderAccountRead.model_fields.keys()) == expected - - -class TestDeploymentProviderAccountCreate: - """Tests for DeploymentProviderAccountCreate schema validators.""" - - def test_rejects_empty_provider_key(self): - with pytest.raises(ValueError, match="provider_key must not be empty"): - DeploymentProviderAccountCreate( - provider_key="", - provider_url="https://example.com", - api_key="key", # pragma: allowlist secret - ) - - def test_rejects_empty_provider_url(self): - with pytest.raises(ValueError, match="provider_url must not be empty"): - DeploymentProviderAccountCreate( - provider_key="watsonx", - provider_url="", - api_key="key", # pragma: allowlist secret - ) - - def test_rejects_empty_api_key(self): - with pytest.raises(ValueError, match="api_key must not be empty"): - DeploymentProviderAccountCreate(provider_key="watsonx", provider_url="https://example.com", api_key="") - - def test_valid_create(self): - obj = DeploymentProviderAccountCreate( - provider_key="watsonx", - provider_url="https://example.com", - api_key="key", # pragma: allowlist secret - ) - assert obj.provider_key == "watsonx" - - def test_blank_tenant_id_normalizes_to_none(self): - obj = DeploymentProviderAccountCreate( - provider_tenant_id=" ", - provider_key="watsonx", - provider_url="https://example.com", - api_key="key", # pragma: allowlist secret - ) - assert obj.provider_tenant_id is None - - def test_strips_tenant_id(self): - obj = DeploymentProviderAccountCreate( - provider_tenant_id=" tenant-1 ", - provider_key="watsonx", - provider_url="https://example.com", - api_key="key", # pragma: allowlist secret - ) - assert obj.provider_tenant_id == "tenant-1" - - -class TestDeploymentProviderAccountUpdate: - """Tests for DeploymentProviderAccountUpdate schema validators.""" - - def test_allows_none_values(self): - obj = DeploymentProviderAccountUpdate() - assert obj.provider_key is None - assert obj.provider_url is None - assert obj.api_key is None - - def test_rejects_empty_provider_key_when_provided(self): - with pytest.raises(ValueError, match="provider_key must not be empty"): - DeploymentProviderAccountUpdate(provider_key="") - - def test_rejects_empty_provider_url_when_provided(self): - with pytest.raises(ValueError, match="provider_url must not be empty"): - DeploymentProviderAccountUpdate(provider_url="") - - def test_rejects_empty_api_key_when_provided(self): - with pytest.raises(ValueError, match="api_key must not be empty"): - DeploymentProviderAccountUpdate(api_key="") - - def test_rejects_whitespace_provider_key_when_provided(self): - with pytest.raises(ValueError, match="provider_key must not be empty"): - DeploymentProviderAccountUpdate(provider_key=" ") - - def test_valid_update(self): - obj = DeploymentProviderAccountUpdate(provider_key="new-key") - assert obj.provider_key == "new-key" - - def test_blank_tenant_id_normalizes_to_none(self): - obj = DeploymentProviderAccountUpdate(provider_tenant_id=" ") - assert obj.provider_tenant_id is None - - def test_strips_tenant_id(self): - obj = DeploymentProviderAccountUpdate(provider_tenant_id=" tenant-1 ") - assert obj.provider_tenant_id == "tenant-1" diff --git a/src/backend/tests/unit/api/v1/test_deployment_schemas.py b/src/backend/tests/unit/api/v1/test_deployment_schemas.py index b68c07e20d..bf076fb93f 100644 --- a/src/backend/tests/unit/api/v1/test_deployment_schemas.py +++ b/src/backend/tests/unit/api/v1/test_deployment_schemas.py @@ -7,9 +7,9 @@ from uuid import uuid4 import pytest from langflow.api.v1.schemas.deployments import ( - DeploymentProviderAccountCreate, - DeploymentProviderAccountResponse, - DeploymentProviderAccountUpdate, + DeploymentProviderAccountCreateRequest, + DeploymentProviderAccountGetResponse, + DeploymentProviderAccountUpdateRequest, FlowVersionsAttach, FlowVersionsPatch, ) @@ -24,12 +24,12 @@ class TestApiKeyWriteOnly: """Ensure api_key is excluded from every response model.""" def test_provider_account_response_excludes_api_key(self): - """DeploymentProviderAccountResponse.model_fields must not contain api_key.""" - assert "api_key" not in DeploymentProviderAccountResponse.model_fields + """DeploymentProviderAccountGetResponse.model_fields must not contain api_key.""" + assert "api_key" not in DeploymentProviderAccountGetResponse.model_fields def test_provider_account_response_dump_excludes_api_key(self): """model_dump() on a response instance must never contain api_key.""" - response = DeploymentProviderAccountResponse( + response = DeploymentProviderAccountGetResponse( id=uuid4(), provider_key="aws", provider_url="https://example.com", @@ -39,7 +39,7 @@ class TestApiKeyWriteOnly: def test_create_schema_masks_api_key_in_repr(self): """SecretStr should mask the value in string representations.""" - account = DeploymentProviderAccountCreate( + account = DeploymentProviderAccountCreateRequest( provider_key="aws", provider_url="https://example.com", api_key="super-secret-key", @@ -49,7 +49,7 @@ class TestApiKeyWriteOnly: def test_update_schema_masks_api_key_in_repr(self): """SecretStr should mask the value in string representations on update.""" - account = DeploymentProviderAccountUpdate(api_key="new-secret") + account = DeploymentProviderAccountUpdateRequest(api_key="new-secret") assert isinstance(account.api_key, SecretStr) assert "new-secret" not in repr(account) @@ -61,7 +61,7 @@ class TestApiKeyWriteOnly: class TestNonEmptyStr: def test_strips_whitespace(self): - account = DeploymentProviderAccountCreate( + account = DeploymentProviderAccountCreateRequest( provider_key=" aws ", provider_url="https://example.com", api_key="key", @@ -70,7 +70,7 @@ class TestNonEmptyStr: def test_rejects_empty_string(self): with pytest.raises(ValidationError): - DeploymentProviderAccountCreate( + DeploymentProviderAccountCreateRequest( provider_key="", provider_url="https://example.com", api_key="key", @@ -78,7 +78,7 @@ class TestNonEmptyStr: def test_rejects_whitespace_only(self): with pytest.raises(ValidationError): - DeploymentProviderAccountCreate( + DeploymentProviderAccountCreateRequest( provider_key=" ", provider_url="https://example.com", api_key="key",