ci: enable Ruff E402 (#44123)

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-19 01:16:16 +00:00
1 parent d8b37884d8
commit 672a627fa5
63 files changed
+700 -370

No files matched your search

-1
View File
@@ -58,7 +58,6 @@ select = [
]
ignore = [
"E402", # module-import-not-at-top-of-file
"F841", # unused-variable
"B901", # allow return in yield
"B904", # raise-without-from-inside-except
+2 -1
View File
@@ -8,6 +8,7 @@ pscycogreen_gevent.patch_psycopg()
print("psycopg2 patched with gevent.", flush=True) # noqa: T201
from app import app, celery
# Import after gevent patching.
from app import app, celery # noqa: E402
__all__ = ["app", "celery"]
@@ -5,14 +5,13 @@ from typing import Any, override
from pydantic.fields import FieldInfo
from .http_request import NacosHttpClient
logger = logging.getLogger(__name__)
from configs.remote_settings_sources.base import RemoteSettingsSource
from .http_request import NacosHttpClient
from .utils import parse_config
logger = logging.getLogger(__name__)
class NacosSettingsSource(RemoteSettingsSource):
def __init__(self, configs: Mapping[str, Any]):
+228 -36
View File
@@ -1,6 +1,7 @@
from collections import OrderedDict
from http import HTTPStatus
from importlib import import_module
from types import ModuleType
from typing import cast
from flask import Blueprint, current_app, got_request_exception
@@ -128,9 +129,224 @@ RESOURCE_MODULES = (
for module_name in RESOURCE_MODULES:
import_module(module_name)
# Ensure resource modules are imported so route decorators are evaluated.
# Import other controllers
from . import (
def _register_controllers() -> tuple[ModuleType, ...]:
"""Load route modules after their namespaces and schema models exist."""
# Ensure resource modules are imported so route decorators are evaluated.
# Import other controllers
from . import (
apikey,
extension,
feature,
human_input_form,
init_validate,
notification,
onboarding,
resource_access_token,
setup,
spec,
system,
workflow_run_archive,
)
from .agent import composer as agent_composer
from .agent import roster as agent_roster
# Import app controllers
from .app import (
advanced_prompt_template,
agent,
agent_app_access,
agent_app_feature,
agent_app_sandbox,
agent_config_inspector,
annotation,
app,
audio,
completion,
conversation,
conversation_variables,
generator,
mcp_server,
message,
model_config,
ops_trace,
site,
statistic,
workflow,
workflow_app_log,
workflow_comment,
workflow_draft_variable,
workflow_node_output_inspector,
workflow_run,
workflow_statistic,
workflow_trigger,
)
# Import auth controllers
from .auth import (
activate,
data_source_bearer_auth,
data_source_oauth,
email_register,
forgot_password,
login,
oauth,
oauth_server,
)
# Import billing controllers
from .billing import billing, compliance
# Import datasets controllers
from .datasets import (
data_source,
datasets,
datasets_document,
datasets_segments,
external,
hit_testing,
metadata,
website,
)
from .datasets.rag_pipeline import (
datasource_auth,
datasource_content_preview,
rag_pipeline,
rag_pipeline_datasets,
rag_pipeline_draft_variable,
rag_pipeline_import,
rag_pipeline_workflow,
)
# Import explore controllers
from .explore import (
banner,
installed_app,
parameter,
recommended_app,
saved_message,
trial,
)
from .snippets import snippet_workflow, snippet_workflow_draft_variable
from .socketio import workflow as socketio_workflow
# Import tag controllers
from .tag import tags
# Import workspace controllers
from .workspace import (
account,
agent_providers,
endpoint,
load_balancing_config,
members,
model_providers,
models,
plugin,
rbac,
skills,
snippets,
tool_providers,
trigger_providers,
workspace,
)
return (
apikey,
extension,
feature,
human_input_form,
init_validate,
notification,
onboarding,
resource_access_token,
setup,
spec,
system,
workflow_run_archive,
agent_composer,
agent_roster,
advanced_prompt_template,
agent,
agent_app_access,
agent_app_feature,
agent_app_sandbox,
agent_config_inspector,
annotation,
app,
audio,
completion,
conversation,
conversation_variables,
generator,
mcp_server,
message,
model_config,
ops_trace,
site,
statistic,
workflow,
workflow_app_log,
workflow_comment,
workflow_draft_variable,
workflow_node_output_inspector,
workflow_run,
workflow_statistic,
workflow_trigger,
activate,
data_source_bearer_auth,
data_source_oauth,
email_register,
forgot_password,
login,
oauth,
oauth_server,
billing,
compliance,
data_source,
datasets,
datasets_document,
datasets_segments,
external,
hit_testing,
metadata,
website,
datasource_auth,
datasource_content_preview,
rag_pipeline,
rag_pipeline_datasets,
rag_pipeline_draft_variable,
rag_pipeline_import,
rag_pipeline_workflow,
banner,
installed_app,
parameter,
recommended_app,
saved_message,
trial,
snippet_workflow,
snippet_workflow_draft_variable,
socketio_workflow,
tags,
account,
agent_providers,
endpoint,
load_balancing_config,
members,
model_providers,
models,
plugin,
rbac,
skills,
snippets,
tool_providers,
trigger_providers,
workspace,
)
(
apikey,
extension,
feature,
@@ -143,12 +359,8 @@ from . import (
spec,
system,
workflow_run_archive,
)
from .agent import composer as agent_composer
from .agent import roster as agent_roster
# Import app controllers
from .app import (
agent_composer,
agent_roster,
advanced_prompt_template,
agent,
agent_app_access,
@@ -176,10 +388,6 @@ from .app import (
workflow_run,
workflow_statistic,
workflow_trigger,
)
# Import auth controllers
from .auth import (
activate,
data_source_bearer_auth,
data_source_oauth,
@@ -188,13 +396,8 @@ from .auth import (
login,
oauth,
oauth_server,
)
# Import billing controllers
from .billing import billing, compliance
# Import datasets controllers
from .datasets import (
billing,
compliance,
data_source,
datasets,
datasets_document,
@@ -203,8 +406,6 @@ from .datasets import (
hit_testing,
metadata,
website,
)
from .datasets.rag_pipeline import (
datasource_auth,
datasource_content_preview,
rag_pipeline,
@@ -212,25 +413,16 @@ from .datasets.rag_pipeline import (
rag_pipeline_draft_variable,
rag_pipeline_import,
rag_pipeline_workflow,
)
# Import explore controllers
from .explore import (
banner,
installed_app,
parameter,
recommended_app,
saved_message,
trial,
)
from .snippets import snippet_workflow, snippet_workflow_draft_variable
from .socketio import workflow as socketio_workflow
# Import tag controllers
from .tag import tags
# Import workspace controllers
from .workspace import (
snippet_workflow,
snippet_workflow_draft_variable,
socketio_workflow,
tags,
account,
agent_providers,
endpoint,
@@ -245,7 +437,7 @@ from .workspace import (
tool_providers,
trigger_providers,
workspace,
)
) = _register_controllers()
api.add_namespace(console_ns)
@@ -355,7 +355,6 @@ class AnnotationApi(Resource):
@console_ns.response(204, "Annotations deleted successfully")
@with_session
def delete(self, session: Session, app_id: UUID):
# Use request.args.getlist to get annotation_ids array directly
annotation_ids = request.args.getlist("annotation_id")
@@ -222,7 +222,6 @@ class CompletionMessageStopApi(Resource):
@with_current_user_id
@get_app_model(mode=AppMode.COMPLETION)
def post(self, current_user_id: str, app_model: App, task_id: str):
AppTaskService.stop_task(
task_id=task_id,
invoke_from=InvokeFrom.DEBUGGER,
@@ -111,7 +111,6 @@ class CompletionConversationApi(Resource):
@get_app_model(mode=AppMode.COMPLETION)
@model_validate(CompletionConversationQuery)
def get(self, req_data: CompletionConversationQuery, session: Session, current_user: Account, app_model: App):
query = sa.select(Conversation).where(
Conversation.app_id == app_model.id, Conversation.mode == "completion", Conversation.is_deleted.is_(False)
)
@@ -241,7 +240,6 @@ class ChatConversationApi(Resource):
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
@model_validate(ChatConversationQuery)
def get(self, req_data: ChatConversationQuery, session: Session, current_user: Account, app_model: App):
subquery = (
sa.select(Conversation.id.label("conversation_id"), EndUser.session_id.label("from_end_user_session_id"))
.outerjoin(EndUser, Conversation.from_end_user_id == EndUser.id)
-5
View File
@@ -274,7 +274,6 @@ class RuleGenerateApi(Resource):
@with_current_tenant_id
@model_validate(RuleGeneratePayload)
def post(self, req_data: RuleGeneratePayload, current_tenant_id: str):
try:
rules = LLMGenerator.generate_rule_config(tenant_id=current_tenant_id, args=req_data)
except ProviderTokenNotInitError as ex:
@@ -308,7 +307,6 @@ class RuleCodeGenerateApi(Resource):
@with_current_tenant_id
@model_validate(RuleCodeGeneratePayload)
def post(self, req_data: RuleCodeGeneratePayload, current_tenant_id: str):
try:
code_result = LLMGenerator.generate_code(
tenant_id=current_tenant_id,
@@ -349,7 +347,6 @@ class RuleStructuredOutputGenerateApi(Resource):
@with_current_tenant_id
@model_validate(RuleStructuredOutputPayload)
def post(self, req_data: RuleStructuredOutputPayload, current_tenant_id: str):
try:
structured_output = LLMGenerator.generate_structured_output(
tenant_id=current_tenant_id,
@@ -563,7 +560,6 @@ class WorkflowGenerateApi(Resource):
@with_current_tenant_id
@model_validate(WorkflowGeneratePayload)
def post(self, req_data: WorkflowGeneratePayload, current_tenant_id: str):
# Reject empty / over-length instructions at the boundary (shared with
# the streaming endpoint) before spending a planner+builder roundtrip.
guard = _workflow_instruction_guard(req_data)
@@ -656,7 +652,6 @@ class WorkflowGenerateStreamApi(Resource):
@with_current_tenant_id
@model_validate(WorkflowGeneratePayload)
def post(self, req_data: WorkflowGeneratePayload, current_tenant_id: str):
# Same boundary guards as the blocking endpoint — return a normal 400
# JSON for these BEFORE opening the stream.
guard = _workflow_instruction_guard(req_data)
-1
View File
@@ -517,7 +517,6 @@ def _update_message_feedback(
def _get_message_suggested_questions(
*, context: RequestContext, app_id: str, app_mode: str, message_id: UUID
) -> dict[str, object]:
try:
questions = application_services().message_suggested_questions.get_suggested_questions(
app_id=app_id,
@@ -545,7 +545,6 @@ class ConversationVariableCollectionApi(Resource):
@get_app_model(mode=AppMode.ADVANCED_CHAT)
@model_validate(ConversationVariableUpdatePayload)
def post(self, req_data: ConversationVariableUpdatePayload, current_user: Account, app_model: App):
workflow_service = WorkflowService()
conversation_variables_list = [
@@ -647,7 +646,6 @@ class EnvironmentVariableCollectionApi(Resource):
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@model_validate(EnvironmentVariableUpdatePayload)
def post(self, req_data: EnvironmentVariableUpdatePayload, current_user: Account, app_model: App):
workflow_service = WorkflowService()
environment_variables_list = [
@@ -1168,7 +1168,6 @@ class RagPipelineRecommendedPluginApi(Resource):
@with_current_tenant_id
@model_validate(RagPipelineRecommendedPluginQuery)
def get(self, req_data: RagPipelineRecommendedPluginQuery, current_tenant_id: str, current_user: Account):
rag_pipeline_service = RagPipelineService(db.session())
recommended_plugins = rag_pipeline_service.get_recommended_plugins(
req_data.type, current_user, current_tenant_id
@@ -300,7 +300,6 @@ class EndpointListApi(Resource):
@with_current_tenant_id
@model_validate(EndpointListQuery)
def get(self, req_data: EndpointListQuery, tenant_id: str, user_id: str):
endpoints = EndpointService.list_endpoints(
tenant_id=tenant_id,
user_id=user_id,
@@ -330,7 +329,6 @@ class EndpointListForSinglePluginApi(Resource):
@with_current_tenant_id
@model_validate(EndpointListForPluginQuery)
def get(self, req_data: EndpointListForPluginQuery, tenant_id: str, user_id: str):
endpoints = EndpointService.list_endpoints_for_single_plugin(
tenant_id=tenant_id,
user_id=user_id,
@@ -216,7 +216,6 @@ class DefaultModelApi(Resource):
@with_current_tenant_id
@model_validate(ParserGetDefault)
def get(self, req_data: ParserGetDefault, tenant_id: str):
model_provider_service = ModelProviderService()
default_model_entity = model_provider_service.get_default_model_of_model_type(
tenant_id=tenant_id, model_type=req_data.model_type
@@ -323,7 +322,6 @@ class ModelProviderModelApi(Resource):
@with_current_tenant_id
@model_validate(ParserDeleteModels)
def delete(self, req_data: ParserDeleteModels, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
model_provider_service.remove_model(
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
@@ -349,7 +347,6 @@ class ModelProviderModelCredentialApi(Resource):
@with_current_tenant_id
@model_validate(ParserGetCredentials)
def get(self, req_data: ParserGetCredentials, tenant_id: str, user: Account, provider: str):
model_provider_service = ModelProviderService()
current_credential = model_provider_service.get_model_credential(
tenant_id=tenant_id,
@@ -412,7 +409,6 @@ class ModelProviderModelCredentialApi(Resource):
@with_current_tenant_id
@model_validate(ParserCreateCredential)
def post(self, req_data: ParserCreateCredential, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
try:
@@ -445,7 +441,6 @@ class ModelProviderModelCredentialApi(Resource):
@with_current_tenant_id
@model_validate(ParserUpdateCredential)
def put(self, req_data: ParserUpdateCredential, current_tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
try:
@@ -473,7 +468,6 @@ class ModelProviderModelCredentialApi(Resource):
@with_current_tenant_id
@model_validate(ParserDeleteCredential)
def delete(self, req_data: ParserDeleteCredential, current_tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
model_provider_service.remove_model_credential(
tenant_id=current_tenant_id,
@@ -498,7 +492,6 @@ class ModelProviderModelCredentialSwitchApi(Resource):
@with_current_tenant_id
@model_validate(ParserSwitch)
def post(self, req_data: ParserSwitch, current_tenant_id: str, provider: str):
service = ModelProviderService()
service.add_model_credential_to_model_list(
tenant_id=current_tenant_id,
@@ -523,7 +516,6 @@ class ModelProviderModelEnableApi(Resource):
@rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace()))
@model_validate(ParserDeleteModels)
def patch(self, req_data: ParserDeleteModels, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
model_provider_service.enable_model(
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
@@ -545,7 +537,6 @@ class ModelProviderModelDisableApi(Resource):
@rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace()))
@model_validate(ParserDeleteModels)
def patch(self, req_data: ParserDeleteModels, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
model_provider_service.disable_model(
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
@@ -577,7 +568,6 @@ class ModelProviderModelValidateApi(Resource):
@with_current_tenant_id
@model_validate(ParserValidate)
def post(self, req_data: ParserValidate, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
result = True
@@ -615,7 +605,6 @@ class ModelProviderModelParameterRuleApi(Resource):
@with_current_tenant_id
@model_validate(ParserParameter)
def get(self, req_data: ParserParameter, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
parameter_rules = model_provider_service.get_model_parameter_rules(
tenant_id=tenant_id, provider=provider, model=req_data.model
@@ -688,7 +688,6 @@ class PluginListLatestVersionsApi(Resource):
@account_initialization_required
@model_validate(ParserLatest)
def post(self, req_data: ParserLatest):
try:
versions = PluginService.list_latest_versions(req_data.plugin_ids)
except PluginDaemonClientSideError as e:
@@ -708,7 +707,6 @@ class PluginListInstallationsFromIdsApi(Resource):
@with_current_tenant_id
@model_validate(ParserLatest)
def post(self, req_data: ParserLatest, tenant_id: str):
try:
plugins = PluginService.list_installations_from_ids(tenant_id, req_data.plugin_ids)
except PluginDaemonClientSideError as e:
@@ -725,7 +723,6 @@ class PluginIconApi(Resource):
@setup_required
@model_validate(ParserIcon)
def get(self, req_data: ParserIcon):
try:
icon_bytes, mimetype = PluginService.get_asset(req_data.tenant_id, req_data.filename)
except PluginDaemonClientSideError as e:
@@ -746,7 +743,6 @@ class PluginAssetApi(Resource):
@with_current_tenant_id
@model_validate(ParserAsset)
def get(self, req_data: ParserAsset, tenant_id: str):
try:
binary = PluginService.extract_asset(tenant_id, req_data.plugin_unique_identifier, req_data.file_name)
return send_file(io.BytesIO(binary), mimetype="application/octet-stream")
@@ -789,7 +785,6 @@ class PluginUploadFromGithubApi(Resource):
@with_current_tenant_id
@model_validate(ParserGithubUpload)
def post(self, req_data: ParserGithubUpload, tenant_id: str):
try:
response = PluginService.upload_pkg_from_github(
tenant_id, req_data.repo, req_data.version, req_data.package
@@ -833,7 +828,6 @@ class PluginInstallFromPkgApi(Resource):
@with_current_tenant_id
@model_validate(ParserPluginIdentifiers)
def post(self, req_data: ParserPluginIdentifiers, tenant_id: str):
try:
response = PluginService.install_from_local_pkg(tenant_id, req_data.plugin_unique_identifiers)
except PluginDaemonClientSideError as e:
@@ -855,7 +849,6 @@ class PluginInstallFromGithubApi(Resource):
@with_current_tenant_id
@model_validate(ParserGithubInstall)
def post(self, req_data: ParserGithubInstall, tenant_id: str):
try:
response = PluginService.install_from_github(
tenant_id,
@@ -883,7 +876,6 @@ class PluginInstallFromMarketplaceApi(Resource):
@with_current_tenant_id
@model_validate(ParserPluginIdentifiers)
def post(self, req_data: ParserPluginIdentifiers, tenant_id: str):
try:
response = PluginService.install_from_marketplace_pkg(tenant_id, req_data.plugin_unique_identifiers)
except PluginDaemonClientSideError as e:
@@ -905,7 +897,6 @@ class PluginFetchMarketplacePkgApi(Resource):
@with_current_tenant_id
@model_validate(ParserPluginIdentifierQuery)
def get(self, req_data: ParserPluginIdentifierQuery, tenant_id: str):
try:
return jsonable_encoder(
{
@@ -932,7 +923,6 @@ class PluginFetchManifestApi(Resource):
@with_current_tenant_id
@model_validate(ParserPluginIdentifierQuery)
def get(self, req_data: ParserPluginIdentifierQuery, tenant_id: str):
try:
return jsonable_encoder(
{
@@ -957,7 +947,6 @@ class PluginFetchInstallTasksApi(Resource):
@with_current_tenant_id
@model_validate(ParserTasks)
def get(self, req_data: ParserTasks, tenant_id: str):
try:
return jsonable_encoder(
{"tasks": PluginService.fetch_install_tasks(tenant_id, req_data.page, req_data.page_size)}
@@ -1043,7 +1032,6 @@ class PluginUpgradeFromMarketplaceApi(Resource):
@with_current_tenant_id
@model_validate(ParserMarketplaceUpgrade)
def post(self, req_data: ParserMarketplaceUpgrade, tenant_id: str):
try:
return jsonable_encoder(
PluginService.upgrade_plugin_with_marketplace(
@@ -1067,7 +1055,6 @@ class PluginUpgradeFromGithubApi(Resource):
@with_current_tenant_id
@model_validate(ParserGithubUpgrade)
def post(self, req_data: ParserGithubUpgrade, tenant_id: str):
try:
return jsonable_encoder(
PluginService.upgrade_plugin_with_github(
@@ -1096,7 +1083,6 @@ class PluginUninstallApi(Resource):
@with_current_tenant_id
@model_validate(ParserUninstall)
def post(self, req_data: ParserUninstall, tenant_id: str):
try:
return {
"success": PluginService.uninstall(
@@ -1171,7 +1157,6 @@ class PluginFetchDynamicSelectOptionsApi(Resource):
@with_current_tenant_id
@model_validate(ParserDynamicOptions)
def get(self, req_data: ParserDynamicOptions, tenant_id: str, current_user: Account):
try:
options = PluginParameterService.get_dynamic_select_options(
tenant_id=tenant_id,
@@ -527,7 +527,6 @@ class ToolBuiltinProviderListToolsApi(Resource):
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str, provider: str):
return dump_response(
ToolApiListResponse,
BuiltinToolManageService.list_builtin_tool_provider_tools(
@@ -549,7 +548,6 @@ class ToolBuiltinProviderInfoApi(Resource):
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str, provider: str):
return _dump_tool_provider_payload(
BuiltinToolManageService.get_builtin_tool_provider_info(tenant_id, provider).to_dict()
)
@@ -571,7 +569,6 @@ class ToolBuiltinProviderDeleteApi(Resource):
@with_current_tenant_id
@model_validate(BuiltinToolCredentialDeletePayload)
def post(self, req_data: BuiltinToolCredentialDeletePayload, tenant_id: str, provider: str):
return dump_response(
SimpleResultResponse,
BuiltinToolManageService.delete_builtin_tool_provider(
@@ -855,7 +852,6 @@ class ToolBuiltinProviderCredentialsSchemaApi(Resource):
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str, provider, credential_type):
return dump_response(
ProviderConfigListResponse,
BuiltinToolManageService.list_builtin_provider_credentials_schema(
@@ -1075,7 +1071,6 @@ class ToolApiListApi(Resource):
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str):
return _dump_tool_provider_payload_list(
[
provider.to_dict()
@@ -1604,7 +1599,6 @@ class ToolMCPListAllApi(Resource):
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str):
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
# Skip sensitive data decryption for list view to improve performance
-2
View File
@@ -402,7 +402,6 @@ def knowledge_pipeline_publish_enabled[**P, R](view: Callable[P, R]) -> Callable
def edit_permission_required[**P, R](f: Callable[P, R]) -> Callable[P, R]:
@wraps(f)
def decorated_function(*args: P.args, **kwargs: P.kwargs):
from libs.login import current_user
if not dify_config.RBAC_ENABLED:
@@ -419,7 +418,6 @@ def edit_permission_required[**P, R](f: Callable[P, R]) -> Callable[P, R]:
def is_admin_or_owner_required[**P, R](f: Callable[P, R]) -> Callable[P, R]:
@wraps(f)
def decorated_function(*args: P.args, **kwargs: P.kwargs):
from libs.login import current_user
if not dify_config.RBAC_ENABLED:
+22 -1
View File
@@ -1,3 +1,5 @@
from types import ModuleType
from flask import Blueprint
from flask_restx import Namespace
@@ -14,7 +16,26 @@ api = ExternalApi(
files_ns = Namespace("files", description="File operations", path="/")
from . import appdeploy_files, plugin_file_upload, tool_files, upload_file_delivery
def _register_controllers() -> tuple[ModuleType, ...]:
"""Load route modules after their namespaces and schema models exist."""
from . import appdeploy_files, plugin_file_upload, tool_files, upload_file_delivery
return (
appdeploy_files,
plugin_file_upload,
tool_files,
upload_file_delivery,
)
(
appdeploy_files,
plugin_file_upload,
tool_files,
upload_file_delivery,
) = _register_controllers()
api.add_namespace(files_ns)
+47 -14
View File
@@ -1,3 +1,5 @@
from types import ModuleType
from flask import Blueprint
from flask_restx import Namespace
@@ -24,20 +26,51 @@ api = ExternalApi(
# Create namespace
inner_api_ns = Namespace("inner_api", description="Internal API operations", path="/")
from . import mail as _mail
from . import runtime_credentials as _runtime_credentials
from .agent import files as _agent_files
from .agent import llm as _agent_llm
from .agent import runtime_usage as _agent_runtime_usage
from .agent import tools as _agent_tools
from .app import app as _app_app
from .app import dsl as _app_dsl
from .app import file_grants as _app_file_grants
from .knowledge import retrieval as _knowledge_retrieval
from .plugin import agent_config as _agent_config
from .plugin import plugin as _plugin
from .plugin import skills as _skills
from .workspace import workspace as _workspace
def _register_controllers() -> tuple[ModuleType, ...]:
"""Load route modules after their namespace exists."""
from . import mail, runtime_credentials
from .agent import files, llm, runtime_usage, tools
from .app import app, dsl, file_grants
from .knowledge import retrieval
from .plugin import agent_config, plugin, skills
from .workspace import workspace
return (
mail,
runtime_credentials,
files,
llm,
runtime_usage,
tools,
app,
dsl,
file_grants,
retrieval,
agent_config,
plugin,
skills,
workspace,
)
(
_mail,
_runtime_credentials,
_agent_files,
_agent_llm,
_agent_runtime_usage,
_agent_tools,
_app_app,
_app_dsl,
_app_file_grants,
_knowledge_retrieval,
_agent_config,
_plugin,
_skills,
_workspace,
) = _register_controllers()
api.add_namespace(inner_api_ns)
+12 -1
View File
@@ -1,3 +1,5 @@
from types import ModuleType
from flask import Blueprint
from flask_restx import Namespace
@@ -14,7 +16,16 @@ api = ExternalApi(
mcp_ns = Namespace("mcp", description="MCP operations", path="/")
from . import mcp
def _register_controllers() -> tuple[ModuleType, ...]:
"""Load route modules after their namespaces and schema models exist."""
from . import mcp
return (mcp,)
(mcp,) = _register_controllers()
api.add_namespace(mcp_ns)
+96 -41
View File
@@ -1,51 +1,15 @@
from types import ModuleType
from typing import override
from flask import Blueprint
from flask_restx import Namespace
from controllers.openapi._catalog import attach_catalog
from controllers.openapi._errors import ErrorBody, OpenApiErrorCode, OpenApiErrorFormatter
from controllers.openapi._upload import describe_multipart_bodies
from libs.device_flow_security import attach_anti_framing
from libs.external_api import ErrorSpec, ExternalApi
bp = Blueprint("openapi", __name__, url_prefix="/openapi/v1")
attach_anti_framing(bp)
attach_catalog(bp)
class _OpenApi(ExternalApi):
"""The surface's document also names the multipart form its file-bearing bodies accept."""
error_response_model = ErrorBody
@override
def finish_document(self, document: dict[str, object]) -> dict[str, object]:
return super().finish_document(describe_multipart_bodies(document))
@override
def error_code_for(self, spec: ErrorSpec) -> str:
return OpenApiErrorFormatter.code_for(spec["status"], spec["error_code"])
@override
def implicit_error_codes(self, status: int) -> set[str]:
return {OpenApiErrorFormatter.code_for(status, None)}
api = _OpenApi(
bp,
version="1.0",
title="OpenAPI",
description="User-scoped programmatic API (bearer auth)",
error_body_formatter=OpenApiErrorFormatter(),
)
openapi_ns = Namespace("openapi", description="User-scoped operations", path="/")
# Register response/query models BEFORE importing controller modules so that
# @openapi_ns.response / @openapi_ns.expect decorators can resolve model names.
from controllers.common.fields import EventStreamResponse, SimpleResultResponse
from controllers.common.schema import register_enum_models, register_response_schema_models, register_schema_models
from controllers.openapi._catalog import attach_catalog
from controllers.openapi._errors import ErrorBody, OpenApiErrorCode, OpenApiErrorFormatter
from controllers.openapi._models import (
AccountPayload,
AccountResponse,
@@ -184,14 +148,50 @@ from controllers.openapi._models import (
WorkspacePayload,
WorkspaceSummaryResponse,
)
from controllers.openapi._upload import describe_multipart_bodies
from fields.file_fields import FileResponse
from fields.workflow_run_fields import (
WorkflowRunDetailResponse,
WorkflowRunNodeExecutionListResponse,
WorkflowRunNodeExecutionResponse,
)
from libs.device_flow_security import attach_anti_framing
from libs.external_api import ErrorSpec, ExternalApi
from services.entities.dsl_entities import CheckDependenciesResult, Import
bp = Blueprint("openapi", __name__, url_prefix="/openapi/v1")
attach_anti_framing(bp)
attach_catalog(bp)
class _OpenApi(ExternalApi):
"""The surface's document also names the multipart form its file-bearing bodies accept."""
error_response_model = ErrorBody
@override
def finish_document(self, document: dict[str, object]) -> dict[str, object]:
return super().finish_document(describe_multipart_bodies(document))
@override
def error_code_for(self, spec: ErrorSpec) -> str:
return OpenApiErrorFormatter.code_for(spec["status"], spec["error_code"])
@override
def implicit_error_codes(self, status: int) -> set[str]:
return {OpenApiErrorFormatter.code_for(status, None)}
api = _OpenApi(
bp,
version="1.0",
title="OpenAPI",
description="User-scoped programmatic API (bearer auth)",
error_body_formatter=OpenApiErrorFormatter(),
)
openapi_ns = Namespace("openapi", description="User-scoped operations", path="/")
register_schema_models(
openapi_ns,
AdvancedChatNodeRunPayload,
@@ -347,7 +347,62 @@ register_response_schema_models(
# string on the wire so old clients keep parsing future codes.
register_enum_models(openapi_ns, OpenApiErrorCode)
from . import (
def _register_controllers() -> tuple[ModuleType, ...]:
"""Load route modules after their namespaces and schema models exist."""
from . import (
account,
app_create,
app_dsl,
app_info,
app_release,
app_run,
app_webapp,
app_workflow,
apps,
apps_permitted_external,
files,
human_input_form,
index,
knowledge_bases,
model_providers,
node_types,
oauth_device,
oauth_device_sso,
plugins,
tool_providers,
workflow_events,
workspaces,
)
return (
account,
app_create,
app_dsl,
app_info,
app_release,
app_run,
app_webapp,
app_workflow,
apps,
apps_permitted_external,
files,
human_input_form,
index,
knowledge_bases,
model_providers,
node_types,
oauth_device,
oauth_device_sso,
plugins,
tool_providers,
workflow_events,
workspaces,
)
(
account,
app_create,
app_dsl,
@@ -370,7 +425,7 @@ from . import (
tool_providers,
workflow_events,
workspaces,
)
) = _register_controllers()
# Request models are imported from _models.py and registered above.
+63 -8
View File
@@ -1,3 +1,5 @@
from types import ModuleType
from flask import Blueprint
from flask_restx import Namespace
@@ -27,8 +29,63 @@ api.document_generic_errors(InvalidArgumentError, NotFoundError, UnauthorizedErr
service_api_ns = Namespace("service_api", description="Service operations", path="/")
from . import index
from .app import (
def _register_controllers() -> tuple[ModuleType, ...]:
"""Load route modules after their namespaces and schema models exist."""
from . import index
from .app import (
annotation,
app,
audio,
completion,
conversation,
file,
file_preview,
human_input_form,
message,
site,
workflow,
workflow_events,
)
from .dataset import (
dataset,
document,
hit_testing,
metadata,
segment,
)
from .dataset.rag_pipeline import rag_pipeline_workflow
from .end_user import end_user
from .workspace import models
return (
index,
annotation,
app,
audio,
completion,
conversation,
file,
file_preview,
human_input_form,
message,
site,
workflow,
workflow_events,
dataset,
document,
hit_testing,
metadata,
segment,
rag_pipeline_workflow,
end_user,
models,
)
(
index,
annotation,
app,
audio,
@@ -41,17 +98,15 @@ from .app import (
site,
workflow,
workflow_events,
)
from .dataset import (
dataset,
document,
hit_testing,
metadata,
segment,
)
from .dataset.rag_pipeline import rag_pipeline_workflow
from .end_user import end_user
from .workspace import models
rag_pipeline_workflow,
end_user,
models,
) = _register_controllers()
__all__ = [
"annotation",
+19 -2
View File
@@ -1,10 +1,27 @@
from types import ModuleType
from flask import Blueprint
# Create trigger blueprint
bp = Blueprint("trigger", __name__, url_prefix="/triggers")
# Import routes after blueprint creation to avoid circular imports
from . import trigger, webhook
def _register_controllers() -> tuple[ModuleType, ...]:
"""Load route modules after their namespaces and schema models exist."""
# Import routes after blueprint creation to avoid circular imports
from . import trigger, webhook
return (
trigger,
webhook,
)
(
trigger,
webhook,
) = _register_controllers()
__all__ = [
"trigger",
+49 -2
View File
@@ -1,3 +1,5 @@
from types import ModuleType
from flask import Blueprint
from flask_restx import Namespace
@@ -38,7 +40,52 @@ api.document_generic_errors(
# Create namespace
web_ns = Namespace("web", description="Web application API operations", path="/")
from . import (
def _register_controllers() -> tuple[ModuleType, ...]:
"""Load route modules after their namespaces and schema models exist."""
from . import (
app,
audio,
completion,
conversation,
feature,
files,
forgot_password,
human_input_file_upload,
human_input_form,
login,
message,
passport,
remote_files,
saved_message,
site,
workflow,
workflow_events,
)
return (
app,
audio,
completion,
conversation,
feature,
files,
forgot_password,
human_input_file_upload,
human_input_form,
login,
message,
passport,
remote_files,
saved_message,
site,
workflow,
workflow_events,
)
(
app,
audio,
completion,
@@ -56,7 +103,7 @@ from . import (
site,
workflow,
workflow_events,
)
) = _register_controllers()
api.add_namespace(web_ns)
+2 -1
View File
@@ -383,7 +383,8 @@ class RagPipelineGenerateEntity(WorkflowAppGenerateEntity):
start_node_id: str | None = None
from core.ops.ops_trace_manager import TraceQueueManager
# Resolve circular model references after model definitions.
from core.ops.ops_trace_manager import TraceQueueManager # noqa: E402
AppGenerateEntity.model_rebuild()
EasyUIBasedAppGenerateEntity.model_rebuild()
+3 -4
View File
@@ -18,6 +18,9 @@ from core.file import remote_fetcher
from core.workflow.file_reference import build_file_reference
from extensions.ext_storage import storage
from graphon.file import File, FileTransferMethod, get_file_type_by_mime_type
# init tool_file_parser
from graphon.file.tool_file_parser import set_tool_file_manager_factory
from models.model import MessageFile
from models.tools import ToolFile
@@ -214,10 +217,6 @@ class ToolFileManager:
return stream, self._build_graph_file_reference(tool_file)
# init tool_file_parser
from graphon.file.tool_file_parser import set_tool_file_manager_factory
def _factory() -> ToolFileManager:
return ToolFileManager()
+5 -2
View File
@@ -13,8 +13,11 @@ API_ROOT = Path(__file__).resolve().parents[1]
if str(API_ROOT) not in sys.path:
sys.path.insert(0, str(API_ROOT))
from dev.generate_swagger_specs import apply_runtime_defaults
from libs.flask_restx_compat import sort_openapi_arrays
# Import after script path setup.
from dev.generate_swagger_specs import apply_runtime_defaults # noqa: E402
# Import after script path setup.
from libs.flask_restx_compat import sort_openapi_arrays # noqa: E402
logger = logging.getLogger(__name__)
+5 -2
View File
@@ -20,8 +20,11 @@ API_ROOT = Path(__file__).resolve().parents[1]
if str(API_ROOT) not in sys.path:
sys.path.insert(0, str(API_ROOT))
from dev.generate_fastopenapi_specs import FASTOPENAPI_SPEC_TARGETS, generate_fastopenapi_specs
from dev.generate_swagger_specs import SPEC_TARGETS, generate_specs
# Import after script path setup.
from dev.generate_fastopenapi_specs import FASTOPENAPI_SPEC_TARGETS, generate_fastopenapi_specs # noqa: E402
# Import after script path setup.
from dev.generate_swagger_specs import SPEC_TARGETS, generate_specs # noqa: E402
logger = logging.getLogger(__name__)
+2 -1
View File
@@ -29,7 +29,8 @@ API_ROOT = Path(__file__).resolve().parents[1]
if str(API_ROOT) not in sys.path:
sys.path.insert(0, str(API_ROOT))
from libs.flask_restx_compat import finalize_openapi_payload
# Import after script path setup.
from libs.flask_restx_compat import finalize_openapi_payload # noqa: E402
@dataclass(frozen=True)
+1 -2
View File
@@ -1,5 +1,3 @@
from typing import override
"""Custom OTEL ID Generator for correlation-based trace/span ID derivation.
Uses contextvars for thread-safe correlation_id -> trace_id mapping.
@@ -11,6 +9,7 @@ without depending on span creation order.
import random
import uuid
from contextvars import ContextVar
from typing import override
from opentelemetry.sdk.trace.id_generator import IdGenerator
+1 -3
View File
@@ -1,10 +1,8 @@
from core.hosting_configuration import HostingConfiguration
from dify_app import DifyApp
hosting_configuration = HostingConfiguration()
from dify_app import DifyApp
def init_app(app: DifyApp):
hosting_configuration.init_app(app)
+1 -2
View File
@@ -27,6 +27,7 @@ from models.enums import (
DocumentCreatedFrom,
DocumentDocType,
IndexingStatus,
PermissionEnum,
ProcessRuleMode,
SegmentStatus,
SegmentType,
@@ -141,8 +142,6 @@ class DocumentDict(TypedDict):
hit_count: int | None
from models.enums import PermissionEnum
# Backward-compatible alias — new code should import PermissionEnum from models.enums
DatasetPermissionEnum = PermissionEnum
+1 -2
View File
@@ -1,8 +1,7 @@
from typing import override
"""Provider ID entities for plugin system."""
import re
from typing import override
from werkzeug.exceptions import NotFound
@@ -32,15 +32,13 @@ from core.ops.entities.trace_entity import (
from graphon.enums import BuiltinNodeTypes
from models import EndUser
from models.enums import EndUserType, MessageStatus
from tests.unit_tests.core.ops.trace_fixtures import message_trace_info, tool_trace_info, workflow_trace_info
def _dt() -> datetime:
return datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
from tests.unit_tests.core.ops.trace_fixtures import message_trace_info, tool_trace_info, workflow_trace_info
@pytest.fixture
def langfuse_config():
return LangfuseConfig(public_key="pk-123", secret_key="sk-123", host="https://cloud.langfuse.com")
@@ -25,15 +25,13 @@ from core.ops.entities.trace_entity import (
from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey
from models import EndUser
from models.enums import EndUserType, MessageStatus
from tests.unit_tests.core.ops.trace_fixtures import message_trace_info, tool_trace_info, workflow_trace_info
def _dt() -> datetime:
return datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
from tests.unit_tests.core.ops.trace_fixtures import message_trace_info, tool_trace_info, workflow_trace_info
@pytest.fixture
def opik_config():
return OpikConfig(
@@ -1,9 +1,8 @@
from typing import override
"""Tencent APM tracing with idempotent client cleanup."""
import inspect
import logging
from typing import override
from sqlalchemy import select
from sqlalchemy.orm import Session, sessionmaker
@@ -3,15 +3,15 @@ from typing import Any, TypedDict
from pydantic import BaseModel, model_validator
from core.rag.datasource.vdb.field import parse_metadata_json
from core.rag.models.document import Document
from extensions.ext_redis import redis_client
_import_err_msg = (
"`alibabacloud_gpdb20160503` and `alibabacloud_tea_openapi` packages not found, "
"please run `pip install alibabacloud_gpdb20160503 alibabacloud_tea_openapi`"
)
from core.rag.datasource.vdb.field import parse_metadata_json
from core.rag.models.document import Document
from extensions.ext_redis import redis_client
class AnalyticdbClientParamsDict(TypedDict):
access_key_id: str
@@ -8,9 +8,6 @@ from typing import TYPE_CHECKING, Any, override
import httpx
import qdrant_client
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
from flask import current_app
from httpx import DigestAuth
from pydantic import BaseModel
@@ -25,6 +22,7 @@ from qdrant_client.http.models import (
)
from qdrant_client.local.qdrant_local import QdrantLocal
from sqlalchemy import select
from sqlalchemy.orm import Session
from configs import dify_config
from core.rag.datasource.vdb.field import Field
@@ -39,6 +37,8 @@ from extensions.ext_redis import redis_client
from models.dataset import Dataset, TidbAuthBinding
from models.enums import TidbAuthBindingStatus
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from qdrant_client import grpc # noqa
from qdrant_client.conversions import common_types
@@ -1,5 +1,3 @@
from typing import override
"""Unit tests for Weaviate vector database implementation.
Focuses on verifying that doc_type is properly handled in:
@@ -13,6 +11,7 @@ import datetime
import json
import unittest
from types import SimpleNamespace
from typing import override
from unittest.mock import MagicMock, patch
import pytest
@@ -5,6 +5,7 @@ This service restores archived workflow run data from S3-compatible storage
back to the database.
"""
import contextlib
import io
import logging
import time
@@ -19,21 +20,6 @@ import click
from pydantic import TypeAdapter
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.engine import CursorResult
class _TableInfo(TypedDict, total=False):
row_count: int
class ArchiveManifest(TypedDict, total=False):
tables: dict[str, _TableInfo]
schema_version: str
_manifest_adapter: TypeAdapter[ArchiveManifest] = TypeAdapter(ArchiveManifest)
import contextlib
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
from extensions.ext_database import db
@@ -56,6 +42,18 @@ from repositories.api_workflow_run_repository import APIWorkflowRunRepository
from repositories.factory import DifyAPIRepositoryFactory
from services.retention.workflow_run.constants import ARCHIVE_BUNDLE_NAME
class _TableInfo(TypedDict, total=False):
row_count: int
class ArchiveManifest(TypedDict, total=False):
tables: dict[str, _TableInfo]
schema_version: str
_manifest_adapter: TypeAdapter[ArchiveManifest] = TypeAdapter(ArchiveManifest)
logger = logging.getLogger(__name__)
+1 -2
View File
@@ -1,5 +1,3 @@
from typing import Protocol, override
"""
Queue dispatcher system for async workflow execution.
@@ -8,6 +6,7 @@ with appropriate queue routing and priority assignment.
"""
from enum import StrEnum
from typing import Protocol, override
from configs import dify_config
from enums import DeploymentEdition
+15 -15
View File
@@ -94,7 +94,22 @@ from services.errors.app import (
WorkflowHashNotEqualError,
WorkflowNotFoundError,
)
from services.human_input_service import HumanInputService
from services.system_feature_service import SystemFeatureService
from services.workflow.workflow_converter import WorkflowConverter
from services.workflow_ref_service import WorkflowRef
from services.workflow_version_number_service import allocate_version_number
from .errors.workflow_service import DraftWorkflowDeletionError, WorkflowInUseError
from .human_input_delivery_test_service import (
DeliveryTestContext,
DeliveryTestEmailRecipient,
DeliveryTestError,
DeliveryTestUnsupportedError,
HumanInputDeliveryTestService,
)
from .workflow_draft_variable_service import DraftVariableSaver, DraftVarLoader, WorkflowDraftVariableService
from .workflow_restore import apply_published_workflow_snapshot_to_draft
@dataclass(frozen=True)
@@ -140,21 +155,6 @@ class _DebugHumanInputNode:
HumanInputNode = _DebugHumanInputNode
from services.human_input_service import HumanInputService
from services.workflow.workflow_converter import WorkflowConverter
from services.workflow_ref_service import WorkflowRef
from services.workflow_version_number_service import allocate_version_number
from .errors.workflow_service import DraftWorkflowDeletionError, WorkflowInUseError
from .human_input_delivery_test_service import (
DeliveryTestContext,
DeliveryTestEmailRecipient,
DeliveryTestError,
DeliveryTestUnsupportedError,
HumanInputDeliveryTestService,
)
from .workflow_draft_variable_service import DraftVariableSaver, DraftVarLoader, WorkflowDraftVariableService
from .workflow_restore import apply_published_workflow_snapshot_to_draft
_file_access_controller = DatabaseFileAccessController()
@@ -5,10 +5,6 @@ import json
import sys
from pathlib import Path
API_PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(API_PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(API_PROJECT_ROOT))
import sqlalchemy as sa
from tests.helpers.legacy_model_type_migration import (
@@ -18,6 +14,10 @@ from tests.helpers.legacy_model_type_migration import (
seed_legacy_model_type_dirty_data,
)
API_PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(API_PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(API_PROJECT_ROOT))
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
@@ -1,9 +1,3 @@
from repositories.knowledge.dataset_read_repository import (
get_dataset_available_segment_count,
get_document_hit_count,
get_document_segment_count,
)
"""
Integration tests for Dataset and Document model properties using testcontainers.
@@ -21,8 +15,11 @@ from models.dataset import Dataset, Document, DocumentSegment
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus, SegmentStatus
from repositories.knowledge.dataset_read_repository import (
get_dataset_available_document_count,
get_dataset_available_segment_count,
get_dataset_document_count,
get_dataset_word_count,
get_document_hit_count,
get_document_segment_count,
get_next_segment,
get_previous_segment,
get_segment_dataset,
+12 -12
View File
@@ -8,6 +8,11 @@ from tempfile import TemporaryDirectory
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, create_autospec, patch
# Ensure OpenDAL fs writes to tmp to avoid polluting workspace
os.environ.setdefault("OPENDAL_SCHEME", "fs")
os.environ.setdefault("OPENDAL_FS_ROOT", "/tmp/dify-storage")
os.environ.setdefault("STORAGE_TYPE", "opendal")
import pytest
from flask import Flask
from redis import Redis
@@ -15,6 +20,13 @@ from sqlalchemy import create_engine
from sqlalchemy.engine import URL, Engine
from sqlalchemy.orm import Session, sessionmaker
import core.db.session_factory as session_factory_module
from extensions import ext_redis
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.base import TypeBase
from tests.unit_tests.config_override import apply_config_overrides
from tests.unit_tests.rbac_fakes import RBACDomain, build_rbac_domain
if TYPE_CHECKING:
from extensions.ext_application_services import ApplicationServices
from tests.unit_tests.account_domain import AccountDomain
@@ -43,18 +55,6 @@ redis_mock.hgetall = MagicMock(return_value={})
redis_mock.hdel = MagicMock()
redis_mock.incr = MagicMock(return_value=1)
# Ensure OpenDAL fs writes to tmp to avoid polluting workspace
os.environ.setdefault("OPENDAL_SCHEME", "fs")
os.environ.setdefault("OPENDAL_FS_ROOT", "/tmp/dify-storage")
os.environ.setdefault("STORAGE_TYPE", "opendal")
import core.db.session_factory as session_factory_module
from extensions import ext_redis
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.base import TypeBase
from tests.unit_tests.config_override import apply_config_overrides
from tests.unit_tests.rbac_fakes import RBACDomain, build_rbac_domain
if TYPE_CHECKING:
from extensions.application_services.app import AppServices
from services.tag_application_service import TagApplicationService
@@ -3,13 +3,14 @@ from unittest.mock import patch
from flask.views import MethodView as FlaskMethodView
from controllers.common.fields import Parameters, Site
from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict
from models.model import IconType
_NEEDS_METHOD_VIEW_CLEANUP = False
if not hasattr(builtins, "MethodView"):
builtins.MethodView = FlaskMethodView
_NEEDS_METHOD_VIEW_CLEANUP = True
from controllers.common.fields import Parameters, Site
from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict
from models.model import IconType
def test_parameters_model_round_trip():
@@ -1,5 +1,3 @@
from __future__ import annotations
"""
Unit tests for the external dataset controller payload schemas.
@@ -8,6 +6,8 @@ in request constraints (e.g. max length changes) without exercising the
full Flask/RESTX request stack.
"""
from __future__ import annotations
import pytest
from pydantic import ValidationError
@@ -12,13 +12,6 @@ from flask import Flask
from flask.views import MethodView as FlaskMethodView
from werkzeug.exceptions import UnprocessableEntity
from tests.unit_tests.config_override import apply_config_overrides
_NEEDS_METHOD_VIEW_CLEANUP = False
if not hasattr(builtins, "MethodView"):
builtins.__dict__["MethodView"] = FlaskMethodView
_NEEDS_METHOD_VIEW_CLEANUP = True
import controllers.console.extension as module
from constants import HIDDEN_VALUE
from controllers.console.extension import (
@@ -28,9 +21,6 @@ from controllers.console.extension import (
CodeBasedExtensionAPI,
)
from enums import DeploymentEdition
if _NEEDS_METHOD_VIEW_CLEANUP:
del builtins.__dict__["MethodView"]
from machinery.context import RequestContext
from models.account import Account, AccountStatus, Tenant
from services.api_based_extension_application_service import (
@@ -40,6 +30,15 @@ from services.api_based_extension_application_service import (
APIBasedExtensionRecord,
APIBasedExtensionUpdate,
)
from tests.unit_tests.config_override import apply_config_overrides
_NEEDS_METHOD_VIEW_CLEANUP = False
if not hasattr(builtins, "MethodView"):
builtins.__dict__["MethodView"] = FlaskMethodView
_NEEDS_METHOD_VIEW_CLEANUP = True
if _NEEDS_METHOD_VIEW_CLEANUP:
del builtins.__dict__["MethodView"]
def unwrap(func: Callable[..., object]) -> Callable[..., object]:
@@ -1756,7 +1756,6 @@ def _run_case(
token_rows: dict[str, ResolvedRow],
monkeypatch: pytest.MonkeyPatch,
) -> TestResponse:
edition = scenario.edition or (
DeploymentEdition.ENTERPRISE if Trait.ENTERPRISE_ONLY in route.traits else DeploymentEdition.COMMUNITY
)
@@ -228,7 +228,6 @@ class TestAnnotationReplyActionStatusApi:
handler(api, app_model=app_model, job_id="j1", action="enable")
def test_error(self, monkeypatch: pytest.MonkeyPatch) -> None:
def _get(key):
if "error" in key:
return b"oops"
@@ -13,6 +13,15 @@ Focus on:
"""
import uuid
# =============================================================================
# API Endpoint Tests
#
# ``FileApi.post`` is wrapped by ``@validate_app_token(fetch_user_arg=...)``
# which preserves ``__wrapped__`` via ``functools.wraps``. We call the
# unwrapped method directly to bypass the decorator.
# =============================================================================
from inspect import unwrap
from unittest.mock import Mock, patch
import pytest
@@ -196,17 +205,6 @@ class TestFileUploadResponse:
assert hasattr(mock_file, "created_at")
# =============================================================================
# API Endpoint Tests
#
# ``FileApi.post`` is wrapped by ``@validate_app_token(fetch_user_arg=...)``
# which preserves ``__wrapped__`` via ``functools.wraps``. We call the
# unwrapped method directly to bypass the decorator.
# =============================================================================
from inspect import unwrap
@pytest.fixture
def mock_app_model():
from models import App
@@ -16,6 +16,55 @@ from pydantic import ValidationError
# ---------------------------------------------------------------------------
from controllers.web.app import AppAccessModeQuery
# ---------------------------------------------------------------------------
# audio.py models
# ---------------------------------------------------------------------------
from controllers.web.audio import TextToAudioPayload
# ---------------------------------------------------------------------------
# completion.py models
# ---------------------------------------------------------------------------
from controllers.web.completion import ChatMessagePayload, CompletionMessagePayload
# ---------------------------------------------------------------------------
# conversation.py models
# ---------------------------------------------------------------------------
from controllers.web.conversation import ConversationListQuery, ConversationRenamePayload
# ---------------------------------------------------------------------------
# forgot_password.py models
# ---------------------------------------------------------------------------
from controllers.web.forgot_password import (
ForgotPasswordCheckPayload,
ForgotPasswordResetPayload,
ForgotPasswordSendPayload,
)
# ---------------------------------------------------------------------------
# login.py models
# ---------------------------------------------------------------------------
from controllers.web.login import EmailCodeLoginSendPayload, EmailCodeLoginVerifyPayload, LoginPayload
# ---------------------------------------------------------------------------
# message.py models
# ---------------------------------------------------------------------------
from controllers.web.message import MessageFeedbackPayload, MessageListQuery, MessageMoreLikeThisQuery
# ---------------------------------------------------------------------------
# remote_files.py models
# ---------------------------------------------------------------------------
from controllers.web.remote_files import RemoteFileUploadPayload
# ---------------------------------------------------------------------------
# saved_message.py models
# ---------------------------------------------------------------------------
from controllers.web.saved_message import SavedMessageCreatePayload, SavedMessageListQuery
# ---------------------------------------------------------------------------
# workflow.py models
# ---------------------------------------------------------------------------
from controllers.web.workflow import WorkflowRunPayload
class TestAppAccessModeQuery:
def test_alias_resolution(self) -> None:
@@ -34,12 +83,6 @@ class TestAppAccessModeQuery:
assert q.app_code == "code1"
# ---------------------------------------------------------------------------
# audio.py models
# ---------------------------------------------------------------------------
from controllers.web.audio import TextToAudioPayload
class TestTextToAudioPayload:
def test_defaults(self) -> None:
p = TextToAudioPayload.model_validate({})
@@ -62,12 +105,6 @@ class TestTextToAudioPayload:
TextToAudioPayload(message_id="not-a-uuid")
# ---------------------------------------------------------------------------
# completion.py models
# ---------------------------------------------------------------------------
from controllers.web.completion import ChatMessagePayload, CompletionMessagePayload
class TestCompletionMessagePayload:
def test_defaults(self) -> None:
p = CompletionMessagePayload(inputs={})
@@ -117,12 +154,6 @@ class TestChatMessagePayload:
ChatMessagePayload(inputs={})
# ---------------------------------------------------------------------------
# conversation.py models
# ---------------------------------------------------------------------------
from controllers.web.conversation import ConversationListQuery, ConversationRenamePayload
class TestConversationListQuery:
def test_defaults(self) -> None:
q = ConversationListQuery()
@@ -183,12 +214,6 @@ class TestConversationRenamePayload:
assert p.name == "test"
# ---------------------------------------------------------------------------
# message.py models
# ---------------------------------------------------------------------------
from controllers.web.message import MessageFeedbackPayload, MessageListQuery, MessageMoreLikeThisQuery
class TestMessageListQuery:
def test_valid_query(self) -> None:
cid = str(uuid4())
@@ -249,12 +274,6 @@ class TestMessageMoreLikeThisQuery:
MessageMoreLikeThisQuery()
# ---------------------------------------------------------------------------
# remote_files.py models
# ---------------------------------------------------------------------------
from controllers.web.remote_files import RemoteFileUploadPayload
class TestRemoteFileUploadPayload:
def test_valid_url(self) -> None:
p = RemoteFileUploadPayload(url="https://example.com/file.pdf")
@@ -270,12 +289,6 @@ class TestRemoteFileUploadPayload:
RemoteFileUploadPayload()
# ---------------------------------------------------------------------------
# saved_message.py models
# ---------------------------------------------------------------------------
from controllers.web.saved_message import SavedMessageCreatePayload, SavedMessageListQuery
class TestSavedMessageListQuery:
def test_defaults(self) -> None:
q = SavedMessageListQuery()
@@ -309,12 +322,6 @@ class TestSavedMessageCreatePayload:
SavedMessageCreatePayload()
# ---------------------------------------------------------------------------
# workflow.py models
# ---------------------------------------------------------------------------
from controllers.web.workflow import WorkflowRunPayload
class TestWorkflowRunPayload:
def test_defaults(self) -> None:
p = WorkflowRunPayload(inputs={})
@@ -330,16 +337,6 @@ class TestWorkflowRunPayload:
WorkflowRunPayload()
# ---------------------------------------------------------------------------
# forgot_password.py models
# ---------------------------------------------------------------------------
from controllers.web.forgot_password import (
ForgotPasswordCheckPayload,
ForgotPasswordResetPayload,
ForgotPasswordSendPayload,
)
class TestForgotPasswordSendPayload:
def test_valid_email(self) -> None:
p = ForgotPasswordSendPayload(email="user@example.com")
@@ -384,12 +381,6 @@ class TestForgotPasswordResetPayload:
ForgotPasswordResetPayload(token="tok", new_password="123456789", password_confirm="123456789")
# ---------------------------------------------------------------------------
# login.py models
# ---------------------------------------------------------------------------
from controllers.web.login import EmailCodeLoginSendPayload, EmailCodeLoginVerifyPayload, LoginPayload
class TestLoginPayload:
def test_valid(self) -> None:
p = LoginPayload(email="a@b.com", password="Valid1234")
@@ -40,6 +40,7 @@ from services.remote_file_service import RemoteFileInvalidUrlError as RemoteFile
from services.remote_file_service import RemoteFileNotFoundError as RemoteFileNotFoundServiceError
from services.remote_file_service import RemoteFileUnavailableError as RemoteFileUnavailableServiceError
from services.remote_file_service import RemoteFileUrlBlockedError as RemoteFileUrlBlockedServiceError
from tests.unit_tests.model_factories import make_end_user
REMOTE_FILE_ERROR_CASES: tuple[tuple[type[RemoteFileError], type[BaseHTTPException], str, int], ...] = (
(RemoteFileInvalidUrlServiceError, RemoteFileInvalidUrlError, "remote_file_invalid_url", 400),
@@ -54,7 +55,6 @@ REMOTE_FILE_ERROR_CASES: tuple[tuple[type[RemoteFileError], type[BaseHTTPExcepti
502,
),
)
from tests.unit_tests.model_factories import make_end_user
def _app_model() -> App:
@@ -20,6 +20,8 @@ from sqlalchemy.orm import Session, sessionmaker
from core.model_manager import ModelManager
from core.tools.entities.tool_entities import ToolProviderType
from core.tools.utils.model_invocation_utils import InvokeModelError, ModelInvocationUtils
from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage
from graphon.model_runtime.entities.message_entities import AssistantPromptMessage
from graphon.model_runtime.entities.model_entities import ModelPropertyKey
from graphon.model_runtime.errors.invoke import (
InvokeAuthorizationError,
@@ -204,7 +206,3 @@ def test_invoke_error_mappings(exc, expected, sqlite_session: Session):
assert persisted.prompt_tokens == 5
assert persisted.answer_tokens == 0
mock_factory.assert_called_once_with(tenant_id=TENANT_ID, user_id=USER_ID)
from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage
from graphon.model_runtime.entities.message_entities import AssistantPromptMessage
@@ -64,7 +64,6 @@ def test_declares_variable():
def test_collect_container_errors():
# Not a container error
nodes = [{"id": "n1", "data": {"type": "llm"}}, {"id": "n2", "data": {"type": "code", "parentId": "n1"}}]
errors = WorkflowGenerator._collect_container_errors(nodes=nodes)
@@ -80,7 +79,6 @@ def test_collect_container_errors():
def test_collect_unknown_tools():
# Missing tool/provider info
nodes = [
{"id": "n1", "data": {"type": "tool", "provider_id": "", "tool_name": ""}},
@@ -95,7 +93,6 @@ def test_collect_unknown_tools():
def test_collect_unresolved_refs():
# Missing node ref
nodes = [{"id": "n1", "data": {"type": "llm", "prompt_template": [{"text": "{#unknown.var#}"}]}}]
# To trigger the parsing we need to mock _collect_refs_in_data behavior or let it parse naturally
@@ -118,7 +115,6 @@ def test_collect_unresolved_refs():
def test_collect_edge_cycle_errors():
# Self-loop
graph = {"nodes": [{"id": "n1"}], "edges": [{"source": "n1", "target": "n1"}]}
errors = WorkflowGenerator._collect_edge_cycle_errors(graph=graph, known_ids={"n1"})
@@ -127,7 +123,6 @@ def test_collect_edge_cycle_errors():
def test_collect_container_errors_empty_container():
# Empty container
nodes = [
{"id": "n1", "data": {"type": "iteration"}},
@@ -138,7 +133,6 @@ def test_collect_container_errors_empty_container():
def test_collect_container_errors_cycle():
# Ancestor cycle
nodes = [
{"id": "n1", "data": {"type": "iteration", "parentId": "n2"}},
@@ -150,7 +144,6 @@ def test_collect_container_errors_cycle():
def test_postprocess_graph_edges():
# Try calling _postprocess_graph directly to trigger _sanitize_node_ids
graph = {
"nodes": [{"id": "sys", "data": {"type": "start"}}, {"id": "bad id", "data": {"type": "llm"}}],
@@ -177,7 +170,6 @@ def test_document_extractor_start_vars():
def test_missing_terminal_mode_auto():
# Empty graph, should get MISSING_TERMINAL
graph = {"nodes": [{"id": "n1", "data": {"type": "start"}}], "edges": []}
@@ -29,7 +29,9 @@ from graphon.node_events import NodeRunResult, StreamChunkEvent, StreamCompleted
from graphon.nodes.code import CodeNode
from graphon.nodes.document_extractor import DocumentExtractorNode
from graphon.nodes.http_request import HttpRequestNode
from graphon.nodes.iteration import IterationNode
from graphon.nodes.llm import LLMNode
from graphon.nodes.loop import LoopNode
from graphon.nodes.parameter_extractor import ParameterExtractorNode
from graphon.nodes.question_classifier import QuestionClassifierNode
from graphon.nodes.template_transform import TemplateTransformNode
@@ -611,10 +613,6 @@ class MockDocumentExtractorNode(MockNodeMixin, DocumentExtractorNode):
)
from graphon.nodes.iteration import IterationNode
from graphon.nodes.loop import LoopNode
class MockIterationNode(MockNodeMixin, IterationNode):
"""Mock implementation of IterationNode that preserves mock configuration."""
@@ -1,5 +1,3 @@
from repositories.knowledge.keyword_table_repository import load_keyword_table
"""
Comprehensive unit tests for Dataset models.
@@ -77,6 +75,7 @@ from repositories.knowledge.dataset_read_repository import (
get_segment_dataset,
get_segment_document,
)
from repositories.knowledge.keyword_table_repository import load_keyword_table
from repositories.knowledge.segment_read_adapter import get_segment_attachments
from services.knowledge.dataset_read_service import get_dataset_queries
from tests.unit_tests.config_override import apply_config_overrides
@@ -134,7 +134,6 @@ def test_writes_commit_and_other_accounts_cannot_mutate(
def test_write_rolls_back_on_commit_failure(
seeded: WorkspaceRepository, sqlite_session_factory: sessionmaker[Session]
) -> None:
def fail_flush(_session: Session, _flush_context: object, _instances: object) -> None:
raise RuntimeError("write failed")
@@ -103,7 +103,6 @@ def test_send_code_normalizes_new_account_email_and_language(
service: AccountEmailRegistrationService,
boundaries: dict[str, MagicMock],
) -> None:
service.send_code(
remote_ip="127.0.0.1",
requested_email="New@Example.com",
@@ -260,7 +259,6 @@ def test_register_rejects_password_mismatch_before_reading_token(
service: AccountEmailRegistrationService,
boundaries: dict[str, MagicMock],
) -> None:
with pytest.raises(EmailRegistrationPasswordMismatchError):
service.register(
remote_ip="127.0.0.1",
@@ -147,7 +147,6 @@ def fixture() -> ServiceFixture:
def test_estimate_new_sources_resolves_each_variant_and_normalizes_rule(fixture: ServiceFixture) -> None:
result = fixture.service.estimate_new_sources(
_context(),
NewSourcesEstimateCommand(
@@ -258,7 +257,6 @@ def test_estimate_new_notion_source_maps_actor_credential_failure_without_runnin
def test_estimate_empty_new_sources_returns_zero_without_runner_call(fixture: ServiceFixture) -> None:
result = fixture.service.estimate_new_sources(
_context(),
NewSourcesEstimateCommand(sources=(), process_rule={"mode": "automatic"}),
@@ -309,7 +307,6 @@ def test_estimate_rejects_missing_saved_process_rule(fixture: ServiceFixture) ->
def test_estimate_empty_batch_raises_not_found_without_runner_call(fixture: ServiceFixture) -> None:
with pytest.raises(EstimateDocumentNotFoundError):
fixture.service.estimate_batch(_context(), dataset_id="dataset-1", batch="batch-1")
@@ -292,7 +292,6 @@ def _application(
def test_mutations_use_request_actor_without_a_flask_request(
sqlite_session_factory: sessionmaker[Session],
) -> None:
with sqlite_session_factory.begin() as session:
session.add(_dataset("dataset-1", "workspace-1"))
session.add(_document("document-1", "workspace-1", "dataset-1"))
@@ -389,7 +388,6 @@ def test_redis_state_uses_existing_keys_and_decodes_status() -> None:
def test_child_chunk_mutations_preserve_explicit_actors(
sqlite_session_factory: sessionmaker[Session],
) -> None:
with sqlite_session_factory.begin() as session:
session.add(_dataset("dataset-1", "workspace-1"))
document = _document("document-1", "workspace-1", "dataset-1")
@@ -1151,7 +1151,6 @@ def test_get_default_block_configs_success(rag_pipeline_service: RagPipelineServ
def test_get_default_block_config_success(rag_pipeline_service: RagPipelineServiceTestContext) -> None:
result = rag_pipeline_service.service.get_default_block_config(BuiltinNodeTypes.LLM)
assert result is not None
assert result["type"] == "llm"
@@ -1172,7 +1171,6 @@ def test_publish_workflow_raises_when_draft_workflow_missing(
def test_get_default_block_config_returns_none_when_mapped_type_missing(
mocker: MockerFixture, rag_pipeline_service: RagPipelineServiceTestContext
) -> None:
mocker.patch("services.rag_pipeline.rag_pipeline.get_node_type_classes_mapping", return_value={})
assert rag_pipeline_service.service.get_default_block_config(BuiltinNodeTypes.START) is None
@@ -1181,7 +1179,6 @@ def test_get_default_block_config_returns_none_when_mapped_type_missing(
def test_get_default_block_config_injects_http_request_filter(
mocker: MockerFixture, rag_pipeline_service: RagPipelineServiceTestContext
) -> None:
fake_node_cls = mocker.Mock()
fake_node_cls.get_default_config.return_value = {"type": "http-request"}
mocker.patch(
@@ -1613,7 +1610,6 @@ def test_set_datasource_variables_raises_when_node_id_missing(
def test_get_default_block_configs_skips_empty_configs(
mocker: MockerFixture, rag_pipeline_service: RagPipelineServiceTestContext
) -> None:
http_node = mocker.Mock()
http_node.get_default_config.return_value = {"type": "http-request"}
empty_node = mocker.Mock()
@@ -2076,7 +2072,6 @@ def test_publish_workflow_skips_dataset_update_for_non_knowledge_nodes(
def test_get_default_block_config_returns_none_when_default_empty(
mocker: MockerFixture, rag_pipeline_service: RagPipelineServiceTestContext
) -> None:
node_cls = mocker.Mock()
node_cls.get_default_config.return_value = None
mocker.patch(
@@ -101,7 +101,6 @@ def test_send_old_email_code_coerces_unexpected_phase_to_initial_state(
service: AccountChangeEmailService,
boundaries: dict[str, MagicMock],
) -> None:
token = service.send_code(
_context(),
requested_email="OLD@example.com",
@@ -444,7 +444,6 @@ def test_submit_form_by_token_missing_inputs(
sample_form_record: HumanInputFormRecord,
unbound_session_factory: sessionmaker[Session],
) -> None:
definition_with_input = FormDefinition(
form_content="hello",
inputs=[ParagraphInputConfig(output_variable_name="content")],