mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-25 15:49:14 +08:00
ref: architectural renames for deployment api, service, persistence layer (#12106)
Decouples deployment API schemas from service-layer ID domains, updates naming to match persistence layer, removes unnecessary database models, and adds strict API wrappers.
This commit is contained in:
@ -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,
|
||||
|
||||
@ -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."""
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
from .model import Deployment, DeploymentCreate, DeploymentRead, DeploymentUpdate
|
||||
from .model import Deployment, DeploymentRead
|
||||
|
||||
__all__ = ["Deployment", "DeploymentCreate", "DeploymentRead", "DeploymentUpdate"]
|
||||
__all__ = ["Deployment", "DeploymentRead"]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,13 +1,9 @@
|
||||
from .model import (
|
||||
DeploymentProviderAccount,
|
||||
DeploymentProviderAccountCreate,
|
||||
DeploymentProviderAccountRead,
|
||||
DeploymentProviderAccountUpdate,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DeploymentProviderAccount",
|
||||
"DeploymentProviderAccountCreate",
|
||||
"DeploymentProviderAccountRead",
|
||||
"DeploymentProviderAccountUpdate",
|
||||
]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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",
|
||||
|
||||
Reference in New Issue
Block a user