mirror of
https://github.com/langgenius/dify.git
synced 2026-08-29 03:45:08 +08:00
refactor(api): extract workflow statistic query service (#41271)
This commit is contained in:
@@ -1,28 +1,25 @@
|
||||
from flask import abort, jsonify
|
||||
from datetime import datetime
|
||||
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from werkzeug.exceptions import BadRequest
|
||||
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.flask_admission import console_account_admission
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_application_services import application_services
|
||||
from fields.base import ResponseModel
|
||||
from libs.datetime_utils import parse_time_range
|
||||
from libs.login import login_required
|
||||
from models.account import Account
|
||||
from models.enums import WorkflowRunTriggeredFrom
|
||||
from libs.helper import dump_response
|
||||
from libs.login import current_account_with_tenant
|
||||
from machinery.context import RequestContext
|
||||
from models.model import App, AppMode
|
||||
from repositories.factory import DifyAPIRepositoryFactory
|
||||
|
||||
|
||||
class WorkflowStatisticQuery(BaseModel):
|
||||
@@ -83,13 +80,22 @@ register_response_schema_models(
|
||||
)
|
||||
|
||||
|
||||
def _resolve_statistic_time_range(
|
||||
req_data: WorkflowStatisticQuery,
|
||||
) -> tuple[datetime | None, datetime | None, str]:
|
||||
timezone = current_account_with_tenant().account.timezone
|
||||
assert timezone is not None
|
||||
|
||||
try:
|
||||
start_date, end_date = parse_time_range(req_data.start, req_data.end, timezone)
|
||||
except ValueError as error:
|
||||
raise BadRequest(str(error)) from error
|
||||
|
||||
return start_date, end_date, timezone
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflow/statistics/daily-conversations")
|
||||
class WorkflowDailyRunsStatistic(Resource):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
session_maker = sessionmaker(bind=db.engine, expire_on_commit=False)
|
||||
self._workflow_run_repo = DifyAPIRepositoryFactory.create_api_workflow_run_repository(session_maker)
|
||||
|
||||
@console_ns.doc("get_workflow_daily_runs_statistic")
|
||||
@console_ns.doc(description="Get workflow daily runs statistics")
|
||||
@console_ns.doc(params={"app_id": "Application ID"})
|
||||
@@ -99,41 +105,27 @@ class WorkflowDailyRunsStatistic(Resource):
|
||||
"Daily runs statistics retrieved successfully",
|
||||
console_ns.models[WorkflowDailyRunsStatisticResponse.__name__],
|
||||
)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@console_account_admission(
|
||||
rbac_resource_scope=RBACResourceScope.APP,
|
||||
rbac_permission=RBACPermission.APP_MONITOR,
|
||||
)
|
||||
@get_app_model
|
||||
@model_validate(WorkflowStatisticQuery)
|
||||
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
|
||||
|
||||
assert account.timezone is not None
|
||||
|
||||
try:
|
||||
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
response_data = self._workflow_run_repo.get_daily_runs_statistics(
|
||||
tenant_id=app_model.tenant_id,
|
||||
def get(self, req_data: WorkflowStatisticQuery, request_context: RequestContext, app_model: App):
|
||||
start_date, end_date, timezone = _resolve_statistic_time_range(req_data)
|
||||
response_data = application_services().workflow_statistics.get_daily_runs(
|
||||
request_context,
|
||||
app_id=app_model.id,
|
||||
triggered_from=WorkflowRunTriggeredFrom.APP_RUN,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
timezone=account.timezone,
|
||||
timezone=timezone,
|
||||
)
|
||||
|
||||
return jsonify({"data": response_data})
|
||||
return dump_response(WorkflowDailyRunsStatisticResponse, {"data": response_data})
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflow/statistics/daily-terminals")
|
||||
class WorkflowDailyTerminalsStatistic(Resource):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
session_maker = sessionmaker(bind=db.engine, expire_on_commit=False)
|
||||
self._workflow_run_repo = DifyAPIRepositoryFactory.create_api_workflow_run_repository(session_maker)
|
||||
|
||||
@console_ns.doc("get_workflow_daily_terminals_statistic")
|
||||
@console_ns.doc(description="Get workflow daily terminals statistics")
|
||||
@console_ns.doc(params={"app_id": "Application ID"})
|
||||
@@ -143,41 +135,27 @@ class WorkflowDailyTerminalsStatistic(Resource):
|
||||
"Daily terminals statistics retrieved successfully",
|
||||
console_ns.models[WorkflowDailyTerminalsStatisticResponse.__name__],
|
||||
)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@console_account_admission(
|
||||
rbac_resource_scope=RBACResourceScope.APP,
|
||||
rbac_permission=RBACPermission.APP_MONITOR,
|
||||
)
|
||||
@get_app_model
|
||||
@model_validate(WorkflowStatisticQuery)
|
||||
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
|
||||
|
||||
assert account.timezone is not None
|
||||
|
||||
try:
|
||||
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
response_data = self._workflow_run_repo.get_daily_terminals_statistics(
|
||||
tenant_id=app_model.tenant_id,
|
||||
def get(self, req_data: WorkflowStatisticQuery, request_context: RequestContext, app_model: App):
|
||||
start_date, end_date, timezone = _resolve_statistic_time_range(req_data)
|
||||
response_data = application_services().workflow_statistics.get_daily_terminals(
|
||||
request_context,
|
||||
app_id=app_model.id,
|
||||
triggered_from=WorkflowRunTriggeredFrom.APP_RUN,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
timezone=account.timezone,
|
||||
timezone=timezone,
|
||||
)
|
||||
|
||||
return jsonify({"data": response_data})
|
||||
return dump_response(WorkflowDailyTerminalsStatisticResponse, {"data": response_data})
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflow/statistics/token-costs")
|
||||
class WorkflowDailyTokenCostStatistic(Resource):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
session_maker = sessionmaker(bind=db.engine, expire_on_commit=False)
|
||||
self._workflow_run_repo = DifyAPIRepositoryFactory.create_api_workflow_run_repository(session_maker)
|
||||
|
||||
@console_ns.doc("get_workflow_daily_token_cost_statistic")
|
||||
@console_ns.doc(description="Get workflow daily token cost statistics")
|
||||
@console_ns.doc(params={"app_id": "Application ID"})
|
||||
@@ -187,41 +165,27 @@ class WorkflowDailyTokenCostStatistic(Resource):
|
||||
"Daily token cost statistics retrieved successfully",
|
||||
console_ns.models[WorkflowDailyTokenCostStatisticResponse.__name__],
|
||||
)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@console_account_admission(
|
||||
rbac_resource_scope=RBACResourceScope.APP,
|
||||
rbac_permission=RBACPermission.APP_MONITOR,
|
||||
)
|
||||
@get_app_model
|
||||
@model_validate(WorkflowStatisticQuery)
|
||||
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
|
||||
|
||||
assert account.timezone is not None
|
||||
|
||||
try:
|
||||
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
response_data = self._workflow_run_repo.get_daily_token_cost_statistics(
|
||||
tenant_id=app_model.tenant_id,
|
||||
def get(self, req_data: WorkflowStatisticQuery, request_context: RequestContext, app_model: App):
|
||||
start_date, end_date, timezone = _resolve_statistic_time_range(req_data)
|
||||
response_data = application_services().workflow_statistics.get_daily_token_costs(
|
||||
request_context,
|
||||
app_id=app_model.id,
|
||||
triggered_from=WorkflowRunTriggeredFrom.APP_RUN,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
timezone=account.timezone,
|
||||
timezone=timezone,
|
||||
)
|
||||
|
||||
return jsonify({"data": response_data})
|
||||
return dump_response(WorkflowDailyTokenCostStatisticResponse, {"data": response_data})
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflow/statistics/average-app-interactions")
|
||||
class WorkflowAverageAppInteractionStatistic(Resource):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
session_maker = sessionmaker(bind=db.engine, expire_on_commit=False)
|
||||
self._workflow_run_repo = DifyAPIRepositoryFactory.create_api_workflow_run_repository(session_maker)
|
||||
|
||||
@console_ns.doc("get_workflow_average_app_interaction_statistic")
|
||||
@console_ns.doc(description="Get workflow average app interaction statistics")
|
||||
@console_ns.doc(params={"app_id": "Application ID"})
|
||||
@@ -231,29 +195,20 @@ class WorkflowAverageAppInteractionStatistic(Resource):
|
||||
"Average app interaction statistics retrieved successfully",
|
||||
console_ns.models[WorkflowAverageAppInteractionStatisticResponse.__name__],
|
||||
)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@console_account_admission(
|
||||
rbac_resource_scope=RBACResourceScope.APP,
|
||||
rbac_permission=RBACPermission.APP_MONITOR,
|
||||
)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
@model_validate(WorkflowStatisticQuery)
|
||||
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
|
||||
|
||||
assert account.timezone is not None
|
||||
|
||||
try:
|
||||
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
response_data = self._workflow_run_repo.get_average_app_interaction_statistics(
|
||||
tenant_id=app_model.tenant_id,
|
||||
def get(self, req_data: WorkflowStatisticQuery, request_context: RequestContext, app_model: App):
|
||||
start_date, end_date, timezone = _resolve_statistic_time_range(req_data)
|
||||
response_data = application_services().workflow_statistics.get_average_app_interactions(
|
||||
request_context,
|
||||
app_id=app_model.id,
|
||||
triggered_from=WorkflowRunTriggeredFrom.APP_RUN,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
timezone=account.timezone,
|
||||
timezone=timezone,
|
||||
)
|
||||
|
||||
return jsonify({"data": response_data})
|
||||
return dump_response(WorkflowAverageAppInteractionStatisticResponse, {"data": response_data})
|
||||
|
||||
@@ -27,6 +27,7 @@ from repositories.app_site_command_repository import AppSiteCommandRepository
|
||||
from repositories.data_source_api_key_auth_repository import SQLAlchemyDataSourceApiKeyAuthBindingRepository
|
||||
from repositories.data_source_oauth_binding_repository import SQLAlchemyDataSourceOAuthBindingRepository
|
||||
from repositories.explore_banner_query_repository import ExploreBannerQueryRepository
|
||||
from repositories.factory import DifyAPIRepositoryFactory
|
||||
from repositories.installation_state_repository import InstallationStateRepository
|
||||
from repositories.oauth_server_repository import RedisOAuthServerTokenRepository, SQLAlchemyOAuthServerRepository
|
||||
from repositories.recommended_app_catalog_repository import DatabaseRecommendedAppCatalogRepository
|
||||
@@ -110,6 +111,7 @@ from services.webapp_access_query_service import (
|
||||
WebAppAccessQueryService,
|
||||
WebAppAccessUnavailableError,
|
||||
)
|
||||
from services.workflow_statistic_query_service import WorkflowStatisticQueryService
|
||||
from services.workspace_member_query_service import WorkspaceMemberQueryService
|
||||
from services.workspace_member_role_resolver import DeploymentWorkspaceMemberRoleResolver
|
||||
from services.workspace_plan_gateway import DeploymentWorkspacePlanGateway
|
||||
@@ -173,6 +175,7 @@ class ApplicationServices:
|
||||
workspace_queries: WorkspaceQueryService
|
||||
workspace_member_queries: WorkspaceMemberQueryService
|
||||
tags: TagApplicationService
|
||||
workflow_statistics: WorkflowStatisticQueryService
|
||||
|
||||
def resolve_data_source_oauth(self, provider: str) -> DataSourceOAuthService:
|
||||
service = self.data_source_oauth.get(provider)
|
||||
@@ -407,6 +410,11 @@ def build_application_services(
|
||||
tags=TagApplicationService(
|
||||
tags=TagRepository(session_factory=database_client),
|
||||
),
|
||||
workflow_statistics=WorkflowStatisticQueryService(
|
||||
workflow_runs=DifyAPIRepositoryFactory.create_api_workflow_run_repository(
|
||||
session_maker=database_client,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Application service for workflow statistic queries."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from machinery.context import RequestContext
|
||||
from models.enums import WorkflowRunTriggeredFrom
|
||||
from repositories.api_workflow_run_repository import APIWorkflowRunRepository
|
||||
from repositories.types import (
|
||||
AverageInteractionStats,
|
||||
DailyRunsStats,
|
||||
DailyTerminalsStats,
|
||||
DailyTokenCostStats,
|
||||
)
|
||||
|
||||
|
||||
class WorkflowStatisticQueryService:
|
||||
def __init__(self, *, workflow_runs: APIWorkflowRunRepository) -> None:
|
||||
self._workflow_runs = workflow_runs
|
||||
|
||||
def get_daily_runs(
|
||||
self,
|
||||
context: RequestContext,
|
||||
*,
|
||||
app_id: str,
|
||||
start_date: datetime | None,
|
||||
end_date: datetime | None,
|
||||
timezone: str,
|
||||
) -> list[DailyRunsStats]:
|
||||
return self._workflow_runs.get_daily_runs_statistics(
|
||||
tenant_id=self._workspace_id(context),
|
||||
app_id=app_id,
|
||||
triggered_from=WorkflowRunTriggeredFrom.APP_RUN,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
timezone=timezone,
|
||||
)
|
||||
|
||||
def get_daily_terminals(
|
||||
self,
|
||||
context: RequestContext,
|
||||
*,
|
||||
app_id: str,
|
||||
start_date: datetime | None,
|
||||
end_date: datetime | None,
|
||||
timezone: str,
|
||||
) -> list[DailyTerminalsStats]:
|
||||
return self._workflow_runs.get_daily_terminals_statistics(
|
||||
tenant_id=self._workspace_id(context),
|
||||
app_id=app_id,
|
||||
triggered_from=WorkflowRunTriggeredFrom.APP_RUN,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
timezone=timezone,
|
||||
)
|
||||
|
||||
def get_daily_token_costs(
|
||||
self,
|
||||
context: RequestContext,
|
||||
*,
|
||||
app_id: str,
|
||||
start_date: datetime | None,
|
||||
end_date: datetime | None,
|
||||
timezone: str,
|
||||
) -> list[DailyTokenCostStats]:
|
||||
return self._workflow_runs.get_daily_token_cost_statistics(
|
||||
tenant_id=self._workspace_id(context),
|
||||
app_id=app_id,
|
||||
triggered_from=WorkflowRunTriggeredFrom.APP_RUN,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
timezone=timezone,
|
||||
)
|
||||
|
||||
def get_average_app_interactions(
|
||||
self,
|
||||
context: RequestContext,
|
||||
*,
|
||||
app_id: str,
|
||||
start_date: datetime | None,
|
||||
end_date: datetime | None,
|
||||
timezone: str,
|
||||
) -> list[AverageInteractionStats]:
|
||||
return self._workflow_runs.get_average_app_interaction_statistics(
|
||||
tenant_id=self._workspace_id(context),
|
||||
app_id=app_id,
|
||||
triggered_from=WorkflowRunTriggeredFrom.APP_RUN,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
timezone=timezone,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _workspace_id(context: RequestContext) -> str:
|
||||
workspace_id = context.active_workspace_id
|
||||
if workspace_id is None:
|
||||
raise RuntimeError("Console account admission did not resolve an active workspace")
|
||||
return workspace_id
|
||||
@@ -5,13 +5,14 @@ from __future__ import annotations
|
||||
import uuid
|
||||
from collections.abc import Iterator
|
||||
from inspect import unwrap
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from pydantic import ValidationError
|
||||
from sqlalchemy import Engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import BadRequest, NotFound
|
||||
|
||||
from controllers.console import console_ns
|
||||
@@ -70,7 +71,6 @@ from models import App, Site
|
||||
from models.account import Account, AccountStatus
|
||||
from models.engine import db
|
||||
from models.trigger import WorkflowWebhookTrigger
|
||||
from repositories.sqlalchemy_api_workflow_run_repository import DifyAPISQLAlchemyWorkflowRunRepository
|
||||
from services.app_site_service import (
|
||||
AppSiteAppNotFoundError,
|
||||
AppSiteChanges,
|
||||
@@ -106,6 +106,15 @@ def _make_app(
|
||||
return app
|
||||
|
||||
|
||||
def _make_request_context() -> RequestContext:
|
||||
return RequestContext(
|
||||
request_id="request-1",
|
||||
trace_id="trace-1",
|
||||
account_id=USER_ID,
|
||||
active_workspace_id=TENANT_ID,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def database_app() -> Iterator[Flask]:
|
||||
app = Flask(__name__)
|
||||
@@ -690,48 +699,162 @@ class TestWorkflowStatisticEndpoints:
|
||||
assert query.end is None
|
||||
|
||||
def test_workflow_daily_runs_statistic(self, database_app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
repository = DifyAPISQLAlchemyWorkflowRunRepository(
|
||||
session_maker=sessionmaker(bind=db.engine, expire_on_commit=False)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
repository,
|
||||
"get_daily_runs_statistics",
|
||||
lambda **_kwargs: [{"date": "2024-01-01"}],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
workflow_statistic_module.DifyAPIRepositoryFactory,
|
||||
"create_api_workflow_run_repository",
|
||||
lambda *_args, **_kwargs: repository,
|
||||
)
|
||||
service = MagicMock()
|
||||
service.get_daily_runs.return_value = [{"date": "2024-01-01", "runs": 2}]
|
||||
monkeypatch.setattr(
|
||||
workflow_statistic_module,
|
||||
"parse_time_range",
|
||||
lambda *_args, **_kwargs: (None, None),
|
||||
"application_services",
|
||||
lambda: SimpleNamespace(workflow_statistics=service),
|
||||
)
|
||||
self._patch_statistic_time_range(monkeypatch)
|
||||
|
||||
api = workflow_statistic_module.WorkflowDailyRunsStatistic()
|
||||
method = unwrap(api.get)
|
||||
request_context = _make_request_context()
|
||||
|
||||
with database_app.test_request_context("/"):
|
||||
account = _make_account()
|
||||
account.timezone = "UTC"
|
||||
response = method(api, WorkflowStatisticQuery(), account, app_model=_make_app("app-1", tenant_id="t1"))
|
||||
response = method(
|
||||
api,
|
||||
WorkflowStatisticQuery(),
|
||||
request_context,
|
||||
app_model=_make_app("app-1", tenant_id="t1"),
|
||||
)
|
||||
|
||||
assert response.get_json() == {"data": [{"date": "2024-01-01"}]}
|
||||
assert response == {"data": [{"date": "2024-01-01", "runs": 2}]}
|
||||
service.get_daily_runs.assert_called_once_with(
|
||||
request_context,
|
||||
app_id="app-1",
|
||||
start_date=None,
|
||||
end_date=None,
|
||||
timezone="UTC",
|
||||
)
|
||||
|
||||
def test_workflow_daily_terminals_statistic(self, database_app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
repository = DifyAPISQLAlchemyWorkflowRunRepository(
|
||||
session_maker=sessionmaker(bind=db.engine, expire_on_commit=False)
|
||||
service = MagicMock()
|
||||
service.get_daily_terminals.return_value = [{"date": "2024-01-02", "terminal_count": 3}]
|
||||
monkeypatch.setattr(
|
||||
workflow_statistic_module,
|
||||
"application_services",
|
||||
lambda: SimpleNamespace(workflow_statistics=service),
|
||||
)
|
||||
self._patch_statistic_time_range(monkeypatch)
|
||||
|
||||
api = workflow_statistic_module.WorkflowDailyTerminalsStatistic()
|
||||
method = unwrap(api.get)
|
||||
request_context = _make_request_context()
|
||||
|
||||
with database_app.test_request_context("/"):
|
||||
response = method(
|
||||
api,
|
||||
WorkflowStatisticQuery(),
|
||||
request_context,
|
||||
app_model=_make_app("app-1", tenant_id="t1"),
|
||||
)
|
||||
|
||||
assert response == {"data": [{"date": "2024-01-02", "terminal_count": 3}]}
|
||||
service.get_daily_terminals.assert_called_once_with(
|
||||
request_context,
|
||||
app_id="app-1",
|
||||
start_date=None,
|
||||
end_date=None,
|
||||
timezone="UTC",
|
||||
)
|
||||
|
||||
def test_workflow_daily_token_cost_statistic(
|
||||
self,
|
||||
database_app: Flask,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
service = MagicMock()
|
||||
service.get_daily_token_costs.return_value = [{"date": "2024-01-03", "token_count": 4}]
|
||||
monkeypatch.setattr(
|
||||
workflow_statistic_module,
|
||||
"application_services",
|
||||
lambda: SimpleNamespace(workflow_statistics=service),
|
||||
)
|
||||
self._patch_statistic_time_range(monkeypatch)
|
||||
|
||||
api = workflow_statistic_module.WorkflowDailyTokenCostStatistic()
|
||||
method = unwrap(api.get)
|
||||
request_context = _make_request_context()
|
||||
|
||||
with database_app.test_request_context("/"):
|
||||
response = method(
|
||||
api,
|
||||
WorkflowStatisticQuery(),
|
||||
request_context,
|
||||
app_model=_make_app("app-1", tenant_id="t1"),
|
||||
)
|
||||
|
||||
assert response == {"data": [{"date": "2024-01-03", "token_count": 4}]}
|
||||
service.get_daily_token_costs.assert_called_once_with(
|
||||
request_context,
|
||||
app_id="app-1",
|
||||
start_date=None,
|
||||
end_date=None,
|
||||
timezone="UTC",
|
||||
)
|
||||
|
||||
def test_workflow_average_app_interaction_statistic(
|
||||
self,
|
||||
database_app: Flask,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
service = MagicMock()
|
||||
service.get_average_app_interactions.return_value = [{"date": "2024-01-04", "interactions": 2.5}]
|
||||
monkeypatch.setattr(
|
||||
workflow_statistic_module,
|
||||
"application_services",
|
||||
lambda: SimpleNamespace(workflow_statistics=service),
|
||||
)
|
||||
self._patch_statistic_time_range(monkeypatch)
|
||||
|
||||
api = workflow_statistic_module.WorkflowAverageAppInteractionStatistic()
|
||||
method = unwrap(api.get)
|
||||
request_context = _make_request_context()
|
||||
|
||||
with database_app.test_request_context("/"):
|
||||
response = method(
|
||||
api,
|
||||
WorkflowStatisticQuery(),
|
||||
request_context,
|
||||
app_model=_make_app("app-1", tenant_id="t1"),
|
||||
)
|
||||
|
||||
assert response == {"data": [{"date": "2024-01-04", "interactions": 2.5}]}
|
||||
service.get_average_app_interactions.assert_called_once_with(
|
||||
request_context,
|
||||
app_id="app-1",
|
||||
start_date=None,
|
||||
end_date=None,
|
||||
timezone="UTC",
|
||||
)
|
||||
|
||||
def test_workflow_statistic_invalid_time_range(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
account = _make_account()
|
||||
account.timezone = "UTC"
|
||||
monkeypatch.setattr(
|
||||
workflow_statistic_module,
|
||||
"current_account_with_tenant",
|
||||
lambda: SimpleNamespace(account=account),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
repository,
|
||||
"get_daily_terminals_statistics",
|
||||
lambda **_kwargs: [{"date": "2024-01-02"}],
|
||||
workflow_statistic_module,
|
||||
"parse_time_range",
|
||||
MagicMock(side_effect=ValueError("invalid range")),
|
||||
)
|
||||
|
||||
with pytest.raises(BadRequest, match="invalid range"):
|
||||
workflow_statistic_module._resolve_statistic_time_range(WorkflowStatisticQuery())
|
||||
|
||||
@staticmethod
|
||||
def _patch_statistic_time_range(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
account = _make_account()
|
||||
account.timezone = "UTC"
|
||||
monkeypatch.setattr(
|
||||
workflow_statistic_module.DifyAPIRepositoryFactory,
|
||||
"create_api_workflow_run_repository",
|
||||
lambda *_args, **_kwargs: repository,
|
||||
workflow_statistic_module,
|
||||
"current_account_with_tenant",
|
||||
lambda: SimpleNamespace(account=account),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
workflow_statistic_module,
|
||||
@@ -739,16 +862,6 @@ class TestWorkflowStatisticEndpoints:
|
||||
lambda *_args, **_kwargs: (None, None),
|
||||
)
|
||||
|
||||
api = workflow_statistic_module.WorkflowDailyTerminalsStatistic()
|
||||
method = unwrap(api.get)
|
||||
|
||||
with database_app.test_request_context("/"):
|
||||
account = _make_account()
|
||||
account.timezone = "UTC"
|
||||
response = method(api, WorkflowStatisticQuery(), account, app_model=_make_app("app-1", tenant_id="t1"))
|
||||
|
||||
assert response.get_json() == {"data": [{"date": "2024-01-02"}]}
|
||||
|
||||
|
||||
class TestWorkflowTriggerEndpoints:
|
||||
def test_webhook_trigger_payload(self):
|
||||
|
||||
@@ -41,6 +41,7 @@ from services.init_validation_service import InvalidInitializationPasswordError
|
||||
from services.partner_tenant_binding_service import PartnerTenantBindingService
|
||||
from services.tag_application_service import TagApplicationService
|
||||
from services.webapp_access_query_service import WebAppAccessUnavailableError
|
||||
from services.workflow_statistic_query_service import WorkflowStatisticQueryService
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -111,6 +112,7 @@ def test_init_app_registers_services_for_the_current_app(
|
||||
services = ext_application_services.application_services()
|
||||
assert services is app.extensions["application_services"]
|
||||
assert services.init_validation.is_validated(session_validated=False) is False
|
||||
assert isinstance(services.workflow_statistics, WorkflowStatisticQueryService)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from machinery.context import RequestContext
|
||||
from models.enums import WorkflowRunTriggeredFrom
|
||||
from repositories.api_workflow_run_repository import APIWorkflowRunRepository
|
||||
from services.workflow_statistic_query_service import WorkflowStatisticQueryService
|
||||
|
||||
|
||||
def _request_context(*, workspace_id: str | None = "workspace-1") -> RequestContext:
|
||||
return RequestContext(
|
||||
request_id="request-1",
|
||||
trace_id="trace-1",
|
||||
account_id="account-1",
|
||||
active_workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
|
||||
def test_workflow_statistic_queries_delegate_to_workflow_run_repository() -> None:
|
||||
workflow_runs = MagicMock(spec=APIWorkflowRunRepository)
|
||||
workflow_runs.get_daily_runs_statistics.return_value = [{"date": "2024-01-01", "runs": 2}]
|
||||
workflow_runs.get_daily_terminals_statistics.return_value = [{"date": "2024-01-01", "terminal_count": 3}]
|
||||
workflow_runs.get_daily_token_cost_statistics.return_value = [{"date": "2024-01-01", "token_count": 4}]
|
||||
workflow_runs.get_average_app_interaction_statistics.return_value = [{"date": "2024-01-01", "interactions": 2.5}]
|
||||
service = WorkflowStatisticQueryService(workflow_runs=workflow_runs)
|
||||
context = _request_context()
|
||||
start_date = datetime(2024, 1, 1, tzinfo=UTC)
|
||||
end_date = datetime(2024, 1, 2, tzinfo=UTC)
|
||||
|
||||
assert service.get_daily_runs(
|
||||
context,
|
||||
app_id="app-1",
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
timezone="Asia/Shanghai",
|
||||
) == [{"date": "2024-01-01", "runs": 2}]
|
||||
assert service.get_daily_terminals(
|
||||
context,
|
||||
app_id="app-1",
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
timezone="Asia/Shanghai",
|
||||
) == [{"date": "2024-01-01", "terminal_count": 3}]
|
||||
assert service.get_daily_token_costs(
|
||||
context,
|
||||
app_id="app-1",
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
timezone="Asia/Shanghai",
|
||||
) == [{"date": "2024-01-01", "token_count": 4}]
|
||||
assert service.get_average_app_interactions(
|
||||
context,
|
||||
app_id="app-1",
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
timezone="Asia/Shanghai",
|
||||
) == [{"date": "2024-01-01", "interactions": 2.5}]
|
||||
|
||||
expected_arguments = {
|
||||
"tenant_id": "workspace-1",
|
||||
"app_id": "app-1",
|
||||
"triggered_from": WorkflowRunTriggeredFrom.APP_RUN,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"timezone": "Asia/Shanghai",
|
||||
}
|
||||
workflow_runs.get_daily_runs_statistics.assert_called_once_with(**expected_arguments)
|
||||
workflow_runs.get_daily_terminals_statistics.assert_called_once_with(**expected_arguments)
|
||||
workflow_runs.get_daily_token_cost_statistics.assert_called_once_with(**expected_arguments)
|
||||
workflow_runs.get_average_app_interaction_statistics.assert_called_once_with(**expected_arguments)
|
||||
|
||||
|
||||
def test_workflow_statistic_query_requires_active_workspace() -> None:
|
||||
workflow_runs = MagicMock(spec=APIWorkflowRunRepository)
|
||||
service = WorkflowStatisticQueryService(workflow_runs=workflow_runs)
|
||||
|
||||
with pytest.raises(RuntimeError, match="did not resolve an active workspace"):
|
||||
service.get_daily_runs(
|
||||
_request_context(workspace_id=None),
|
||||
app_id="app-1",
|
||||
start_date=None,
|
||||
end_date=None,
|
||||
timezone="UTC",
|
||||
)
|
||||
|
||||
workflow_runs.get_daily_runs_statistics.assert_not_called()
|
||||
Reference in New Issue
Block a user