From ff4611850ca5dbbd3c0efcffb91663388f05c395 Mon Sep 17 00:00:00 2001 From: phoenix Date: Thu, 8 Aug 2024 13:04:20 +0300 Subject: [PATCH] https://github.com/PhoenixNazarov/prompt-admin/issues/12 new unit test system --- .../api/job/unit_test_job.py | 14 +-- .../api/routers/config/__init__.py | 2 + .../api/routers/config/unit_test.py | 11 ++ .../api/service/preview_template_service.py | 3 +- .../api/service/prompt_load_service.py | 95 ++++++++------ .../api/service/prompt_sync_service.py | 38 +++--- .../api/service/prompt_unit_test_service.py | 116 +++++++++++------- .../data/entity/sync_data.py | 5 - .../data/entity/unit_test.py | 13 ++ server/promptadmin_server/data/init.sql | 37 ++++-- .../data/repository/unit_test_repository.py | 7 ++ .../data/service/mapping_entity_service.py | 10 ++ .../data/service/unit_test_service.py | 18 +++ 13 files changed, 244 insertions(+), 125 deletions(-) create mode 100644 server/promptadmin_server/api/routers/config/unit_test.py create mode 100644 server/promptadmin_server/data/entity/unit_test.py create mode 100644 server/promptadmin_server/data/repository/unit_test_repository.py create mode 100644 server/promptadmin_server/data/service/unit_test_service.py diff --git a/server/promptadmin_server/api/job/unit_test_job.py b/server/promptadmin_server/api/job/unit_test_job.py index 67e5922..bc9a8f2 100644 --- a/server/promptadmin_server/api/job/unit_test_job.py +++ b/server/promptadmin_server/api/job/unit_test_job.py @@ -4,29 +4,27 @@ from promptadmin_server.commons.dto import ViewParamsBuilder, ViewParamsFilter from promptadmin_server.commons.fastapi.background_task import BackgroundTask from promptadmin_server.data.entity.sync_data import SyncData +from promptadmin_server.data.service.mapping_entity_service import MappingEntityService from promptadmin_server.data.service.sync_data_service import SyncDataService class UnitTestJob(BackgroundTask): def __init__(self, sync_data_service: SyncDataService = None, - prompt_unit_test_service: PromptUnitTestService = None + prompt_unit_test_service: PromptUnitTestService = None, + mapping_entity_service: MappingEntityService = None ): self.sync_data_service = sync_data_service or SyncDataService() self.prompt_unit_test_service = prompt_unit_test_service or PromptUnitTestService() + self.mapping_entity_service = mapping_entity_service or MappingEntityService() async def start(self): await asyncio.sleep(60 * 10) while True: - view_params = ( - ViewParamsBuilder() - .filter(ViewParamsFilter(field=SyncData.test_status, value='wait')) - .build() - ) - sync_datas = await self.sync_data_service.find_by_view_params(view_params) + sync_datas = await self.sync_data_service.find_all() for i in sync_datas: sync_data = await self.sync_data_service.find_by_id(i.id) - await self.prompt_unit_test_service.process(sync_data) + await self.prompt_unit_test_service.process_sync_data(sync_data, delay=20) await asyncio.sleep(20) await asyncio.sleep(60 * 10) diff --git a/server/promptadmin_server/api/routers/config/__init__.py b/server/promptadmin_server/api/routers/config/__init__.py index 88be4b3..851d4f2 100644 --- a/server/promptadmin_server/api/routers/config/__init__.py +++ b/server/promptadmin_server/api/routers/config/__init__.py @@ -9,6 +9,7 @@ from .prompt_audit import router as prompt_audit_router from .account import router as account_router from .sync_data import router as sync_data_router +from .unit_test import router as unit_test_router router = APIRouter() @@ -21,3 +22,4 @@ router.include_router(prompt_audit_router, prefix='/prompt_audit') router.include_router(account_router, prefix='/account') router.include_router(sync_data_router, prefix='/sync_data') +router.include_router(unit_test_router, prefix='/unit_test') diff --git a/server/promptadmin_server/api/routers/config/unit_test.py b/server/promptadmin_server/api/routers/config/unit_test.py new file mode 100644 index 0000000..2980a2d --- /dev/null +++ b/server/promptadmin_server/api/routers/config/unit_test.py @@ -0,0 +1,11 @@ +from fastapi import APIRouter + +from promptadmin_server.api.routers.config.base_config_router_factory import bind_view +from promptadmin_server.data.entity.unit_test import UnitTest +from promptadmin_server.data.service.unit_test_service import UnitTestService + +router = APIRouter() + +unit_test_service = UnitTestService() + +bind_view(router, UnitTest, UnitTest, unit_test_service) diff --git a/server/promptadmin_server/api/service/preview_template_service.py b/server/promptadmin_server/api/service/preview_template_service.py index a700979..84f4a0d 100644 --- a/server/promptadmin_server/api/service/preview_template_service.py +++ b/server/promptadmin_server/api/service/preview_template_service.py @@ -1,4 +1,5 @@ import json +from typing import Any import jinja2 from promptadmin.output.parser_output_service import ParserOutputService @@ -46,7 +47,7 @@ async def preview_prompt(self, prompt: Prompt, context: dict[str, str] = None) - return self.preview(prompt.value, data) @staticmethod - def preview(prompt: str, context: dict[str, str]): + def preview(prompt: str, context: dict[str, Any]): environment = jinja2.Environment() template = environment.from_string(prompt) diff --git a/server/promptadmin_server/api/service/prompt_load_service.py b/server/promptadmin_server/api/service/prompt_load_service.py index 662e0d9..3b1d03f 100644 --- a/server/promptadmin_server/api/service/prompt_load_service.py +++ b/server/promptadmin_server/api/service/prompt_load_service.py @@ -7,12 +7,13 @@ from promptadmin_server.api.service.user_data import UserData from promptadmin_server.commons.dto import ViewParamsBuilder, ViewParamsFilter from promptadmin_server.data.entity.mapping import Mapping -from promptadmin_server.data.entity.mapping_entity import MappingEntity, MappingEntityData +from promptadmin_server.data.entity.mapping_entity import MappingEntity from promptadmin_server.data.entity.prompt_audit import PromptAudit from promptadmin_server.data.service.mapping_entity_service import MappingEntityService from promptadmin_server.data.service.mapping_service import MappingService from promptadmin_server.data.service.prompt_audit_service import PromptAuditService from promptadmin_server.data.service.sync_data_service import SyncDataService +from promptadmin_server.data.service.unit_test_service import UnitTestService from settings import SETTINGS logger = logging.getLogger(__name__) @@ -23,62 +24,72 @@ def __init__(self, mapping_service: MappingService = None, prompt_audit_service: PromptAuditService = None, mapping_entity_service: MappingEntityService = None, - sync_data_service: SyncDataService = None + sync_data_service: SyncDataService = None, + unit_test_service: UnitTestService = None ): self.mapping_service = mapping_service or MappingService() self.prompt_audit_service = prompt_audit_service or PromptAuditService() self.mapping_entity_service = mapping_entity_service or MappingEntityService() self.sync_data_service = sync_data_service or SyncDataService() + self.unit_test_service = unit_test_service or UnitTestService() - async def load_all(self): - async def load_mapping(mapping: Mapping) -> list[Prompt]: - try: - conn = await asyncpg.connect(SETTINGS.connections[mapping.connection_name]) - except Exception as e: - logger.error('Error connection database', exc_info=e) - return [] - - mapping_name = f', {mapping.field_name}' if mapping.field_name else '' - order_name = f', {mapping.field_order}' if mapping.field_order else '' - - row = await conn.fetch(f'SELECT id, {mapping.field} {mapping_name} {order_name} FROM {mapping.table}') - - result = [] - for i in row: - result.append( - Prompt( - mapping_id=mapping.id, - table=mapping.table, - field=mapping.field, - id=i['id'], - value=i[mapping.field], - name=i.get(mapping.field_name), - sort_value=i.get(mapping.field_order) - ) + @staticmethod + async def load_mapping(mapping: Mapping) -> list[Prompt]: + try: + conn = await asyncpg.connect(SETTINGS.connections[mapping.connection_name]) + except Exception as e: + logger.error('Error connection database', exc_info=e) + return [] + + mapping_name = f', {mapping.field_name}' if mapping.field_name else '' + order_name = f', {mapping.field_order}' if mapping.field_order else '' + + row = await conn.fetch(f'SELECT id, {mapping.field} {mapping_name} {order_name} FROM {mapping.table}') + + result = [] + for i in row: + result.append( + Prompt( + mapping_id=mapping.id, + table=mapping.table, + field=mapping.field, + id=i['id'], + value=i[mapping.field], + name=i.get(mapping.field_name), + sort_value=i.get(mapping.field_order) ) - return result + ) + return result + async def load_all(self): mappings = await self.mapping_service.find_all() - - res = await asyncio.gather(*[load_mapping(m) for m in mappings]) - + res = await asyncio.gather(*[self.load_mapping(m) for m in mappings]) out = [] for i in res: out += i - return out - async def load(self, mapping: Mapping, name: str) -> str: + async def load_mapping_name(self, mapping: Mapping, name: str) -> str: + return await self.load( + mapping.connection_name, + mapping.field, + mapping.table, + mapping.field_name, + name, + ) + + @staticmethod + async def load(connection_name: str, field: str, table: str, field_name: str, name: str) -> str: try: - conn = await asyncpg.connect(SETTINGS.connections[mapping.connection_name]) + conn = await asyncpg.connect(SETTINGS.connections[connection_name]) except Exception as e: logger.error('Error connection database', exc_info=e) return '' - name = f'WHERE {mapping.field_name} = \'{name}\'' + name = f'WHERE {field_name} = \'{name}\'' - row = await conn.fetch(f'SELECT {mapping.field} FROM {mapping.table} {name}') - return row[0].get(mapping.field, '') + row = await conn.fetch(f'SELECT {field} FROM {table} {name}') + return row[0].get(field, '') async def save(self, prompt: Prompt, user_data: UserData): mapping = await self.mapping_service.find_by_table_field(prompt.table, prompt.field) @@ -102,7 +113,6 @@ async def save(self, prompt: Prompt, user_data: UserData): .filter(ViewParamsFilter(field=MappingEntity.connection_name, value=mapping.connection_name)) .filter(ViewParamsFilter(field=MappingEntity.table, value=prompt.table)) .filter(ViewParamsFilter(field=MappingEntity.field, value=prompt.field)) - .filter(ViewParamsFilter(field=MappingEntity.name, value=prompt.name)) .filter(ViewParamsFilter(field=MappingEntity.entity, value='sync_data')) .build() ) @@ -110,5 +120,10 @@ async def save(self, prompt: Prompt, user_data: UserData): if len(mapping_entities) <= 0: return sync_data = await self.sync_data_service.find_by_id(mapping_entities[0].entity_id) - sync_data.test_status = 'wait' - await self.sync_data_service.save(sync_data) + if sync_data is None: + return + unit_test = self.unit_test_service.find_by_sync_data_name(sync_data.id, prompt.name) + if unit_test is None: + return + unit_test.test_status = 'wait' + await self.unit_test_service.save(unit_test) diff --git a/server/promptadmin_server/api/service/prompt_sync_service.py b/server/promptadmin_server/api/service/prompt_sync_service.py index f374fc7..dfd3bc1 100644 --- a/server/promptadmin_server/api/service/prompt_sync_service.py +++ b/server/promptadmin_server/api/service/prompt_sync_service.py @@ -29,27 +29,29 @@ async def sync_endpoint(self, endpoint: str, secret: str): async with httpx.AsyncClient() as client: try: r = await client.get(endpoint, headers={'Prompt-Admin-Secret': secret}) - result = r.json() - app = result['app'] - prompt_service_info = result['prompt_service_info'] - for i in prompt_service_info: - await self.sync( - app=app, - table=i['table'], - field=i['field'], - field_name=i['field_name'], - name=i['name'], - service_model_info=i['service_model_info'], - template_context_type=i['template_context_type'], - template_context_default=i['template_context_default'], - history_context_default=i['history_context_default'], - parsed_model_type=i['parsed_model_type'], - parsed_model_default=i['parsed_model_default'], - fail_parse_model_strategy=i['fail_parse_model_strategy'] - ) + await self.sync_json(r.json()) except Exception as e: logger.error('Sync exception', exc_info=e) + async def sync_json(self, result: dict): + app = result['app'] + prompt_service_info = result['prompt_service_info'] + for i in prompt_service_info: + await self.sync( + app=app, + table=i['table'], + field=i['field'], + field_name=i['field_name'], + name=i['name'], + service_model_info=i['service_model_info'], + template_context_type=i['template_context_type'], + template_context_default=i['template_context_default'], + history_context_default=i['history_context_default'], + parsed_model_type=i['parsed_model_type'], + parsed_model_default=i['parsed_model_default'], + fail_parse_model_strategy=i['fail_parse_model_strategy'] + ) + async def sync( self, app: str, diff --git a/server/promptadmin_server/api/service/prompt_unit_test_service.py b/server/promptadmin_server/api/service/prompt_unit_test_service.py index f1460eb..45d3993 100644 --- a/server/promptadmin_server/api/service/prompt_unit_test_service.py +++ b/server/promptadmin_server/api/service/prompt_unit_test_service.py @@ -1,3 +1,4 @@ +import asyncio import json from promptadmin.types import ModelServiceInfo, Message @@ -7,11 +8,12 @@ from promptadmin_server.api.service.prompt_load_service import PromptLoadService from promptadmin_server.commons.dto import ViewParamsBuilder, ViewParamsFilter from promptadmin_server.data.entity.mapping import Mapping -from promptadmin_server.data.entity.mapping_entity import MappingEntity from promptadmin_server.data.entity.sync_data import SyncData +from promptadmin_server.data.entity.unit_test import UnitTest from promptadmin_server.data.service.mapping_entity_service import MappingEntityService from promptadmin_server.data.service.mapping_service import MappingService from promptadmin_server.data.service.sync_data_service import SyncDataService +from promptadmin_server.data.service.unit_test_service import UnitTestService from settings import SETTINGS @@ -22,76 +24,104 @@ def __init__( sync_data_service: SyncDataService = None, prompt_load_service: PromptLoadService = None, mapping_service: MappingService = None, - mapping_entity_service: MappingEntityService = None + mapping_entity_service: MappingEntityService = None, + unit_test_service: UnitTestService = None ): self.preview_template_service = preview_template_service or PreviewTemplateService() self.sync_data_service = sync_data_service or SyncDataService() self.prompt_load_service = prompt_load_service or PromptLoadService() self.mapping_service = mapping_service or MappingService() self.mapping_entity_service = mapping_entity_service or MappingEntityService() + self.unit_test_service = unit_test_service or UnitTestService() - async def process(self, sync_data: SyncData): - # sync_data - sync_data.test_status = 'process' - sync_data.test_exception = None - sync_data.test_preview = None - sync_data.test_response_model = None - sync_data = await self.sync_data_service.save(sync_data) - - await self.preview(sync_data) - if sync_data.test_exception: + async def process_sync_data(self, sync_data: SyncData, delay: int): + mapping_entity = await self.mapping_entity_service.find_by_entity_id('sync_data', sync_data.id) + if len(mapping_entity) <= 0: return - await self.execution(sync_data) - if sync_data.test_exception: + mapping_entity = mapping_entity[0] + + view_params = ( + ViewParamsBuilder() + .filter(ViewParamsFilter(field=Mapping.table, value=mapping_entity.table)) + .filter(ViewParamsFilter(field=Mapping.field, value=mapping_entity.field)) + .filter(ViewParamsFilter(field=Mapping.connection_name, value=mapping_entity.connection_name)) + .build() + ) + mapping = await self.mapping_service.find_by_view_params_first(view_params) + if mapping is None: return - async def preview(self, sync_data: SyncData): - sync_data.test_status = 'preview' - try: - view_params = ( - ViewParamsBuilder() - .filter(ViewParamsFilter(field=MappingEntity.entity, value='sync_data')) - .filter(ViewParamsFilter(field=MappingEntity.entity_id, value=sync_data.id)) - .build() - ) - mapping_entity = await self.mapping_entity_service.find_by_view_params_first(view_params) - view_params = ( - ViewParamsBuilder() - .filter(ViewParamsFilter(field=Mapping.table, value=mapping_entity.table)) - .filter(ViewParamsFilter(field=Mapping.field, value=mapping_entity.field)) - .filter(ViewParamsFilter(field=Mapping.connection_name, value=mapping_entity.connection_name)) - .build() + if mapping_entity.name: + await self.process_sync_data_name(sync_data, mapping_entity.name, mapping) + else: + prompts = await self.prompt_load_service.load_mapping(mapping) + for prompt in prompts: + if prompt.name is None: + continue + await self.process_sync_data_name(sync_data, prompt.name, mapping) + await asyncio.sleep(delay) + + async def process_sync_data_name( + self, + sync_data: SyncData, + name: str, + mapping: Mapping, + ): + unit_test = await self.unit_test_service.find_by_sync_data_name(sync_data.id, name) + if unit_test and unit_test.test_status != 'wait': + return + if unit_test is None: + unit_test = UnitTest( + sync_data_id=sync_data.id, + name=name ) - mapping = await self.mapping_service.find_by_view_params_first(view_params) + await self.process(unit_test, sync_data, mapping) + + async def process(self, unit_test: UnitTest, sync_data: SyncData, mapping: Mapping): + unit_test.test_status = 'process' + unit_test.test_exception = None + unit_test.test_preview = None + unit_test.test_response_model = None + unit_test = await self.unit_test_service.save(unit_test) + + await self.preview(unit_test, sync_data, mapping) + if unit_test.test_exception: + return + await self.execution(unit_test, sync_data) + if unit_test.test_exception: + return + async def preview(self, unit_test: UnitTest, sync_data: SyncData, mapping: Mapping): + unit_test.test_status = 'preview' + try: connection = SETTINGS.connections.get(mapping.connection_name) if connection is None: return {} collect_vars = await VarService(connection).collect_vars() context = {} - context.update(collect_vars) + context.update({'var': collect_vars}) context.update(json.loads(sync_data.template_context_default)) - prompt = await self.prompt_load_service.load(mapping, mapping_entity.name) + prompt = await self.prompt_load_service.load_mapping_name(mapping, unit_test.name) preview = self.preview_template_service.preview(prompt, context) - sync_data.test_preview = preview + unit_test.test_preview = preview except Exception as e: - sync_data.test_exception = str(e) - await self.sync_data_service.save(sync_data) + unit_test.test_exception = str(e) + await self.unit_test_service.save(unit_test) - async def execution(self, sync_data: SyncData): - sync_data.test_status = 'execution' + async def execution(self, unit_test: UnitTest, sync_data: SyncData): + unit_test.test_status = 'execution' try: execution = await self.preview_template_service.execute( model_service_info=ModelServiceInfo.model_validate_json(sync_data.service_model_info), - prompt=sync_data.test_preview, + prompt=unit_test.test_preview, history=[Message.model_validate(i) for i in json.loads(sync_data.history_context_default)], parsed_model_type=json.loads(sync_data.parsed_model_type) if sync_data.parsed_model_type else None ) - sync_data.test_response_model = execution.response_model.json() + unit_test.test_response_model = execution.response_model.json() if execution.parsed_model_error: - sync_data.test_exception = 'Parsed model error' + unit_test.test_exception = 'Parsed model error' except Exception as e: - sync_data.test_exception = str(e) - await self.sync_data_service.save(sync_data) + unit_test.test_exception = str(e) + await self.unit_test_service.save(unit_test) diff --git a/server/promptadmin_server/data/entity/sync_data.py b/server/promptadmin_server/data/entity/sync_data.py index 3b42524..42fec4a 100644 --- a/server/promptadmin_server/data/entity/sync_data.py +++ b/server/promptadmin_server/data/entity/sync_data.py @@ -11,8 +11,3 @@ class SyncData(BaseEntity, table=True): parsed_model_type: str | None parsed_model_default: str | None fail_parse_model_strategy: str | None - - test_status: str = 'wait' - test_preview: str | None = None - test_response_model: str | None = None # json - test_exception: str | None = None diff --git a/server/promptadmin_server/data/entity/unit_test.py b/server/promptadmin_server/data/entity/unit_test.py new file mode 100644 index 0000000..aa160f7 --- /dev/null +++ b/server/promptadmin_server/data/entity/unit_test.py @@ -0,0 +1,13 @@ +from promptadmin_server.commons.entity import BaseEntity + + +class UnitTest(BaseEntity, table=True): + __tablename__ = 'pa_unit_test' + + sync_data_id: int + name: str + + test_status: str = 'wait' + test_preview: str | None = None + test_response_model: str | None = None # json + test_exception: str | None = None diff --git a/server/promptadmin_server/data/init.sql b/server/promptadmin_server/data/init.sql index f52d996..a09bfbb 100644 --- a/server/promptadmin_server/data/init.sql +++ b/server/promptadmin_server/data/init.sql @@ -172,17 +172,34 @@ create table pa_sync_data id serial constraint pa_sync_data_pk primary key, - time_create timestamp default now(), + time_create timestamp default now(), - service_model_info varchar(30000) not null, - template_context_type varchar(50000) not null, - template_context_default varchar(100000) not null, - history_context_default varchar(30000) not null, + service_model_info varchar(30000) not null, + template_context_type varchar(50000) not null, + template_context_default varchar(100000) not null, + history_context_default varchar(30000) not null, parsed_model_type varchar(30000), parsed_model_default varchar(30000), - fail_parse_model_strategy varchar(50), - test_status varchar(15) default 'wait' not null, - test_preview varchar(50000), - test_response_model varchar(30000), - test_exception varchar(10000) + fail_parse_model_strategy varchar(50) ); + + +-- PA_UNIT_TEST +create table pa_unit_test +( + id serial + constraint pa_unit_test_pk + primary key, + time_create timestamp default now(), + + sync_data_id integer + constraint pa_unit_test__sync_id_fk + references pa_sync_data + on delete CASCADE, + "name" varchar(128), + + test_status varchar(15) default 'wait' not null, + test_preview varchar(50000), + test_response_model varchar(30000), + test_exception varchar(10000) +); \ No newline at end of file diff --git a/server/promptadmin_server/data/repository/unit_test_repository.py b/server/promptadmin_server/data/repository/unit_test_repository.py new file mode 100644 index 0000000..97a613c --- /dev/null +++ b/server/promptadmin_server/data/repository/unit_test_repository.py @@ -0,0 +1,7 @@ +from promptadmin_server.commons.repository import AsyncBaseRepository +from promptadmin_server.data.entity.unit_test import UnitTest + + +class UnitTestRepository(AsyncBaseRepository[UnitTest]): + def __init__(self): + super().__init__(UnitTest) diff --git a/server/promptadmin_server/data/service/mapping_entity_service.py b/server/promptadmin_server/data/service/mapping_entity_service.py index 4b5c557..e23ac69 100644 --- a/server/promptadmin_server/data/service/mapping_entity_service.py +++ b/server/promptadmin_server/data/service/mapping_entity_service.py @@ -1,3 +1,4 @@ +from promptadmin_server.commons.dto import ViewParamsBuilder, ViewParamsFilter from promptadmin_server.commons.service.async_base_service import AsyncBaseService from promptadmin_server.data.entity.mapping_entity import MappingEntity from promptadmin_server.data.repository.mapping_entity_repository import MappingEntityRepository @@ -6,3 +7,12 @@ class MappingEntityService(AsyncBaseService[MappingEntity]): def __init__(self, repository: MappingEntityRepository = None): super().__init__(repository or MappingEntityRepository()) + + async def find_by_entity_id(self, entity: str, entity_id: int) -> list[MappingEntity]: + view_params = ( + ViewParamsBuilder() + .filter(ViewParamsFilter(field=MappingEntity.entity, value=entity)) + .filter(ViewParamsFilter(field=MappingEntity.entity_id, value=entity_id)) + .build() + ) + return await self.find_by_view_params(view_params) diff --git a/server/promptadmin_server/data/service/unit_test_service.py b/server/promptadmin_server/data/service/unit_test_service.py new file mode 100644 index 0000000..8747a17 --- /dev/null +++ b/server/promptadmin_server/data/service/unit_test_service.py @@ -0,0 +1,18 @@ +from promptadmin_server.commons.dto import ViewParamsBuilder, ViewParamsFilter +from promptadmin_server.commons.service.async_base_service import AsyncBaseService +from promptadmin_server.data.entity.unit_test import UnitTest +from promptadmin_server.data.repository.unit_test_repository import UnitTestRepository + + +class UnitTestService(AsyncBaseService[UnitTest]): + def __init__(self, repository: UnitTestRepository = None): + super().__init__(repository or UnitTestRepository()) + + async def find_by_sync_data_name(self, sync_data_id: int, name: str) -> UnitTest | None: + view_params = ( + ViewParamsBuilder() + .filter(ViewParamsFilter(field=UnitTest.sync_data_id, value=sync_data_id)) + .filter(ViewParamsFilter(field=UnitTest.name, value=name)) + .build() + ) + return await self.find_by_view_params_first(view_params)