From 24111facdd8a209bdbcaf3a10b99be515aa8e391 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:26:22 +0000 Subject: [PATCH 1/8] chore(i18n): sync translations with en-US (#34339) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> --- web/i18n/ar-TN/workflow.json | 3 +++ web/i18n/de-DE/workflow.json | 3 +++ web/i18n/es-ES/workflow.json | 3 +++ web/i18n/fa-IR/workflow.json | 3 +++ web/i18n/fr-FR/workflow.json | 3 +++ web/i18n/hi-IN/workflow.json | 3 +++ web/i18n/id-ID/workflow.json | 3 +++ web/i18n/it-IT/workflow.json | 3 +++ web/i18n/ja-JP/workflow.json | 3 +++ web/i18n/ko-KR/workflow.json | 3 +++ web/i18n/nl-NL/workflow.json | 3 +++ web/i18n/pl-PL/workflow.json | 3 +++ web/i18n/pt-BR/workflow.json | 3 +++ web/i18n/ro-RO/workflow.json | 3 +++ web/i18n/ru-RU/workflow.json | 3 +++ web/i18n/sl-SI/workflow.json | 3 +++ web/i18n/th-TH/workflow.json | 3 +++ web/i18n/tr-TR/workflow.json | 3 +++ web/i18n/uk-UA/workflow.json | 3 +++ web/i18n/vi-VN/workflow.json | 3 +++ web/i18n/zh-Hans/workflow.json | 3 +++ web/i18n/zh-Hant/workflow.json | 3 +++ 22 files changed, 66 insertions(+) diff --git a/web/i18n/ar-TN/workflow.json b/web/i18n/ar-TN/workflow.json index 24875380712..9396649c69a 100644 --- a/web/i18n/ar-TN/workflow.json +++ b/web/i18n/ar-TN/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "تصدير القيم السرية", "env.export.export": "تصدير DSL مع القيم السرية ", "env.export.ignore": "تصدير DSL", + "env.export.name": "الاسم", + "env.export.secret": "سري", "env.export.title": "تصدير متغيرات البيئة السرية؟", + "env.export.value": "القيمة", "env.modal.description": "الوصف", "env.modal.descriptionPlaceholder": "وصف المتغير", "env.modal.editTitle": "تعديل متغير بيئة", diff --git a/web/i18n/de-DE/workflow.json b/web/i18n/de-DE/workflow.json index 362eac19b64..66484506869 100644 --- a/web/i18n/de-DE/workflow.json +++ b/web/i18n/de-DE/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Geheime Werte exportieren", "env.export.export": "DSL mit geheimen Werten exportieren", "env.export.ignore": "DSL exportieren", + "env.export.name": "Name", + "env.export.secret": "Geheim", "env.export.title": "Geheime Umgebungsvariablen exportieren?", + "env.export.value": "Wert", "env.modal.description": "Beschreibung", "env.modal.descriptionPlaceholder": "Beschreiben Sie die Variable", "env.modal.editTitle": "Umgebungsvariable bearbeiten", diff --git a/web/i18n/es-ES/workflow.json b/web/i18n/es-ES/workflow.json index 393859a36ff..d23dd40a16b 100644 --- a/web/i18n/es-ES/workflow.json +++ b/web/i18n/es-ES/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Exportar valores secretos", "env.export.export": "Exportar DSL con valores secretos", "env.export.ignore": "Exportar DSL", + "env.export.name": "Nombre", + "env.export.secret": "Secreto", "env.export.title": "¿Exportar variables de entorno secretas?", + "env.export.value": "Valor", "env.modal.description": "Descripción", "env.modal.descriptionPlaceholder": "Describa la variable", "env.modal.editTitle": "Editar Variable de Entorno", diff --git a/web/i18n/fa-IR/workflow.json b/web/i18n/fa-IR/workflow.json index 7a8fca11f11..1b1bf59d94c 100644 --- a/web/i18n/fa-IR/workflow.json +++ b/web/i18n/fa-IR/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "خروجی مقادیر محرمانه", "env.export.export": "خروجی DSL با مقادیر محرمانه", "env.export.ignore": "خروجی DSL", + "env.export.name": "نام", + "env.export.secret": "محرمانه", "env.export.title": "آیا متغیرهای محیطی محرمانه صادر شوند؟", + "env.export.value": "مقدار", "env.modal.description": "توضیحات", "env.modal.descriptionPlaceholder": "توصیف متغیر", "env.modal.editTitle": "ویرایش متغیر محیطی", diff --git a/web/i18n/fr-FR/workflow.json b/web/i18n/fr-FR/workflow.json index 09d140445e4..c172dbf41de 100644 --- a/web/i18n/fr-FR/workflow.json +++ b/web/i18n/fr-FR/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Exporter les valeurs secrètes", "env.export.export": "Exporter les DSL avec des valeurs secrètes", "env.export.ignore": "Exporter DSL", + "env.export.name": "Nom", + "env.export.secret": "Secret", "env.export.title": "Exporter des variables d'environnement secrètes?", + "env.export.value": "valeur", "env.modal.description": "Description", "env.modal.descriptionPlaceholder": "Décrivez la variable", "env.modal.editTitle": "Editer titre", diff --git a/web/i18n/hi-IN/workflow.json b/web/i18n/hi-IN/workflow.json index fb9256536ed..2c14a31f55e 100644 --- a/web/i18n/hi-IN/workflow.json +++ b/web/i18n/hi-IN/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "गुप्त मान निर्यात करें", "env.export.export": "गुप्त मानों के साथ DSL निर्यात करें", "env.export.ignore": "DSL निर्यात करें", + "env.export.name": "नाम", + "env.export.secret": "गुप्त", "env.export.title": "गुप्त पर्यावरण चर निर्यात करें?", + "env.export.value": "मान", "env.modal.description": "विवरण", "env.modal.descriptionPlaceholder": "चर का वर्णन करें", "env.modal.editTitle": "पर्यावरण चर संपादित करें", diff --git a/web/i18n/id-ID/workflow.json b/web/i18n/id-ID/workflow.json index 76e80be7d7c..87d04157931 100644 --- a/web/i18n/id-ID/workflow.json +++ b/web/i18n/id-ID/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Mengekspor nilai rahasia", "env.export.export": "Mengekspor DSL dengan nilai rahasia", "env.export.ignore": "Ekspor DSL", + "env.export.name": "Nama", + "env.export.secret": "Rahasia", "env.export.title": "Mengekspor variabel lingkungan Rahasia?", + "env.export.value": "Nilai", "env.modal.description": "Deskripsi", "env.modal.descriptionPlaceholder": "Jelaskan variabel", "env.modal.editTitle": "Edit Variabel Lingkungan", diff --git a/web/i18n/it-IT/workflow.json b/web/i18n/it-IT/workflow.json index 519d7d7e2a7..d9e802c2b6c 100644 --- a/web/i18n/it-IT/workflow.json +++ b/web/i18n/it-IT/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Esporta valori segreti", "env.export.export": "Esporta DSL con valori segreti", "env.export.ignore": "Esporta DSL", + "env.export.name": "Nome", + "env.export.secret": "Segreto", "env.export.title": "Esportare variabili d'ambiente segrete?", + "env.export.value": "Valore", "env.modal.description": "Descrizione", "env.modal.descriptionPlaceholder": "Descrivi la variabile", "env.modal.editTitle": "Modifica Variabile d'Ambiente", diff --git a/web/i18n/ja-JP/workflow.json b/web/i18n/ja-JP/workflow.json index acf43b8ce85..0242249d301 100644 --- a/web/i18n/ja-JP/workflow.json +++ b/web/i18n/ja-JP/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "シークレット値を含む", "env.export.export": "シークレット値付きでエクスポート", "env.export.ignore": "DSL をエクスポート", + "env.export.name": "名前", + "env.export.secret": "シークレット", "env.export.title": "シークレット環境変数をエクスポートしますか?", + "env.export.value": "値", "env.modal.description": "説明", "env.modal.descriptionPlaceholder": "変数の説明を入力", "env.modal.editTitle": "環境変数を編集", diff --git a/web/i18n/ko-KR/workflow.json b/web/i18n/ko-KR/workflow.json index 24e8e634d3e..2709fa19170 100644 --- a/web/i18n/ko-KR/workflow.json +++ b/web/i18n/ko-KR/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "비밀 값 내보내기", "env.export.export": "비밀 값이 포함된 DSL 내보내기", "env.export.ignore": "DSL 내보내기", + "env.export.name": "이름", + "env.export.secret": "비밀", "env.export.title": "비밀 환경 변수를 내보내시겠습니까?", + "env.export.value": "값", "env.modal.description": "설명", "env.modal.descriptionPlaceholder": "변수에 대해 설명하세요", "env.modal.editTitle": "환경 변수 편집", diff --git a/web/i18n/nl-NL/workflow.json b/web/i18n/nl-NL/workflow.json index 891df72387e..eb18daf9f7d 100644 --- a/web/i18n/nl-NL/workflow.json +++ b/web/i18n/nl-NL/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Export secret values", "env.export.export": "Export DSL with secret values ", "env.export.ignore": "Export DSL", + "env.export.name": "Naam", + "env.export.secret": "Geheim", "env.export.title": "Export Secret environment variables?", + "env.export.value": "Waarde", "env.modal.description": "Description", "env.modal.descriptionPlaceholder": "Describe the variable", "env.modal.editTitle": "Edit Environment Variable", diff --git a/web/i18n/pl-PL/workflow.json b/web/i18n/pl-PL/workflow.json index 8aad8e0b716..adb639f2953 100644 --- a/web/i18n/pl-PL/workflow.json +++ b/web/i18n/pl-PL/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Eksportuj tajne wartości", "env.export.export": "Eksportuj DSL z tajnymi wartościami", "env.export.ignore": "Eksportuj DSL", + "env.export.name": "Nazwa", + "env.export.secret": "Tajny", "env.export.title": "Eksportować tajne zmienne środowiskowe?", + "env.export.value": "Wartość", "env.modal.description": "Opis", "env.modal.descriptionPlaceholder": "Opisz zmienną", "env.modal.editTitle": "Edytuj Zmienną Środowiskową", diff --git a/web/i18n/pt-BR/workflow.json b/web/i18n/pt-BR/workflow.json index dc986df82c3..aebf281b343 100644 --- a/web/i18n/pt-BR/workflow.json +++ b/web/i18n/pt-BR/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Exportar valores secretos", "env.export.export": "Exportar DSL com valores secretos", "env.export.ignore": "Exportar DSL", + "env.export.name": "Nome", + "env.export.secret": "Secreto", "env.export.title": "Exportar variáveis de ambiente secretas?", + "env.export.value": "Valor", "env.modal.description": "Descrição", "env.modal.descriptionPlaceholder": "Descreva a variável", "env.modal.editTitle": "Editar Variável de Ambiente", diff --git a/web/i18n/ro-RO/workflow.json b/web/i18n/ro-RO/workflow.json index e34ba420072..ff21dbb9fc2 100644 --- a/web/i18n/ro-RO/workflow.json +++ b/web/i18n/ro-RO/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Exportă valori secrete", "env.export.export": "Exportă DSL cu valori secrete", "env.export.ignore": "Exportă DSL", + "env.export.name": "Nume", + "env.export.secret": "Secret", "env.export.title": "Exportă variabile de mediu secrete?", + "env.export.value": "Valoare", "env.modal.description": "Descriere", "env.modal.descriptionPlaceholder": "Descrieți variabila", "env.modal.editTitle": "Editează Variabilă de Mediu", diff --git a/web/i18n/ru-RU/workflow.json b/web/i18n/ru-RU/workflow.json index 73cdad253ac..9b302c19f68 100644 --- a/web/i18n/ru-RU/workflow.json +++ b/web/i18n/ru-RU/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Экспортировать секретные значения", "env.export.export": "Экспортировать DSL с секретными значениями ", "env.export.ignore": "Экспортировать DSL", + "env.export.name": "Имя", + "env.export.secret": "Секрет", "env.export.title": "Экспортировать секретные переменные среды?", + "env.export.value": "Значение", "env.modal.description": "Описание", "env.modal.descriptionPlaceholder": "Опишите переменную", "env.modal.editTitle": "Редактировать переменную среды", diff --git a/web/i18n/sl-SI/workflow.json b/web/i18n/sl-SI/workflow.json index a20a30753d1..814fe5b117e 100644 --- a/web/i18n/sl-SI/workflow.json +++ b/web/i18n/sl-SI/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Izvozi tajne vrednosti", "env.export.export": "Izvozi DSL z skrivnimi vrednostmi", "env.export.ignore": "Izvoz DSL", + "env.export.name": "Ime", + "env.export.secret": "Skrivnost", "env.export.title": "Izvozi skrivne okoljske spremenljivke?", + "env.export.value": "Vrednost", "env.modal.description": "Opis", "env.modal.descriptionPlaceholder": "Opisujte spremenljivko", "env.modal.editTitle": "Uredi okoljsko spremenljivko", diff --git a/web/i18n/th-TH/workflow.json b/web/i18n/th-TH/workflow.json index 7819e884c3b..df656580eaf 100644 --- a/web/i18n/th-TH/workflow.json +++ b/web/i18n/th-TH/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "ส่งออกค่าข้อมูลลับ", "env.export.export": "ส่งออก DSL ด้วยค่าลับ", "env.export.ignore": "ส่งออก DSL", + "env.export.name": "ชื่อ", + "env.export.secret": "Secret", "env.export.title": "ส่งออกตัวแปรสภาพแวดล้อม Secret หรือไม่", + "env.export.value": "ค่า", "env.modal.description": "คำอธิบาย", "env.modal.descriptionPlaceholder": "อธิบายตัวแปร", "env.modal.editTitle": "แก้ไขตัวแปรสภาพแวดล้อม", diff --git a/web/i18n/tr-TR/workflow.json b/web/i18n/tr-TR/workflow.json index 58e6afae143..847f3a61f49 100644 --- a/web/i18n/tr-TR/workflow.json +++ b/web/i18n/tr-TR/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Gizli değerleri dışa aktar", "env.export.export": "Gizli değerlerle DSL'yi dışa aktar", "env.export.ignore": "DSL'yi dışa aktar", + "env.export.name": "Ad", + "env.export.secret": "Gizli", "env.export.title": "Gizli çevre değişkenleri dışa aktarılsın mı?", + "env.export.value": "Değer", "env.modal.description": "Açıklama", "env.modal.descriptionPlaceholder": "Değişkeni açıklayın", "env.modal.editTitle": "Çevre Değişkenini Düzenle", diff --git a/web/i18n/uk-UA/workflow.json b/web/i18n/uk-UA/workflow.json index 4fa95f6d57c..eaf7d551a71 100644 --- a/web/i18n/uk-UA/workflow.json +++ b/web/i18n/uk-UA/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Експортувати секретні значення", "env.export.export": "Експортувати DSL з секретними значеннями", "env.export.ignore": "Експортувати DSL", + "env.export.name": "Назва", + "env.export.secret": "Секрет", "env.export.title": "Експортувати секретні змінні середовища?", + "env.export.value": "Значення", "env.modal.description": "Опис", "env.modal.descriptionPlaceholder": "Опишіть змінну", "env.modal.editTitle": "Редагувати змінну середовища", diff --git a/web/i18n/vi-VN/workflow.json b/web/i18n/vi-VN/workflow.json index 3a8bdbaaf1d..94a4dfd8487 100644 --- a/web/i18n/vi-VN/workflow.json +++ b/web/i18n/vi-VN/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Xuất giá trị bí mật", "env.export.export": "Xuất DSL với giá trị bí mật", "env.export.ignore": "Xuất DSL", + "env.export.name": "Tên", + "env.export.secret": "Bí mật", "env.export.title": "Xuất biến môi trường bí mật?", + "env.export.value": "Giá trị", "env.modal.description": "Mô tả", "env.modal.descriptionPlaceholder": "Mô tả biến", "env.modal.editTitle": "Sửa Biến Môi Trường", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index 29a1f06350c..e6fc7d9ba9f 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "导出 secret 值", "env.export.export": "导出包含 Secret 值的 DSL", "env.export.ignore": "导出 DSL", + "env.export.name": "名称", + "env.export.secret": "Secret", "env.export.title": "导出 Secret 类型环境变量?", + "env.export.value": "值", "env.modal.description": "描述", "env.modal.descriptionPlaceholder": "变量的描述", "env.modal.editTitle": "编辑环境变量", diff --git a/web/i18n/zh-Hant/workflow.json b/web/i18n/zh-Hant/workflow.json index b739984977e..b7e34018d43 100644 --- a/web/i18n/zh-Hant/workflow.json +++ b/web/i18n/zh-Hant/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "導出機密值", "env.export.export": "導出帶有機密值的 DSL", "env.export.ignore": "導出 DSL", + "env.export.name": "名稱", + "env.export.secret": "機密", "env.export.title": "導出機密環境變數?", + "env.export.value": "值", "env.modal.description": "描述", "env.modal.descriptionPlaceholder": "描述此變數", "env.modal.editTitle": "編輯環境變數", From 90f94be2b3b30f848b9225dadd8d149151fa4ff3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:26:57 +0000 Subject: [PATCH 2/8] chore(i18n): sync translations with en-US (#34338) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> --- web/i18n/ar-TN/common.json | 9 +++++++++ web/i18n/de-DE/common.json | 9 +++++++++ web/i18n/es-ES/common.json | 9 +++++++++ web/i18n/fa-IR/common.json | 9 +++++++++ web/i18n/fr-FR/common.json | 9 +++++++++ web/i18n/hi-IN/common.json | 9 +++++++++ web/i18n/id-ID/common.json | 9 +++++++++ web/i18n/it-IT/common.json | 9 +++++++++ web/i18n/ja-JP/common.json | 9 +++++++++ web/i18n/ko-KR/common.json | 9 +++++++++ web/i18n/nl-NL/common.json | 9 +++++++++ web/i18n/pl-PL/common.json | 9 +++++++++ web/i18n/pt-BR/common.json | 9 +++++++++ web/i18n/ro-RO/common.json | 9 +++++++++ web/i18n/ru-RU/common.json | 9 +++++++++ web/i18n/sl-SI/common.json | 9 +++++++++ web/i18n/th-TH/common.json | 9 +++++++++ web/i18n/tr-TR/common.json | 9 +++++++++ web/i18n/uk-UA/common.json | 9 +++++++++ web/i18n/vi-VN/common.json | 9 +++++++++ web/i18n/zh-Hans/common.json | 9 +++++++++ web/i18n/zh-Hant/common.json | 9 +++++++++ 22 files changed, 198 insertions(+) diff --git a/web/i18n/ar-TN/common.json b/web/i18n/ar-TN/common.json index 3bc7c055646..2d81e44a718 100644 --- a/web/i18n/ar-TN/common.json +++ b/web/i18n/ar-TN/common.json @@ -162,6 +162,15 @@ "environment.development": "تطوير", "environment.testing": "اختبار", "error": "خطأ", + "errorBoundary.componentStack": "مكدس المكون:", + "errorBoundary.details": "تفاصيل الخطأ (التطوير فقط)", + "errorBoundary.errorCount": "حدث هذا الخطأ {{count}} مرة", + "errorBoundary.fallbackTitle": "عذراً! حدث خطأ ما", + "errorBoundary.message": "حدث خطأ غير متوقع أثناء عرض هذا المكون.", + "errorBoundary.reloadPage": "إعادة تحميل الصفحة", + "errorBoundary.title": "حدث خطأ ما", + "errorBoundary.tryAgain": "حاول مجدداً", + "errorBoundary.tryAgainCompact": "حاول مجدداً", "errorMsg.fieldRequired": "{{field}} مطلوب", "errorMsg.urlError": "يجب أن يبدأ العنوان بـ http:// أو https://", "feedback.content": "محتوى التعليق", diff --git a/web/i18n/de-DE/common.json b/web/i18n/de-DE/common.json index 8639a24f3e2..d33c1bcba14 100644 --- a/web/i18n/de-DE/common.json +++ b/web/i18n/de-DE/common.json @@ -162,6 +162,15 @@ "environment.development": "ENTWICKLUNG", "environment.testing": "TESTEN", "error": "Fehler", + "errorBoundary.componentStack": "Komponenten-Stack:", + "errorBoundary.details": "Fehlerdetails (Nur Entwicklung)", + "errorBoundary.errorCount": "Dieser Fehler ist {{count}} Mal aufgetreten", + "errorBoundary.fallbackTitle": "Hoppla! Etwas ist schiefgelaufen", + "errorBoundary.message": "Beim Rendern dieser Komponente ist ein unerwarteter Fehler aufgetreten.", + "errorBoundary.reloadPage": "Seite neu laden", + "errorBoundary.title": "Etwas ist schiefgelaufen", + "errorBoundary.tryAgain": "Erneut versuchen", + "errorBoundary.tryAgainCompact": "Erneut versuchen", "errorMsg.fieldRequired": "{{field}} ist erforderlich", "errorMsg.urlError": "Die URL sollte mit http:// oder https:// beginnen", "feedback.content": "Feedback-Inhalt", diff --git a/web/i18n/es-ES/common.json b/web/i18n/es-ES/common.json index 1b97ce680d9..38f9d10396a 100644 --- a/web/i18n/es-ES/common.json +++ b/web/i18n/es-ES/common.json @@ -162,6 +162,15 @@ "environment.development": "DESARROLLO", "environment.testing": "PRUEBAS", "error": "Error", + "errorBoundary.componentStack": "Pila de Componentes:", + "errorBoundary.details": "Detalles del Error (Solo Desarrollo)", + "errorBoundary.errorCount": "Este error ha ocurrido {{count}} veces", + "errorBoundary.fallbackTitle": "¡Vaya! Algo salió mal", + "errorBoundary.message": "Ocurrió un error inesperado al renderizar este componente.", + "errorBoundary.reloadPage": "Recargar Página", + "errorBoundary.title": "Algo salió mal", + "errorBoundary.tryAgain": "Intentar de Nuevo", + "errorBoundary.tryAgainCompact": "Intentar de nuevo", "errorMsg.fieldRequired": "{{field}} es requerido", "errorMsg.urlError": "la URL debe comenzar con http:// o https://", "feedback.content": "Contenido de retroalimentación", diff --git a/web/i18n/fa-IR/common.json b/web/i18n/fa-IR/common.json index d2b1e8158cf..686a6a59970 100644 --- a/web/i18n/fa-IR/common.json +++ b/web/i18n/fa-IR/common.json @@ -162,6 +162,15 @@ "environment.development": "توسعه", "environment.testing": "آزمایشی", "error": "خطا", + "errorBoundary.componentStack": "پشته کامپوننت:", + "errorBoundary.details": "جزئیات خطا (فقط در محیط توسعه)", + "errorBoundary.errorCount": "این خطا {{count}} بار رخ داده است", + "errorBoundary.fallbackTitle": "اوه! مشکلی پیش آمد", + "errorBoundary.message": "هنگام رندر کردن این کامپوننت، یک خطای غیرمنتظره رخ داد.", + "errorBoundary.reloadPage": "بارگذاری مجدد صفحه", + "errorBoundary.title": "مشکلی پیش آمد", + "errorBoundary.tryAgain": "تلاش مجدد", + "errorBoundary.tryAgainCompact": "تلاش مجدد", "errorMsg.fieldRequired": "{{field}} الزامی است", "errorMsg.urlError": "آدرس باید با http:// یا https:// شروع شود", "feedback.content": "محتوای بازخورد", diff --git a/web/i18n/fr-FR/common.json b/web/i18n/fr-FR/common.json index 8710c04c443..a2bc856bf7b 100644 --- a/web/i18n/fr-FR/common.json +++ b/web/i18n/fr-FR/common.json @@ -162,6 +162,15 @@ "environment.development": "DÉVELOPPEMENT", "environment.testing": "TESTER", "error": "Erreur", + "errorBoundary.componentStack": "Pile du Composant :", + "errorBoundary.details": "Détails de l'Erreur (Développement Uniquement)", + "errorBoundary.errorCount": "Cette erreur s'est produite {{count}} fois", + "errorBoundary.fallbackTitle": "Oups ! Quelque chose s'est mal passé", + "errorBoundary.message": "Une erreur inattendue s'est produite lors du rendu de ce composant.", + "errorBoundary.reloadPage": "Recharger la Page", + "errorBoundary.title": "Quelque chose s'est mal passé", + "errorBoundary.tryAgain": "Réessayer", + "errorBoundary.tryAgainCompact": "Réessayer", "errorMsg.fieldRequired": "{{field}} est obligatoire", "errorMsg.urlError": "L’URL doit commencer par http:// ou https://", "feedback.content": "Contenu des retours", diff --git a/web/i18n/hi-IN/common.json b/web/i18n/hi-IN/common.json index e61c96ca456..bbdd619b121 100644 --- a/web/i18n/hi-IN/common.json +++ b/web/i18n/hi-IN/common.json @@ -162,6 +162,15 @@ "environment.development": "विकास", "environment.testing": "परीक्षण", "error": "त्रुटि", + "errorBoundary.componentStack": "कंपोनेंट स्टैक:", + "errorBoundary.details": "त्रुटि विवरण (केवल डेवलपमेंट)", + "errorBoundary.errorCount": "यह त्रुटि {{count}} बार हुई है", + "errorBoundary.fallbackTitle": "उफ़! कुछ गलत हो गया", + "errorBoundary.message": "इस कंपोनेंट को रेंडर करते समय एक अप्रत्याशित त्रुटि हुई।", + "errorBoundary.reloadPage": "पेज रीलोड करें", + "errorBoundary.title": "कुछ गलत हो गया", + "errorBoundary.tryAgain": "पुनः प्रयास करें", + "errorBoundary.tryAgainCompact": "पुनः प्रयास करें", "errorMsg.fieldRequired": "{{field}} आवश्यक है", "errorMsg.urlError": "url को http:// या https:// से शुरू होना चाहिए", "feedback.content": "प्रतिक्रिया सामग्री", diff --git a/web/i18n/id-ID/common.json b/web/i18n/id-ID/common.json index 51cd4299924..81245aec7b2 100644 --- a/web/i18n/id-ID/common.json +++ b/web/i18n/id-ID/common.json @@ -162,6 +162,15 @@ "environment.development": "PENGEMBANGAN", "environment.testing": "PENGUJIAN", "error": "Kesalahan", + "errorBoundary.componentStack": "Tumpukan Komponen:", + "errorBoundary.details": "Detail Kesalahan (Hanya Pengembangan)", + "errorBoundary.errorCount": "Kesalahan ini telah terjadi {{count}} kali", + "errorBoundary.fallbackTitle": "Ups! Ada yang salah", + "errorBoundary.message": "Terjadi kesalahan tak terduga saat merender komponen ini.", + "errorBoundary.reloadPage": "Muat Ulang Halaman", + "errorBoundary.title": "Ada yang salah", + "errorBoundary.tryAgain": "Coba Lagi", + "errorBoundary.tryAgainCompact": "Coba lagi", "errorMsg.fieldRequired": "{{field}} wajib diisi", "errorMsg.urlError": "URL harus dimulai dengan http:// atau https://", "feedback.content": "Konten Umpan Balik", diff --git a/web/i18n/it-IT/common.json b/web/i18n/it-IT/common.json index 283c090ea85..b389ed09ef4 100644 --- a/web/i18n/it-IT/common.json +++ b/web/i18n/it-IT/common.json @@ -162,6 +162,15 @@ "environment.development": "SVILUPPO", "environment.testing": "TEST", "error": "Errore", + "errorBoundary.componentStack": "Stack del Componente:", + "errorBoundary.details": "Dettagli Errore (Solo Sviluppo)", + "errorBoundary.errorCount": "Questo errore si è verificato {{count}} volte", + "errorBoundary.fallbackTitle": "Ops! Qualcosa è andato storto", + "errorBoundary.message": "Si è verificato un errore imprevisto durante il rendering di questo componente.", + "errorBoundary.reloadPage": "Ricarica Pagina", + "errorBoundary.title": "Qualcosa è andato storto", + "errorBoundary.tryAgain": "Riprova", + "errorBoundary.tryAgainCompact": "Riprova", "errorMsg.fieldRequired": "{{field}} è obbligatorio", "errorMsg.urlError": "L'URL deve iniziare con http:// o https://", "feedback.content": "Contenuto del feedback", diff --git a/web/i18n/ja-JP/common.json b/web/i18n/ja-JP/common.json index a65d8e933c1..7b2e34e7573 100644 --- a/web/i18n/ja-JP/common.json +++ b/web/i18n/ja-JP/common.json @@ -162,6 +162,15 @@ "environment.development": "開発", "environment.testing": "テスト", "error": "エラー", + "errorBoundary.componentStack": "コンポーネントスタック:", + "errorBoundary.details": "エラー詳細(開発環境のみ)", + "errorBoundary.errorCount": "このエラーは{{count}}回発生しました", + "errorBoundary.fallbackTitle": "おっと!問題が発生しました", + "errorBoundary.message": "このコンポーネントのレンダリング中に予期しないエラーが発生しました。", + "errorBoundary.reloadPage": "ページを再読み込み", + "errorBoundary.title": "問題が発生しました", + "errorBoundary.tryAgain": "再試行", + "errorBoundary.tryAgainCompact": "再試行", "errorMsg.fieldRequired": "{{field}}は必要です", "errorMsg.urlError": "URL は http:// または https:// で始まる必要があります", "feedback.content": "フィードバックコンテンツ", diff --git a/web/i18n/ko-KR/common.json b/web/i18n/ko-KR/common.json index e50a7c24283..bdd08a77152 100644 --- a/web/i18n/ko-KR/common.json +++ b/web/i18n/ko-KR/common.json @@ -162,6 +162,15 @@ "environment.development": "개발", "environment.testing": "테스트", "error": "오류", + "errorBoundary.componentStack": "컴포넌트 스택:", + "errorBoundary.details": "오류 세부 정보 (개발 환경 전용)", + "errorBoundary.errorCount": "이 오류가 {{count}}번 발생했습니다", + "errorBoundary.fallbackTitle": "이런! 문제가 발생했습니다", + "errorBoundary.message": "이 컴포넌트를 렌더링하는 동안 예기치 않은 오류가 발생했습니다.", + "errorBoundary.reloadPage": "페이지 새로고침", + "errorBoundary.title": "문제가 발생했습니다", + "errorBoundary.tryAgain": "다시 시도", + "errorBoundary.tryAgainCompact": "다시 시도", "errorMsg.fieldRequired": "{{field}}는 필수입니다.", "errorMsg.urlError": "URL 은 http:// 또는 https:// 로 시작해야 합니다.", "feedback.content": "피드백 내용", diff --git a/web/i18n/nl-NL/common.json b/web/i18n/nl-NL/common.json index fb1b332a0c9..592b85dc63f 100644 --- a/web/i18n/nl-NL/common.json +++ b/web/i18n/nl-NL/common.json @@ -162,6 +162,15 @@ "environment.development": "DEVELOPMENT", "environment.testing": "TESTING", "error": "Error", + "errorBoundary.componentStack": "Componentstack:", + "errorBoundary.details": "Foutdetails (Alleen Ontwikkeling)", + "errorBoundary.errorCount": "Deze fout is {{count}} keer opgetreden", + "errorBoundary.fallbackTitle": "Oeps! Er is iets fout gegaan", + "errorBoundary.message": "Er is een onverwachte fout opgetreden bij het renderen van dit component.", + "errorBoundary.reloadPage": "Pagina herladen", + "errorBoundary.title": "Er is iets fout gegaan", + "errorBoundary.tryAgain": "Opnieuw proberen", + "errorBoundary.tryAgainCompact": "Opnieuw proberen", "errorMsg.fieldRequired": "{{field}} is required", "errorMsg.urlError": "url should start with http:// or https://", "feedback.content": "Feedback Content", diff --git a/web/i18n/pl-PL/common.json b/web/i18n/pl-PL/common.json index 130950a57c9..5e693dd2f57 100644 --- a/web/i18n/pl-PL/common.json +++ b/web/i18n/pl-PL/common.json @@ -162,6 +162,15 @@ "environment.development": "ROZWOJOWA", "environment.testing": "TESTOWANIE", "error": "Błąd", + "errorBoundary.componentStack": "Stos komponentów:", + "errorBoundary.details": "Szczegóły błędu (tylko tryb deweloperski)", + "errorBoundary.errorCount": "Ten błąd wystąpił {{count}} razy", + "errorBoundary.fallbackTitle": "Ups! Coś poszło nie tak", + "errorBoundary.message": "Wystąpił nieoczekiwany błąd podczas renderowania tego komponentu.", + "errorBoundary.reloadPage": "Odśwież stronę", + "errorBoundary.title": "Coś poszło nie tak", + "errorBoundary.tryAgain": "Spróbuj ponownie", + "errorBoundary.tryAgainCompact": "Spróbuj ponownie", "errorMsg.fieldRequired": "{{field}} jest wymagane", "errorMsg.urlError": "Adres URL powinien zaczynać się od http:// lub https://", "feedback.content": "Treść opinii", diff --git a/web/i18n/pt-BR/common.json b/web/i18n/pt-BR/common.json index 6840bb964b0..2d40cf1ee71 100644 --- a/web/i18n/pt-BR/common.json +++ b/web/i18n/pt-BR/common.json @@ -162,6 +162,15 @@ "environment.development": "DESENVOLVIMENTO", "environment.testing": "TESTE", "error": "Erro", + "errorBoundary.componentStack": "Stack do Componente:", + "errorBoundary.details": "Detalhes do Erro (Somente Desenvolvimento)", + "errorBoundary.errorCount": "Este erro ocorreu {{count}} vezes", + "errorBoundary.fallbackTitle": "Ops! Algo deu errado", + "errorBoundary.message": "Ocorreu um erro inesperado ao renderizar este componente.", + "errorBoundary.reloadPage": "Recarregar Página", + "errorBoundary.title": "Algo deu errado", + "errorBoundary.tryAgain": "Tentar Novamente", + "errorBoundary.tryAgainCompact": "Tentar novamente", "errorMsg.fieldRequired": "{{field}} é obrigatório", "errorMsg.urlError": "URL deve começar com http:// ou https://", "feedback.content": "Conteúdo do feedback", diff --git a/web/i18n/ro-RO/common.json b/web/i18n/ro-RO/common.json index 306439768b9..84ae4cdce0c 100644 --- a/web/i18n/ro-RO/common.json +++ b/web/i18n/ro-RO/common.json @@ -162,6 +162,15 @@ "environment.development": "DEZVOLTARE", "environment.testing": "TESTARE", "error": "Eroare", + "errorBoundary.componentStack": "Stiva componentelor:", + "errorBoundary.details": "Detalii eroare (Numai în dezvoltare)", + "errorBoundary.errorCount": "Această eroare a apărut de {{count}} ori", + "errorBoundary.fallbackTitle": "Ups! Ceva a mers prost", + "errorBoundary.message": "A apărut o eroare neașteptată la redarea acestei componente.", + "errorBoundary.reloadPage": "Reîncarcă pagina", + "errorBoundary.title": "Ceva a mers prost", + "errorBoundary.tryAgain": "Încearcă din nou", + "errorBoundary.tryAgainCompact": "Încearcă din nou", "errorMsg.fieldRequired": "{{field}} este obligatoriu", "errorMsg.urlError": "URL-ul ar trebui să înceapă cu http:// sau https://", "feedback.content": "Conținut de feedback", diff --git a/web/i18n/ru-RU/common.json b/web/i18n/ru-RU/common.json index aec9a694837..ba6a3f60786 100644 --- a/web/i18n/ru-RU/common.json +++ b/web/i18n/ru-RU/common.json @@ -162,6 +162,15 @@ "environment.development": "РАЗРАБОТКА", "environment.testing": "ТЕСТИРОВАНИЕ", "error": "Ошибка", + "errorBoundary.componentStack": "Стек компонентов:", + "errorBoundary.details": "Детали ошибки (только разработка)", + "errorBoundary.errorCount": "Эта ошибка произошла {{count}} раз(а)", + "errorBoundary.fallbackTitle": "Упс! Что-то пошло не так", + "errorBoundary.message": "При рендеринге этого компонента произошла непредвиденная ошибка.", + "errorBoundary.reloadPage": "Перезагрузить страницу", + "errorBoundary.title": "Что-то пошло не так", + "errorBoundary.tryAgain": "Попробовать снова", + "errorBoundary.tryAgainCompact": "Попробовать снова", "errorMsg.fieldRequired": "{{field}} обязательно", "errorMsg.urlError": "URL должен начинаться с http:// или https://", "feedback.content": "Содержимое обратной связи", diff --git a/web/i18n/sl-SI/common.json b/web/i18n/sl-SI/common.json index 6ec4fe430cd..a0e71301761 100644 --- a/web/i18n/sl-SI/common.json +++ b/web/i18n/sl-SI/common.json @@ -162,6 +162,15 @@ "environment.development": "RAZVOJ", "environment.testing": "PREIZKUŠANJE", "error": "Napaka", + "errorBoundary.componentStack": "Sklad komponent:", + "errorBoundary.details": "Podrobnosti napake (samo razvojna okolja)", + "errorBoundary.errorCount": "Ta napaka se je pojavila {{count}} krat", + "errorBoundary.fallbackTitle": "Ojoj! Nekaj je šlo narobe", + "errorBoundary.message": "Med prikazovanjem te komponente je prišlo do nepričakovane napake.", + "errorBoundary.reloadPage": "Znova naloži stran", + "errorBoundary.title": "Nekaj je šlo narobe", + "errorBoundary.tryAgain": "Poskusi znova", + "errorBoundary.tryAgainCompact": "Poskusi znova", "errorMsg.fieldRequired": "{{field}} je obvezno", "errorMsg.urlError": "url mora začeti z http:// ali https://", "feedback.content": "Vsebina povratnih informacij", diff --git a/web/i18n/th-TH/common.json b/web/i18n/th-TH/common.json index 6eed5eba936..eb45b7e796e 100644 --- a/web/i18n/th-TH/common.json +++ b/web/i18n/th-TH/common.json @@ -162,6 +162,15 @@ "environment.development": "พัฒนาการ", "environment.testing": "การทดสอบ", "error": "ข้อผิดพลาด", + "errorBoundary.componentStack": "สแตกของคอมโพเนนต์:", + "errorBoundary.details": "รายละเอียดข้อผิดพลาด (สำหรับการพัฒนาเท่านั้น)", + "errorBoundary.errorCount": "ข้อผิดพลาดนี้เกิดขึ้น {{count}} ครั้ง", + "errorBoundary.fallbackTitle": "อุ๊ปส์! มีบางอย่างผิดพลาด", + "errorBoundary.message": "เกิดข้อผิดพลาดที่ไม่คาดคิดขณะแสดงผลคอมโพเนนต์นี้", + "errorBoundary.reloadPage": "โหลดหน้าใหม่", + "errorBoundary.title": "มีบางอย่างผิดพลาด", + "errorBoundary.tryAgain": "ลองอีกครั้ง", + "errorBoundary.tryAgainCompact": "ลองอีกครั้ง", "errorMsg.fieldRequired": "{{field}} เป็นสิ่งจําเป็น", "errorMsg.urlError": "url ควรขึ้นต้นด้วย http:// หรือ https://", "feedback.content": "เนื้อหาข้อเสนอแนะ", diff --git a/web/i18n/tr-TR/common.json b/web/i18n/tr-TR/common.json index 66e895fd2ba..f8877e75caa 100644 --- a/web/i18n/tr-TR/common.json +++ b/web/i18n/tr-TR/common.json @@ -162,6 +162,15 @@ "environment.development": "GELİŞTİRME", "environment.testing": "TEST", "error": "Hata", + "errorBoundary.componentStack": "Bileşen Yığını:", + "errorBoundary.details": "Hata Ayrıntıları (Yalnızca Geliştirme)", + "errorBoundary.errorCount": "Bu hata {{count}} kez oluştu", + "errorBoundary.fallbackTitle": "Hay aksi! Bir şeyler ters gitti", + "errorBoundary.message": "Bu bileşen işlenirken beklenmedik bir hata oluştu.", + "errorBoundary.reloadPage": "Sayfayı Yenile", + "errorBoundary.title": "Bir şeyler ters gitti", + "errorBoundary.tryAgain": "Tekrar Dene", + "errorBoundary.tryAgainCompact": "Tekrar dene", "errorMsg.fieldRequired": "{{field}} gereklidir", "errorMsg.urlError": "URL http:// veya https:// ile başlamalıdır", "feedback.content": "Geri Bildirim İçeriği", diff --git a/web/i18n/uk-UA/common.json b/web/i18n/uk-UA/common.json index 806cbede3d7..2eb457c835a 100644 --- a/web/i18n/uk-UA/common.json +++ b/web/i18n/uk-UA/common.json @@ -162,6 +162,15 @@ "environment.development": "РОЗРОБКА", "environment.testing": "ТЕСТУВАННЯ", "error": "Помилка", + "errorBoundary.componentStack": "Стек компонентів:", + "errorBoundary.details": "Деталі помилки (тільки розробка)", + "errorBoundary.errorCount": "Ця помилка сталася {{count}} раз(ів)", + "errorBoundary.fallbackTitle": "Ой! Щось пішло не так", + "errorBoundary.message": "Під час відображення цього компонента сталася непередбачена помилка.", + "errorBoundary.reloadPage": "Перезавантажити сторінку", + "errorBoundary.title": "Щось пішло не так", + "errorBoundary.tryAgain": "Спробувати знову", + "errorBoundary.tryAgainCompact": "Спробувати знову", "errorMsg.fieldRequired": "{{field}} є обов'язковим", "errorMsg.urlError": "URL-адреса повинна починатися з http:// або https://", "feedback.content": "Зміст відгуку", diff --git a/web/i18n/vi-VN/common.json b/web/i18n/vi-VN/common.json index 820bfdfdab6..13e74daccf1 100644 --- a/web/i18n/vi-VN/common.json +++ b/web/i18n/vi-VN/common.json @@ -162,6 +162,15 @@ "environment.development": "DEVELOPMENT", "environment.testing": "TESTING", "error": "Lỗi", + "errorBoundary.componentStack": "Ngăn xếp thành phần:", + "errorBoundary.details": "Chi tiết lỗi (Chỉ dành cho phát triển)", + "errorBoundary.errorCount": "Lỗi này đã xảy ra {{count}} lần", + "errorBoundary.fallbackTitle": "Ôi! Đã xảy ra sự cố", + "errorBoundary.message": "Đã xảy ra lỗi không mong muốn khi hiển thị thành phần này.", + "errorBoundary.reloadPage": "Tải lại trang", + "errorBoundary.title": "Đã xảy ra sự cố", + "errorBoundary.tryAgain": "Thử lại", + "errorBoundary.tryAgainCompact": "Thử lại", "errorMsg.fieldRequired": "{{field}} là bắt buộc", "errorMsg.urlError": "URL phải bắt đầu bằng http:// hoặc https://", "feedback.content": "Nội dung phản hồi", diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/zh-Hans/common.json index 9676b8efb24..3c406e8938a 100644 --- a/web/i18n/zh-Hans/common.json +++ b/web/i18n/zh-Hans/common.json @@ -162,6 +162,15 @@ "environment.development": "开发环境", "environment.testing": "测试环境", "error": "错误", + "errorBoundary.componentStack": "组件堆栈:", + "errorBoundary.details": "错误详情(仅开发模式)", + "errorBoundary.errorCount": "此错误已发生 {{count}} 次", + "errorBoundary.fallbackTitle": "哎呀!出了点问题", + "errorBoundary.message": "渲染此组件时发生了意外错误。", + "errorBoundary.reloadPage": "重新加载页面", + "errorBoundary.title": "出了点问题", + "errorBoundary.tryAgain": "重试", + "errorBoundary.tryAgainCompact": "重试", "errorMsg.fieldRequired": "{{field}} 为必填项", "errorMsg.urlError": "url 应该以 http:// 或 https:// 开头", "feedback.content": "反馈内容", diff --git a/web/i18n/zh-Hant/common.json b/web/i18n/zh-Hant/common.json index 9317f68f82a..6cabc3638fa 100644 --- a/web/i18n/zh-Hant/common.json +++ b/web/i18n/zh-Hant/common.json @@ -162,6 +162,15 @@ "environment.development": "開發環境", "environment.testing": "測試環境", "error": "錯誤", + "errorBoundary.componentStack": "元件堆疊:", + "errorBoundary.details": "錯誤詳情(僅開發模式)", + "errorBoundary.errorCount": "此錯誤已發生 {{count}} 次", + "errorBoundary.fallbackTitle": "哎呀!出了點問題", + "errorBoundary.message": "渲染此元件時發生了意外錯誤。", + "errorBoundary.reloadPage": "重新載入頁面", + "errorBoundary.title": "出了點問題", + "errorBoundary.tryAgain": "重試", + "errorBoundary.tryAgainCompact": "重試", "errorMsg.fieldRequired": "{{field}} 為必填項", "errorMsg.urlError": "URL 應以 http:// 或 https:// 開頭", "feedback.content": "反饋內容", From b818cc07662adaec161367cab630c219ac6d99b5 Mon Sep 17 00:00:00 2001 From: Desel72 Date: Tue, 31 Mar 2026 16:06:42 +0300 Subject: [PATCH 3/8] test: migrate apikey controller tests to testcontainers (#34286) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../controllers/console/test_apikey.py | 153 ++++++++++++++++++ .../controllers/console/test_apikey.py | 139 ---------------- 2 files changed, 153 insertions(+), 139 deletions(-) create mode 100644 api/tests/test_containers_integration_tests/controllers/console/test_apikey.py delete mode 100644 api/tests/unit_tests/controllers/console/test_apikey.py diff --git a/api/tests/test_containers_integration_tests/controllers/console/test_apikey.py b/api/tests/test_containers_integration_tests/controllers/console/test_apikey.py new file mode 100644 index 00000000000..7df63aae1a6 --- /dev/null +++ b/api/tests/test_containers_integration_tests/controllers/console/test_apikey.py @@ -0,0 +1,153 @@ +"""Integration tests for console API key endpoints using testcontainers.""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest +from flask.testing import FlaskClient +from sqlalchemy import delete +from sqlalchemy.orm import Session + +from models.enums import ApiTokenType +from models.model import ApiToken, App, AppMode +from tests.test_containers_integration_tests.controllers.console.helpers import ( + authenticate_console_client, + create_console_account_and_tenant, + create_console_app, +) + + +@pytest.fixture +def setup_app( + db_session_with_containers: Session, + test_client_with_containers: FlaskClient, +) -> tuple[FlaskClient, dict[str, str], App]: + """Create an authenticated client with an app for API key tests.""" + account, tenant = create_console_account_and_tenant(db_session_with_containers) + app = create_console_app(db_session_with_containers, tenant.id, account.id, AppMode.CHAT) + headers = authenticate_console_client(test_client_with_containers, account) + return test_client_with_containers, headers, app + + +@pytest.fixture(autouse=True) +def cleanup_api_tokens(db_session_with_containers: Session): + """Remove API tokens created during each test.""" + yield + db_session_with_containers.execute(delete(ApiToken)) + db_session_with_containers.commit() + + +class TestAppApiKeyListResource: + """Tests for GET/POST /apps//api-keys.""" + + def test_get_empty_keys(self, setup_app: tuple[FlaskClient, dict[str, str], App]) -> None: + client, headers, app = setup_app + resp = client.get(f"/console/api/apps/{app.id}/api-keys", headers=headers) + assert resp.status_code == 200 + assert resp.json is not None + assert resp.json["data"] == [] + + def test_create_api_key(self, setup_app: tuple[FlaskClient, dict[str, str], App]) -> None: + client, headers, app = setup_app + resp = client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers) + assert resp.status_code == 201 + data = resp.json + assert data is not None + assert data["token"].startswith("app-") + assert data["id"] is not None + + def test_get_keys_after_create(self, setup_app: tuple[FlaskClient, dict[str, str], App]) -> None: + client, headers, app = setup_app + client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers) + client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers) + + resp = client.get(f"/console/api/apps/{app.id}/api-keys", headers=headers) + assert resp.status_code == 200 + assert resp.json is not None + assert len(resp.json["data"]) == 2 + + def test_create_key_max_limit( + self, + setup_app: tuple[FlaskClient, dict[str, str], App], + db_session_with_containers: Session, + ) -> None: + client, headers, app = setup_app + # Create 10 keys (the max) + for _ in range(10): + client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers) + + # 11th should fail + resp = client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers) + assert resp.status_code == 400 + + def test_get_keys_for_nonexistent_app( + self, + setup_app: tuple[FlaskClient, dict[str, str], App], + ) -> None: + client, headers, _ = setup_app + resp = client.get( + "/console/api/apps/00000000-0000-0000-0000-000000000000/api-keys", + headers=headers, + ) + assert resp.status_code == 404 + + +class TestAppApiKeyResource: + """Tests for DELETE /apps//api-keys/.""" + + def test_delete_key_success(self, setup_app: tuple[FlaskClient, dict[str, str], App]) -> None: + client, headers, app = setup_app + create_resp = client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers) + assert create_resp.json is not None + key_id = create_resp.json["id"] + + resp = client.delete(f"/console/api/apps/{app.id}/api-keys/{key_id}", headers=headers) + assert resp.status_code == 204 + + def test_delete_nonexistent_key(self, setup_app: tuple[FlaskClient, dict[str, str], App]) -> None: + client, headers, app = setup_app + resp = client.delete( + f"/console/api/apps/{app.id}/api-keys/00000000-0000-0000-0000-000000000000", + headers=headers, + ) + assert resp.status_code == 404 + + def test_delete_key_nonexistent_app( + self, + setup_app: tuple[FlaskClient, dict[str, str], App], + ) -> None: + client, headers, _ = setup_app + resp = client.delete( + "/console/api/apps/00000000-0000-0000-0000-000000000000/api-keys/00000000-0000-0000-0000-000000000000", + headers=headers, + ) + assert resp.status_code == 404 + + def test_delete_forbidden_for_non_admin( + self, + flask_app_with_containers, + ) -> None: + """A non-admin member cannot delete API keys via the controller permission check.""" + from werkzeug.exceptions import Forbidden + + from controllers.console.apikey import BaseApiKeyResource + + resource = BaseApiKeyResource() + resource.resource_type = ApiTokenType.APP + resource.resource_model = MagicMock() + resource.resource_id_field = "app_id" + + non_admin = MagicMock() + non_admin.is_admin_or_owner = False + + with ( + flask_app_with_containers.test_request_context("/"), + patch( + "controllers.console.apikey.current_account_with_tenant", + return_value=(non_admin, "tenant-id"), + ), + patch("controllers.console.apikey._get_resource"), + ): + with pytest.raises(Forbidden): + BaseApiKeyResource.delete(resource, "rid", "kid") diff --git a/api/tests/unit_tests/controllers/console/test_apikey.py b/api/tests/unit_tests/controllers/console/test_apikey.py deleted file mode 100644 index 2dff9c4037f..00000000000 --- a/api/tests/unit_tests/controllers/console/test_apikey.py +++ /dev/null @@ -1,139 +0,0 @@ -from unittest.mock import MagicMock, patch - -import pytest -from werkzeug.exceptions import Forbidden - -from controllers.console.apikey import ( - BaseApiKeyListResource, - BaseApiKeyResource, - _get_resource, -) -from models.enums import ApiTokenType - - -@pytest.fixture -def tenant_context_admin(): - with patch("controllers.console.apikey.current_account_with_tenant") as mock: - user = MagicMock() - user.is_admin_or_owner = True - mock.return_value = (user, "tenant-123") - yield mock - - -@pytest.fixture -def tenant_context_non_admin(): - with patch("controllers.console.apikey.current_account_with_tenant") as mock: - user = MagicMock() - user.is_admin_or_owner = False - mock.return_value = (user, "tenant-123") - yield mock - - -@pytest.fixture -def db_mock(): - with patch("controllers.console.apikey.db") as mock_db: - mock_db.session = MagicMock() - yield mock_db - - -@pytest.fixture(autouse=True) -def bypass_permissions(): - with patch( - "controllers.console.apikey.edit_permission_required", - lambda f: f, - ): - yield - - -class DummyApiKeyListResource(BaseApiKeyListResource): - resource_type = ApiTokenType.APP - resource_model = MagicMock() - resource_id_field = "app_id" - token_prefix = "app-" - - -class DummyApiKeyResource(BaseApiKeyResource): - resource_type = ApiTokenType.APP - resource_model = MagicMock() - resource_id_field = "app_id" - - -class TestGetResource: - def test_get_resource_success(self): - fake_resource = MagicMock() - - with ( - patch("controllers.console.apikey.select") as mock_select, - patch("controllers.console.apikey.Session") as mock_session, - patch("controllers.console.apikey.db") as mock_db, - ): - mock_db.engine = MagicMock() - mock_select.return_value.filter_by.return_value = MagicMock() - - session = mock_session.return_value.__enter__.return_value - session.execute.return_value.scalar_one_or_none.return_value = fake_resource - - result = _get_resource("rid", "tid", MagicMock) - assert result == fake_resource - - def test_get_resource_not_found(self): - with ( - patch("controllers.console.apikey.select") as mock_select, - patch("controllers.console.apikey.Session") as mock_session, - patch("controllers.console.apikey.db") as mock_db, - patch("controllers.console.apikey.flask_restx.abort") as abort, - ): - mock_db.engine = MagicMock() - mock_select.return_value.filter_by.return_value = MagicMock() - - session = mock_session.return_value.__enter__.return_value - session.execute.return_value.scalar_one_or_none.return_value = None - - _get_resource("rid", "tid", MagicMock) - - abort.assert_called_once() - - -class TestBaseApiKeyListResource: - def test_get_apikeys_success(self, tenant_context_admin, db_mock): - resource = DummyApiKeyListResource() - - with patch("controllers.console.apikey._get_resource"): - db_mock.session.scalars.return_value.all.return_value = [MagicMock(), MagicMock()] - - result = DummyApiKeyListResource.get.__wrapped__(resource, "resource-id") - assert "items" in result - - -class TestBaseApiKeyResource: - def test_delete_forbidden(self, tenant_context_non_admin, db_mock): - resource = DummyApiKeyResource() - - with patch("controllers.console.apikey._get_resource"): - with pytest.raises(Forbidden): - DummyApiKeyResource.delete(resource, "rid", "kid") - - def test_delete_key_not_found(self, tenant_context_admin, db_mock): - resource = DummyApiKeyResource() - db_mock.session.scalar.return_value = None - - with patch("controllers.console.apikey._get_resource"): - with pytest.raises(Exception) as exc_info: - DummyApiKeyResource.delete(resource, "rid", "kid") - - # flask_restx.abort raises HTTPException with message in data attribute - assert exc_info.value.data["message"] == "API key not found" - - def test_delete_success(self, tenant_context_admin, db_mock): - resource = DummyApiKeyResource() - db_mock.session.scalar.return_value = MagicMock() - - with ( - patch("controllers.console.apikey._get_resource"), - patch("controllers.console.apikey.ApiTokenCache.delete"), - ): - result, status = DummyApiKeyResource.delete(resource, "rid", "kid") - - assert status == 204 - assert result == {"result": "success"} - db_mock.session.commit.assert_called_once() From d9a0665b2c8bfee584c2caf59f0312d1e5ace4f1 Mon Sep 17 00:00:00 2001 From: Desel72 Date: Tue, 31 Mar 2026 16:09:18 +0300 Subject: [PATCH 4/8] refactor: use sessionmaker().begin() in console datasets controllers (#34283) --- .../console/datasets/data_source.py | 6 ++--- .../datasets/rag_pipeline/rag_pipeline.py | 4 +-- .../rag_pipeline/rag_pipeline_datasets.py | 4 +-- .../rag_pipeline_draft_variable.py | 8 +++--- .../rag_pipeline/rag_pipeline_import.py | 12 ++++----- .../rag_pipeline/rag_pipeline_workflow.py | 16 ++++-------- .../console/datasets/test_data_source.py | 26 +++++++++---------- 7 files changed, 34 insertions(+), 42 deletions(-) diff --git a/api/controllers/console/datasets/data_source.py b/api/controllers/console/datasets/data_source.py index daef4e005ae..ac143490458 100644 --- a/api/controllers/console/datasets/data_source.py +++ b/api/controllers/console/datasets/data_source.py @@ -6,7 +6,7 @@ from flask import request from flask_restx import Resource, fields, marshal_with from pydantic import BaseModel, Field from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import NotFound from controllers.common.schema import get_or_create_model, register_schema_model @@ -159,7 +159,7 @@ class DataSourceApi(Resource): @account_initialization_required def patch(self, binding_id, action: Literal["enable", "disable"]): binding_id = str(binding_id) - with Session(db.engine) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: data_source_binding = session.execute( select(DataSourceOauthBinding).filter_by(id=binding_id) ).scalar_one_or_none() @@ -211,7 +211,7 @@ class DataSourceNotionListApi(Resource): if not credential: raise NotFound("Credential not found.") exist_page_ids = [] - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: # import notion in the exist dataset if query.dataset_id: dataset = DatasetService.get_dataset(query.dataset_id) diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py index 4f31093cfe7..1758bad31da 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py @@ -3,7 +3,7 @@ import logging from flask import request from flask_restx import Resource from pydantic import BaseModel, Field -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from controllers.common.schema import register_schema_models from controllers.console import console_ns @@ -85,7 +85,7 @@ class CustomizedPipelineTemplateApi(Resource): @account_initialization_required @enterprise_license_required def post(self, template_id: str): - with Session(db.engine) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: template = ( session.query(PipelineCustomizedTemplate).where(PipelineCustomizedTemplate.id == template_id).first() ) diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_datasets.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_datasets.py index e65cb19b397..a6ca0689d0e 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_datasets.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_datasets.py @@ -1,6 +1,6 @@ from flask_restx import Resource, marshal from pydantic import BaseModel -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden import services @@ -54,7 +54,7 @@ class CreateRagPipelineDatasetApi(Resource): yaml_content=payload.yaml_content, ) try: - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: rag_pipeline_dsl_service = RagPipelineDslService(session) import_info = rag_pipeline_dsl_service.create_rag_pipeline_dataset( tenant_id=current_tenant_id, diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py index f12cbd34959..d635dcb5301 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py @@ -5,7 +5,7 @@ from flask import Response, request from flask_restx import Resource, marshal, marshal_with from graphon.variables.types import SegmentType from pydantic import BaseModel, Field -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden from controllers.common.schema import register_schema_models @@ -96,7 +96,7 @@ class RagPipelineVariableCollectionApi(Resource): raise DraftWorkflowNotExist() # fetch draft workflow by app_model - with Session(bind=db.engine, expire_on_commit=False) as session: + with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session: draft_var_srv = WorkflowDraftVariableService( session=session, ) @@ -143,7 +143,7 @@ class RagPipelineNodeVariableCollectionApi(Resource): @marshal_with(workflow_draft_variable_list_model) def get(self, pipeline: Pipeline, node_id: str): validate_node_id(node_id) - with Session(bind=db.engine, expire_on_commit=False) as session: + with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session: draft_var_srv = WorkflowDraftVariableService( session=session, ) @@ -289,7 +289,7 @@ class RagPipelineVariableResetApi(Resource): def _get_variable_list(pipeline: Pipeline, node_id) -> WorkflowDraftVariableList: - with Session(bind=db.engine, expire_on_commit=False) as session: + with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session: draft_var_srv = WorkflowDraftVariableService( session=session, ) diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_import.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_import.py index af142b46464..732a6dc4468 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_import.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_import.py @@ -1,7 +1,7 @@ from flask import request from flask_restx import Resource, fields, marshal_with # type: ignore from pydantic import BaseModel, Field -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from controllers.common.schema import get_or_create_model, register_schema_models from controllers.console import console_ns @@ -68,7 +68,7 @@ class RagPipelineImportApi(Resource): payload = RagPipelineImportPayload.model_validate(console_ns.payload or {}) # Create service with session - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: import_service = RagPipelineDslService(session) # Import app account = current_user @@ -80,7 +80,6 @@ class RagPipelineImportApi(Resource): pipeline_id=payload.pipeline_id, dataset_name=payload.name, ) - session.commit() # Return appropriate status code based on result status = result.status @@ -102,12 +101,11 @@ class RagPipelineImportConfirmApi(Resource): current_user, _ = current_account_with_tenant() # Create service with session - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: import_service = RagPipelineDslService(session) # Confirm import account = current_user result = import_service.confirm_import(import_id=import_id, account=account) - session.commit() # Return appropriate status code based on result if result.status == ImportStatus.FAILED: @@ -124,7 +122,7 @@ class RagPipelineImportCheckDependenciesApi(Resource): @edit_permission_required @marshal_with(pipeline_import_check_dependencies_model) def get(self, pipeline: Pipeline): - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: import_service = RagPipelineDslService(session) result = import_service.check_dependencies(pipeline=pipeline) @@ -142,7 +140,7 @@ class RagPipelineExportApi(Resource): # Add include_secret params query = IncludeSecretQuery.model_validate(request.args.to_dict()) - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: export_service = RagPipelineDslService(session) result = export_service.export_rag_pipeline_dsl( pipeline=pipeline, include_secret=query.include_secret == "true" diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py index 8efb59a8e9c..e08cb155b6a 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py @@ -6,7 +6,7 @@ from flask import abort, request from flask_restx import Resource, marshal_with # type: ignore from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound import services @@ -608,7 +608,7 @@ class PublishedRagPipelineApi(Resource): # The role of the current user in the ta table must be admin, owner, or editor current_user, _ = current_account_with_tenant() rag_pipeline_service = RagPipelineService() - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: pipeline = session.merge(pipeline) workflow = rag_pipeline_service.publish_workflow( session=session, @@ -620,8 +620,6 @@ class PublishedRagPipelineApi(Resource): session.add(pipeline) workflow_created_at = TimestampField().format(workflow.created_at) - session.commit() - return { "result": "success", "created_at": workflow_created_at, @@ -695,7 +693,7 @@ class PublishedAllRagPipelineApi(Resource): raise Forbidden() rag_pipeline_service = RagPipelineService() - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: workflows, has_more = rag_pipeline_service.get_all_published_workflow( session=session, pipeline=pipeline, @@ -767,7 +765,7 @@ class RagPipelineByIdApi(Resource): rag_pipeline_service = RagPipelineService() # Create a session and manage the transaction - with Session(db.engine, expire_on_commit=False) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: workflow = rag_pipeline_service.update_workflow( session=session, workflow_id=workflow_id, @@ -779,9 +777,6 @@ class RagPipelineByIdApi(Resource): if not workflow: raise NotFound("Workflow not found") - # Commit the transaction in the controller - session.commit() - return workflow @setup_required @@ -798,14 +793,13 @@ class RagPipelineByIdApi(Resource): workflow_service = WorkflowService() - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: try: workflow_service.delete_workflow( session=session, workflow_id=workflow_id, tenant_id=pipeline.tenant_id, ) - session.commit() except WorkflowInUseError as e: abort(400, description=str(e)) except DraftWorkflowDeletionError as e: diff --git a/api/tests/test_containers_integration_tests/controllers/console/datasets/test_data_source.py b/api/tests/test_containers_integration_tests/controllers/console/datasets/test_data_source.py index 1c07d4ca1c2..1c4c6a899f6 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/datasets/test_data_source.py +++ b/api/tests/test_containers_integration_tests/controllers/console/datasets/test_data_source.py @@ -102,12 +102,12 @@ class TestDataSourceApi: with ( app.test_request_context("/"), - patch("controllers.console.datasets.data_source.Session") as mock_session_class, + patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class, patch("controllers.console.datasets.data_source.db.session.add"), patch("controllers.console.datasets.data_source.db.session.commit"), ): mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session mock_session.execute.return_value.scalar_one_or_none.return_value = binding response, status = method(api, "b1", "enable") @@ -123,12 +123,12 @@ class TestDataSourceApi: with ( app.test_request_context("/"), - patch("controllers.console.datasets.data_source.Session") as mock_session_class, + patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class, patch("controllers.console.datasets.data_source.db.session.add"), patch("controllers.console.datasets.data_source.db.session.commit"), ): mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session mock_session.execute.return_value.scalar_one_or_none.return_value = binding response, status = method(api, "b1", "disable") @@ -142,10 +142,10 @@ class TestDataSourceApi: with ( app.test_request_context("/"), - patch("controllers.console.datasets.data_source.Session") as mock_session_class, + patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class, ): mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session mock_session.execute.return_value.scalar_one_or_none.return_value = None with pytest.raises(NotFound): @@ -159,10 +159,10 @@ class TestDataSourceApi: with ( app.test_request_context("/"), - patch("controllers.console.datasets.data_source.Session") as mock_session_class, + patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class, ): mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session mock_session.execute.return_value.scalar_one_or_none.return_value = binding with pytest.raises(ValueError): @@ -176,10 +176,10 @@ class TestDataSourceApi: with ( app.test_request_context("/"), - patch("controllers.console.datasets.data_source.Session") as mock_session_class, + patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class, ): mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session mock_session.execute.return_value.scalar_one_or_none.return_value = binding with pytest.raises(ValueError): @@ -282,7 +282,7 @@ class TestDataSourceNotionListApi: "controllers.console.datasets.data_source.DatasetService.get_dataset", return_value=dataset, ), - patch("controllers.console.datasets.data_source.Session") as mock_session_class, + patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class, patch( "core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime", return_value=MagicMock( @@ -292,7 +292,7 @@ class TestDataSourceNotionListApi: ), ): mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session mock_session.scalars.return_value.all.return_value = [document] response, status = method(api) @@ -315,7 +315,7 @@ class TestDataSourceNotionListApi: "controllers.console.datasets.data_source.DatasetService.get_dataset", return_value=dataset, ), - patch("controllers.console.datasets.data_source.Session"), + patch("controllers.console.datasets.data_source.sessionmaker"), ): with pytest.raises(ValueError): method(api) From cf50d7c7b52449bd3c8ee40b8b0f80b3663b485e Mon Sep 17 00:00:00 2001 From: Desel72 Date: Tue, 31 Mar 2026 16:10:16 +0300 Subject: [PATCH 5/8] refactor: use sessionmaker().begin() in console app controllers (#34282) Co-authored-by: Asuka Minato --- api/controllers/console/app/app.py | 5 ++- api/controllers/console/app/app_import.py | 10 +++--- .../console/app/conversation_variables.py | 4 +-- api/controllers/console/app/workflow.py | 17 +++------- .../console/app/workflow_app_log.py | 6 ++-- .../console/app/workflow_draft_variable.py | 8 ++--- .../console/app/workflow_trigger.py | 11 +++---- .../controllers/console/app/test_app_apis.py | 33 +++++++++++++++---- 8 files changed, 51 insertions(+), 43 deletions(-) diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 738e77b3715..ec56cd3baaa 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -9,7 +9,7 @@ from graphon.enums import WorkflowExecutionStatus from graphon.file import helpers as file_helpers from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field, field_validator from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest from controllers.common.helpers import FileInfo @@ -642,7 +642,7 @@ class AppCopyApi(Resource): args = CopyAppPayload.model_validate(console_ns.payload or {}) - with Session(db.engine) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: import_service = AppDslService(session) yaml_content = import_service.export_dsl(app_model=app_model, include_secret=True) result = import_service.import_app( @@ -655,7 +655,6 @@ class AppCopyApi(Resource): icon=args.icon, icon_background=args.icon_background, ) - session.commit() # Inherit web app permission from original app if result.app_id and FeatureService.get_system_features().webapp_auth.enabled: diff --git a/api/controllers/console/app/app_import.py b/api/controllers/console/app/app_import.py index fdef54ba5a4..16e1fa32455 100644 --- a/api/controllers/console/app/app_import.py +++ b/api/controllers/console/app/app_import.py @@ -1,6 +1,6 @@ from flask_restx import Resource, fields, marshal_with from pydantic import BaseModel, Field -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( @@ -71,7 +71,7 @@ class AppImportApi(Resource): args = AppImportPayload.model_validate(console_ns.payload) # Create service with session - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: import_service = AppDslService(session) # Import app account = current_user @@ -87,7 +87,6 @@ class AppImportApi(Resource): icon_background=args.icon_background, app_id=args.app_id, ) - session.commit() if result.app_id and FeatureService.get_system_features().webapp_auth.enabled: # update web app setting as private EnterpriseService.WebAppAuth.update_app_access_mode(result.app_id, "private") @@ -112,12 +111,11 @@ class AppImportConfirmApi(Resource): current_user, _ = current_account_with_tenant() # Create service with session - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: import_service = AppDslService(session) # Confirm import account = current_user result = import_service.confirm_import(import_id=import_id, account=account) - session.commit() # Return appropriate status code based on result if result.status == ImportStatus.FAILED: @@ -134,7 +132,7 @@ class AppImportCheckDependenciesApi(Resource): @marshal_with(app_import_check_dependencies_model) @edit_permission_required def get(self, app_model: App): - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: import_service = AppDslService(session) result = import_service.check_dependencies(app_model=app_model) diff --git a/api/controllers/console/app/conversation_variables.py b/api/controllers/console/app/conversation_variables.py index 368a6112ba7..369c26a80cb 100644 --- a/api/controllers/console/app/conversation_variables.py +++ b/api/controllers/console/app/conversation_variables.py @@ -2,7 +2,7 @@ from flask import request from flask_restx import Resource, fields, marshal_with from pydantic import BaseModel, Field from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from controllers.console import console_ns from controllers.console.app.wraps import get_app_model @@ -69,7 +69,7 @@ class ConversationVariablesApi(Resource): page_size = 100 stmt = stmt.limit(page_size).offset((page - 1) * page_size) - with Session(db.engine) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: rows = session.scalars(stmt).all() return { diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index 1f5a84c0b2b..6df8f7032ec 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -10,7 +10,7 @@ from graphon.file import File from graphon.graph_engine.manager import GraphEngineManager from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, field_validator -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound import services @@ -840,7 +840,7 @@ class PublishedWorkflowApi(Resource): args = PublishWorkflowPayload.model_validate(console_ns.payload or {}) workflow_service = WorkflowService() - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: workflow = workflow_service.publish_workflow( session=session, app_model=app_model, @@ -858,8 +858,6 @@ class PublishedWorkflowApi(Resource): workflow_created_at = TimestampField().format(workflow.created_at) - session.commit() - return { "result": "success", "created_at": workflow_created_at, @@ -982,7 +980,7 @@ class PublishedAllWorkflowApi(Resource): raise Forbidden() workflow_service = WorkflowService() - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: workflows, has_more = workflow_service.get_all_published_workflow( session=session, app_model=app_model, @@ -1072,7 +1070,7 @@ class WorkflowByIdApi(Resource): workflow_service = WorkflowService() # Create a session and manage the transaction - with Session(db.engine, expire_on_commit=False) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: workflow = workflow_service.update_workflow( session=session, workflow_id=workflow_id, @@ -1084,9 +1082,6 @@ class WorkflowByIdApi(Resource): if not workflow: raise NotFound("Workflow not found") - # Commit the transaction in the controller - session.commit() - return workflow @setup_required @@ -1101,13 +1096,11 @@ class WorkflowByIdApi(Resource): workflow_service = WorkflowService() # Create a session and manage the transaction - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: try: workflow_service.delete_workflow( session=session, workflow_id=workflow_id, tenant_id=app_model.tenant_id ) - # Commit the transaction in the controller - session.commit() except WorkflowInUseError as e: abort(400, description=str(e)) except DraftWorkflowDeletionError as e: diff --git a/api/controllers/console/app/workflow_app_log.py b/api/controllers/console/app/workflow_app_log.py index f0e26c86a5a..3b24c2a4021 100644 --- a/api/controllers/console/app/workflow_app_log.py +++ b/api/controllers/console/app/workflow_app_log.py @@ -5,7 +5,7 @@ from flask import request from flask_restx import Resource, marshal_with from graphon.enums import WorkflowExecutionStatus from pydantic import BaseModel, Field, field_validator -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from controllers.console import console_ns from controllers.console.app.wraps import get_app_model @@ -87,7 +87,7 @@ class WorkflowAppLogApi(Resource): # get paginate workflow app logs workflow_app_service = WorkflowAppService() - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_app_logs( session=session, app_model=app_model, @@ -124,7 +124,7 @@ class WorkflowArchivedLogApi(Resource): args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore workflow_app_service = WorkflowAppService() - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_archive_logs( session=session, app_model=app_model, diff --git a/api/controllers/console/app/workflow_draft_variable.py b/api/controllers/console/app/workflow_draft_variable.py index 4052897e9a4..35e2df847c3 100644 --- a/api/controllers/console/app/workflow_draft_variable.py +++ b/api/controllers/console/app/workflow_draft_variable.py @@ -10,7 +10,7 @@ from graphon.variables.segment_group import SegmentGroup from graphon.variables.segments import ArrayFileSegment, FileSegment, Segment from graphon.variables.types import SegmentType from pydantic import BaseModel, Field -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from controllers.console import console_ns from controllers.console.app.error import ( @@ -244,7 +244,7 @@ class WorkflowVariableCollectionApi(Resource): raise DraftWorkflowNotExist() # fetch draft workflow by app_model - with Session(bind=db.engine, expire_on_commit=False) as session: + with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session: draft_var_srv = WorkflowDraftVariableService( session=session, ) @@ -298,7 +298,7 @@ class NodeVariableCollectionApi(Resource): @marshal_with(workflow_draft_variable_list_model) def get(self, app_model: App, node_id: str): validate_node_id(node_id) - with Session(bind=db.engine, expire_on_commit=False) as session: + with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session: draft_var_srv = WorkflowDraftVariableService( session=session, ) @@ -465,7 +465,7 @@ class VariableResetApi(Resource): def _get_variable_list(app_model: App, node_id) -> WorkflowDraftVariableList: - with Session(bind=db.engine, expire_on_commit=False) as session: + with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session: draft_var_srv = WorkflowDraftVariableService( session=session, ) diff --git a/api/controllers/console/app/workflow_trigger.py b/api/controllers/console/app/workflow_trigger.py index 8236e766ae3..aa37d247383 100644 --- a/api/controllers/console/app/workflow_trigger.py +++ b/api/controllers/console/app/workflow_trigger.py @@ -4,7 +4,7 @@ from flask import request from flask_restx import Resource, fields, marshal_with from pydantic import BaseModel from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import NotFound from configs import dify_config @@ -64,7 +64,7 @@ class WebhookTriggerApi(Resource): node_id = args.node_id - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: # Get webhook trigger for this app and node webhook_trigger = ( session.query(WorkflowWebhookTrigger) @@ -95,7 +95,7 @@ class AppTriggersApi(Resource): assert isinstance(current_user, Account) assert current_user.current_tenant_id is not None - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: # Get all triggers for this app using select API triggers = ( session.execute( @@ -137,7 +137,7 @@ class AppTriggerEnableApi(Resource): assert current_user.current_tenant_id is not None trigger_id = args.trigger_id - with Session(db.engine) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: # Find the trigger using select trigger = session.execute( select(AppTrigger).where( @@ -153,9 +153,6 @@ class AppTriggerEnableApi(Resource): # Update status based on enable_trigger boolean trigger.status = AppTriggerStatus.ENABLED if args.enable_trigger else AppTriggerStatus.DISABLED - session.commit() - session.refresh(trigger) - # Add computed icon field url_prefix = dify_config.CONSOLE_API_URL + "/console/api/workspaces/current/tool-provider/builtin/" if trigger.trigger_type == "trigger-plugin": diff --git a/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py b/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py index fbaec069bb9..0841217fcfe 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py +++ b/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py @@ -383,14 +383,21 @@ class TestWorkflowAppLogEndpoints: monkeypatch.setattr(workflow_app_log_module, "db", SimpleNamespace(engine=MagicMock())) - class DummySession: + class DummySessionCtx: def __enter__(self): return "session" def __exit__(self, exc_type, exc, tb): return False - monkeypatch.setattr(workflow_app_log_module, "Session", lambda *args, **kwargs: DummySession()) + class DummySessionMaker: + def __init__(self, *args, **kwargs): + pass + + def begin(self): + return DummySessionCtx() + + monkeypatch.setattr(workflow_app_log_module, "sessionmaker", DummySessionMaker) def fake_get_paginate(self, **_kwargs): return {"items": [], "total": 0} @@ -423,13 +430,20 @@ class TestWorkflowDraftVariableEndpoints: monkeypatch.setattr(workflow_draft_variable_module, "db", SimpleNamespace(engine=MagicMock())) monkeypatch.setattr(workflow_draft_variable_module, "current_user", SimpleNamespace(id="user-1")) - class DummySession: + class DummySessionCtx: def __enter__(self): return "session" def __exit__(self, exc_type, exc, tb): return False + class DummySessionMaker: + def __init__(self, *args, **kwargs): + pass + + def begin(self): + return DummySessionCtx() + class DummyDraftService: def __init__(self, session): self.session = session @@ -437,7 +451,7 @@ class TestWorkflowDraftVariableEndpoints: def list_variables_without_values(self, **_kwargs): return {"items": [], "total": 0} - monkeypatch.setattr(workflow_draft_variable_module, "Session", lambda *args, **kwargs: DummySession()) + monkeypatch.setattr(workflow_draft_variable_module, "sessionmaker", DummySessionMaker) class DummyWorkflowService: def is_workflow_exist(self, *args, **kwargs): @@ -543,14 +557,21 @@ class TestWorkflowTriggerEndpoints: session = MagicMock() session.query.return_value.where.return_value.first.return_value = trigger - class DummySession: + class DummySessionCtx: def __enter__(self): return session def __exit__(self, exc_type, exc, tb): return False - monkeypatch.setattr(workflow_trigger_module, "Session", lambda *_args, **_kwargs: DummySession()) + class DummySessionMaker: + def __init__(self, *args, **kwargs): + pass + + def begin(self): + return DummySessionCtx() + + monkeypatch.setattr(workflow_trigger_module, "sessionmaker", DummySessionMaker) with app.test_request_context("/?node_id=node-1"): result = method(app_model=SimpleNamespace(id="app-1")) From 2c8b47ce443d81222c6abf12c308e87d57a0c9ff Mon Sep 17 00:00:00 2001 From: Desel72 Date: Tue, 31 Mar 2026 17:26:37 +0300 Subject: [PATCH 6/8] refactor: use sessionmaker().begin() in web and mcp controllers (#34281) --- api/controllers/mcp/mcp.py | 10 ++++------ api/controllers/web/conversation.py | 4 ++-- api/controllers/web/forgot_password.py | 11 +++++------ api/controllers/web/wraps.py | 4 ++-- .../controllers/web/test_web_forgot_password.py | 13 ++++++------- 5 files changed, 19 insertions(+), 23 deletions(-) diff --git a/api/controllers/mcp/mcp.py b/api/controllers/mcp/mcp.py index 58ec76243b9..3c59535a48f 100644 --- a/api/controllers/mcp/mcp.py +++ b/api/controllers/mcp/mcp.py @@ -4,7 +4,7 @@ from flask import Response from flask_restx import Resource from graphon.variables.input_entities import VariableEntity from pydantic import BaseModel, Field, ValidationError -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, sessionmaker from controllers.common.schema import register_schema_model from controllers.mcp import mcp_ns @@ -67,7 +67,7 @@ class MCPAppApi(Resource): request_id: Union[int, str] | None = args.id mcp_request = self._parse_mcp_request(args.model_dump(exclude_none=True)) - with Session(db.engine, expire_on_commit=False) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: # Get MCP server and app mcp_server, app = self._get_mcp_server_and_app(server_code, session) self._validate_server_status(mcp_server) @@ -189,7 +189,7 @@ class MCPAppApi(Resource): def _retrieve_end_user(self, tenant_id: str, mcp_server_id: str) -> EndUser | None: """Get end user - manages its own database session""" - with Session(db.engine, expire_on_commit=False) as session, session.begin(): + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: return ( session.query(EndUser) .where(EndUser.tenant_id == tenant_id) @@ -229,9 +229,7 @@ class MCPAppApi(Resource): if not end_user and isinstance(mcp_request.root, mcp_types.InitializeRequest): client_info = mcp_request.root.params.clientInfo client_name = f"{client_info.name}@{client_info.version}" - # Commit the session before creating end user to avoid transaction conflicts - session.commit() - with Session(db.engine, expire_on_commit=False) as create_session, create_session.begin(): + with sessionmaker(db.engine, expire_on_commit=False).begin() as create_session: end_user = self._create_end_user(client_name, app.tenant_id, app.id, mcp_server.id, create_session) return handle_mcp_request(app, mcp_request, user_input_form, mcp_server, end_user, request_id) diff --git a/api/controllers/web/conversation.py b/api/controllers/web/conversation.py index e76649495a0..d5baa5fb7d1 100644 --- a/api/controllers/web/conversation.py +++ b/api/controllers/web/conversation.py @@ -2,7 +2,7 @@ from typing import Literal from flask import request from pydantic import BaseModel, Field, TypeAdapter, field_validator, model_validator -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import NotFound from controllers.common.schema import register_schema_models @@ -99,7 +99,7 @@ class ConversationListApi(WebApiResource): query = ConversationListQuery.model_validate(raw_args) try: - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: pagination = WebConversationService.pagination_by_last_id( session=session, app_model=app_model, diff --git a/api/controllers/web/forgot_password.py b/api/controllers/web/forgot_password.py index 91d206f7270..d69571cc9cf 100644 --- a/api/controllers/web/forgot_password.py +++ b/api/controllers/web/forgot_password.py @@ -4,7 +4,7 @@ import secrets from flask import request from flask_restx import Resource from pydantic import BaseModel, Field, field_validator -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from controllers.common.schema import register_schema_models from controllers.console.auth.error import ( @@ -81,7 +81,7 @@ class ForgotPasswordSendEmailApi(Resource): else: language = "en-US" - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: account = AccountService.get_account_by_email_with_case_fallback(request_email, session=session) token = None if account is None: @@ -180,18 +180,17 @@ class ForgotPasswordResetApi(Resource): email = reset_data.get("email", "") - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: account = AccountService.get_account_by_email_with_case_fallback(email, session=session) if account: - self._update_existing_account(account, password_hashed, salt, session) + self._update_existing_account(account, password_hashed, salt) else: raise AuthenticationFailedError() return {"result": "success"} - def _update_existing_account(self, account: Account, password_hashed, salt, session): + def _update_existing_account(self, account: Account, password_hashed, salt): # Update existing account credentials account.password = base64.b64encode(password_hashed).decode() account.password_salt = base64.b64encode(salt).decode() - session.commit() diff --git a/api/controllers/web/wraps.py b/api/controllers/web/wraps.py index 152137f39c8..654951a1aa5 100644 --- a/api/controllers/web/wraps.py +++ b/api/controllers/web/wraps.py @@ -6,7 +6,7 @@ from typing import Concatenate, ParamSpec, TypeVar from flask import request from flask_restx import Resource from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, NotFound, Unauthorized from constants import HEADER_NAME_APP_CODE @@ -49,7 +49,7 @@ def decode_jwt_token(app_code: str | None = None, user_id: str | None = None): decoded = PassportService().verify(tk) app_code = decoded.get("app_code") app_id = decoded.get("app_id") - with Session(db.engine, expire_on_commit=False) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: app_model = session.scalar(select(App).where(App.id == app_id)) site = session.scalar(select(Site).where(Site.code == app_code)) if not app_model: diff --git a/api/tests/test_containers_integration_tests/controllers/web/test_web_forgot_password.py b/api/tests/test_containers_integration_tests/controllers/web/test_web_forgot_password.py index 19057726c34..04ad143103b 100644 --- a/api/tests/test_containers_integration_tests/controllers/web/test_web_forgot_password.py +++ b/api/tests/test_containers_integration_tests/controllers/web/test_web_forgot_password.py @@ -37,7 +37,7 @@ class TestForgotPasswordSendEmailApi: @patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback") @patch("controllers.web.forgot_password.AccountService.is_email_send_ip_limit", return_value=False) @patch("controllers.web.forgot_password.extract_remote_ip", return_value="127.0.0.1") - @patch("controllers.web.forgot_password.Session") + @patch("controllers.web.forgot_password.sessionmaker") def test_should_normalize_email_before_sending( self, mock_session_cls, @@ -51,7 +51,7 @@ class TestForgotPasswordSendEmailApi: mock_get_account.return_value = mock_account mock_send_mail.return_value = "token-123" mock_session = MagicMock() - mock_session_cls.return_value.__enter__.return_value = mock_session + mock_session_cls.return_value.begin.return_value.__enter__.return_value = mock_session with patch("controllers.web.forgot_password.db", SimpleNamespace(engine="engine")): with app.test_request_context( @@ -153,7 +153,7 @@ class TestForgotPasswordResetApi: @patch("controllers.web.forgot_password.ForgotPasswordResetApi._update_existing_account") @patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback") - @patch("controllers.web.forgot_password.Session") + @patch("controllers.web.forgot_password.sessionmaker") @patch("controllers.web.forgot_password.AccountService.revoke_reset_password_token") @patch("controllers.web.forgot_password.AccountService.get_reset_password_data") def test_should_fetch_account_with_fallback( @@ -169,7 +169,7 @@ class TestForgotPasswordResetApi: mock_account = MagicMock() mock_get_account.return_value = mock_account mock_session = MagicMock() - mock_session_cls.return_value.__enter__.return_value = mock_session + mock_session_cls.return_value.begin.return_value.__enter__.return_value = mock_session with patch("controllers.web.forgot_password.db", SimpleNamespace(engine="engine")): with app.test_request_context( @@ -190,7 +190,7 @@ class TestForgotPasswordResetApi: @patch("controllers.web.forgot_password.hash_password", return_value=b"hashed-value") @patch("controllers.web.forgot_password.secrets.token_bytes", return_value=b"0123456789abcdef") - @patch("controllers.web.forgot_password.Session") + @patch("controllers.web.forgot_password.sessionmaker") @patch("controllers.web.forgot_password.AccountService.revoke_reset_password_token") @patch("controllers.web.forgot_password.AccountService.get_reset_password_data") @patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback") @@ -208,7 +208,7 @@ class TestForgotPasswordResetApi: account = MagicMock() mock_get_account.return_value = account mock_session = MagicMock() - mock_session_cls.return_value.__enter__.return_value = mock_session + mock_session_cls.return_value.begin.return_value.__enter__.return_value = mock_session with patch("controllers.web.forgot_password.db", SimpleNamespace(engine="engine")): with app.test_request_context( @@ -231,4 +231,3 @@ class TestForgotPasswordResetApi: assert account.password == expected_password expected_salt = base64.b64encode(b"0123456789abcdef").decode() assert account.password_salt == expected_salt - mock_session.commit.assert_called_once() From dbdbb098d5cc04e7e89880f1e2ad43bad3e7cd5f Mon Sep 17 00:00:00 2001 From: Desel72 Date: Tue, 31 Mar 2026 17:28:05 +0300 Subject: [PATCH 7/8] =?UTF-8?q?refactor:=20use=20sessionmaker().begin()=20?= =?UTF-8?q?in=20console=20workspace=20and=20misc=20co=E2=80=A6=20(#34284)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/controllers/console/apikey.py | 4 +-- .../console/explore/conversation.py | 4 +-- api/controllers/console/workspace/__init__.py | 4 +-- api/controllers/console/workspace/account.py | 4 +-- .../console/workspace/tool_providers.py | 26 +++++++++---------- .../console/workspace/trigger_providers.py | 5 ++-- .../console/workspace/test_tool_provider.py | 4 +-- .../workspace/test_trigger_providers.py | 8 +++--- 8 files changed, 29 insertions(+), 30 deletions(-) diff --git a/api/controllers/console/apikey.py b/api/controllers/console/apikey.py index 783cb5c444b..772bb9d0f19 100644 --- a/api/controllers/console/apikey.py +++ b/api/controllers/console/apikey.py @@ -2,7 +2,7 @@ import flask_restx from flask_restx import Resource, fields, marshal_with from flask_restx._http import HTTPStatus from sqlalchemy import delete, func, select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden from extensions.ext_database import db @@ -34,7 +34,7 @@ api_key_list_model = console_ns.model( def _get_resource(resource_id, tenant_id, resource_model): - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: resource = session.execute( select(resource_model).filter_by(id=resource_id, tenant_id=tenant_id) ).scalar_one_or_none() diff --git a/api/controllers/console/explore/conversation.py b/api/controllers/console/explore/conversation.py index 933c80f509f..092f509f1c3 100644 --- a/api/controllers/console/explore/conversation.py +++ b/api/controllers/console/explore/conversation.py @@ -2,7 +2,7 @@ from typing import Any from flask import request from pydantic import BaseModel, Field, TypeAdapter, model_validator -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import NotFound from controllers.common.schema import register_schema_models @@ -74,7 +74,7 @@ class ConversationListApi(InstalledAppResource): try: if not isinstance(current_user, Account): raise ValueError("current_user must be an Account instance") - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: pagination = WebConversationService.pagination_by_last_id( session=session, app_model=app_model, diff --git a/api/controllers/console/workspace/__init__.py b/api/controllers/console/workspace/__init__.py index 876e2301f2e..9484cc773e4 100644 --- a/api/controllers/console/workspace/__init__.py +++ b/api/controllers/console/workspace/__init__.py @@ -2,7 +2,7 @@ from collections.abc import Callable from functools import wraps from typing import ParamSpec, TypeVar -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden from extensions.ext_database import db @@ -24,7 +24,7 @@ def plugin_permission_required( user = current_user tenant_id = current_tenant_id - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: permission = ( session.query(TenantPluginPermission) .where( diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index 6f93ff1e70e..dcd4438b67a 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -8,7 +8,7 @@ from flask import request from flask_restx import Resource, fields, marshal_with from pydantic import BaseModel, Field, field_validator, model_validator from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from configs import dify_config from constants.languages import supported_language @@ -562,7 +562,7 @@ class ChangeEmailSendEmailApi(Resource): user_email = current_user.email else: - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: account = AccountService.get_account_by_email_with_case_fallback(args.email, session=session) if account is None: raise AccountNotFound() diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py index 80216915cd1..c9956501e28 100644 --- a/api/controllers/console/workspace/tool_providers.py +++ b/api/controllers/console/workspace/tool_providers.py @@ -7,7 +7,7 @@ from flask import make_response, redirect, request, send_file from flask_restx import Resource from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, HttpUrl, field_validator, model_validator -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden from configs import dify_config @@ -1019,7 +1019,7 @@ class ToolProviderMCPApi(Resource): # Step 1: Get provider data for URL validation (short-lived session, no network I/O) validation_data = None - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) validation_data = service.get_provider_for_url_validation( tenant_id=current_tenant_id, provider_id=payload.provider_id @@ -1034,7 +1034,7 @@ class ToolProviderMCPApi(Resource): ) # Step 3: Perform database update in a transaction - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) service.update_provider( tenant_id=current_tenant_id, @@ -1061,7 +1061,7 @@ class ToolProviderMCPApi(Resource): payload = MCPProviderDeletePayload.model_validate(console_ns.payload or {}) _, current_tenant_id = current_account_with_tenant() - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) service.delete_provider(tenant_id=current_tenant_id, provider_id=payload.provider_id) @@ -1079,7 +1079,7 @@ class ToolMCPAuthApi(Resource): provider_id = payload.provider_id _, tenant_id = current_account_with_tenant() - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) db_provider = service.get_provider(provider_id=provider_id, tenant_id=tenant_id) if not db_provider: @@ -1100,7 +1100,7 @@ class ToolMCPAuthApi(Resource): sse_read_timeout=provider_entity.sse_read_timeout, ): # Update credentials in new transaction - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) service.update_provider_credentials( provider_id=provider_id, @@ -1118,17 +1118,17 @@ class ToolMCPAuthApi(Resource): resource_metadata_url=e.resource_metadata_url, scope_hint=e.scope_hint, ) - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) response = service.execute_auth_actions(auth_result) return response except MCPRefreshTokenError as e: - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) service.clear_provider_credentials(provider_id=provider_id, tenant_id=tenant_id) raise ValueError(f"Failed to refresh token, please try to authorize again: {e}") from e except (MCPError, ValueError) as e: - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) service.clear_provider_credentials(provider_id=provider_id, tenant_id=tenant_id) raise ValueError(f"Failed to connect to MCP server: {e}") from e @@ -1141,7 +1141,7 @@ class ToolMCPDetailApi(Resource): @account_initialization_required def get(self, provider_id): _, tenant_id = current_account_with_tenant() - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) provider = service.get_provider(provider_id=provider_id, tenant_id=tenant_id) return jsonable_encoder(ToolTransformService.mcp_provider_to_user_provider(provider, for_list=True)) @@ -1155,7 +1155,7 @@ class ToolMCPListAllApi(Resource): def get(self): _, tenant_id = current_account_with_tenant() - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) # Skip sensitive data decryption for list view to improve performance tools = service.list_providers(tenant_id=tenant_id, include_sensitive=False) @@ -1170,7 +1170,7 @@ class ToolMCPUpdateApi(Resource): @account_initialization_required def get(self, provider_id): _, tenant_id = current_account_with_tenant() - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) tools = service.list_provider_tools( tenant_id=tenant_id, @@ -1188,7 +1188,7 @@ class ToolMCPCallbackApi(Resource): authorization_code = query.code # Create service instance for handle_callback - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: mcp_service = MCPToolManageService(session=session) # handle_callback now returns state data and tokens state_data, tokens = handle_callback(state_key, authorization_code) diff --git a/api/controllers/console/workspace/trigger_providers.py b/api/controllers/console/workspace/trigger_providers.py index 76d64cb97c8..7a28a09861c 100644 --- a/api/controllers/console/workspace/trigger_providers.py +++ b/api/controllers/console/workspace/trigger_providers.py @@ -5,7 +5,7 @@ from flask import make_response, redirect, request from flask_restx import Resource from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, model_validator -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, Forbidden from configs import dify_config @@ -375,7 +375,7 @@ class TriggerSubscriptionDeleteApi(Resource): assert user.current_tenant_id is not None try: - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: # Delete trigger provider subscription TriggerProviderService.delete_trigger_provider( session=session, @@ -388,7 +388,6 @@ class TriggerSubscriptionDeleteApi(Resource): tenant_id=user.current_tenant_id, subscription_id=subscription_id, ) - session.commit() return {"result": "success"} except ValueError as e: raise BadRequest(str(e)) diff --git a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py index e36bd213d92..f2e7104b18e 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py +++ b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py @@ -69,7 +69,7 @@ def client(flask_app_with_containers): return_value=(MagicMock(id="u1"), "t1"), autospec=True, ) -@patch("controllers.console.workspace.tool_providers.Session", autospec=True) +@patch("controllers.console.workspace.tool_providers.sessionmaker", autospec=True) @patch("controllers.console.workspace.tool_providers.MCPToolManageService._reconnect_with_url", autospec=True) @pytest.mark.usefixtures("_mock_cache", "_mock_user_tenant") def test_create_mcp_provider_populates_tools(mock_reconnect, mock_session, mock_current_account_with_tenant, client): @@ -88,7 +88,7 @@ def test_create_mcp_provider_populates_tools(mock_reconnect, mock_session, mock_ create_result.id = "provider-1" svc.create_provider.return_value = create_result svc.get_provider.return_value = MagicMock(id="provider-1", tenant_id="t1") # used by reload path - mock_session.return_value.__enter__.return_value = MagicMock() + mock_session.return_value.begin.return_value.__enter__.return_value = MagicMock() # Patch MCPToolManageService constructed inside controller with patch("controllers.console.workspace.tool_providers.MCPToolManageService", return_value=svc, autospec=True): payload = { diff --git a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py index b4d12bff62a..ca8195af530 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py +++ b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py @@ -306,14 +306,14 @@ class TestTriggerSubscriptionCrud: app.test_request_context("/"), patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch("controllers.console.workspace.trigger_providers.db") as mock_db, - patch("controllers.console.workspace.trigger_providers.Session") as mock_session_cls, + patch("controllers.console.workspace.trigger_providers.sessionmaker") as mock_session_cls, patch("controllers.console.workspace.trigger_providers.TriggerProviderService.delete_trigger_provider"), patch( "controllers.console.workspace.trigger_providers.TriggerSubscriptionOperatorService.delete_plugin_trigger_by_subscription" ), ): mock_db.engine = MagicMock() - mock_session_cls.return_value.__enter__.return_value = mock_session + mock_session_cls.return_value.begin.return_value.__enter__.return_value = mock_session result = method(api, "sub1") @@ -327,14 +327,14 @@ class TestTriggerSubscriptionCrud: app.test_request_context("/"), patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch("controllers.console.workspace.trigger_providers.db") as mock_db, - patch("controllers.console.workspace.trigger_providers.Session") as session_cls, + patch("controllers.console.workspace.trigger_providers.sessionmaker") as session_cls, patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.delete_trigger_provider", side_effect=ValueError("bad"), ), ): mock_db.engine = MagicMock() - session_cls.return_value.__enter__.return_value = MagicMock() + session_cls.return_value.begin.return_value.__enter__.return_value = MagicMock() with pytest.raises(BadRequest): method(api, "sub1") From 19530e880ae7de50d733679d1b281a598e3115bc Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Wed, 1 Apr 2026 06:52:35 +0800 Subject: [PATCH 8/8] =?UTF-8?q?refactor(api):=20clean=20redundant=20type?= =?UTF-8?q?=20ignore=20in=20request=20query=20parsing=20=F0=9F=A4=96?= =?UTF-8?q?=F0=9F=A4=96=F0=9F=A4=96=20(#34350)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/controllers/console/billing/billing.py | 2 +- api/controllers/console/billing/compliance.py | 2 +- api/controllers/console/workspace/account.py | 2 +- api/controllers/console/workspace/model_providers.py | 4 ++-- api/controllers/console/workspace/workspace.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/controllers/console/billing/billing.py b/api/controllers/console/billing/billing.py index ac039f9c5de..23c01eedb1d 100644 --- a/api/controllers/console/billing/billing.py +++ b/api/controllers/console/billing/billing.py @@ -36,7 +36,7 @@ class Subscription(Resource): @only_edition_cloud def get(self): current_user, current_tenant_id = current_account_with_tenant() - args = SubscriptionQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore + args = SubscriptionQuery.model_validate(request.args.to_dict(flat=True)) BillingService.is_tenant_owner_or_admin(current_user) return BillingService.get_subscription(args.plan, args.interval, current_user.email, current_tenant_id) diff --git a/api/controllers/console/billing/compliance.py b/api/controllers/console/billing/compliance.py index afc5f92b685..b5a08e0791f 100644 --- a/api/controllers/console/billing/compliance.py +++ b/api/controllers/console/billing/compliance.py @@ -31,7 +31,7 @@ class ComplianceApi(Resource): @only_edition_cloud def get(self): current_user, current_tenant_id = current_account_with_tenant() - args = ComplianceDownloadQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore + args = ComplianceDownloadQuery.model_validate(request.args.to_dict(flat=True)) ip_address = extract_remote_ip(request) device_info = request.headers.get("User-Agent", "Unknown device") diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index dcd4438b67a..626d330e9de 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -519,7 +519,7 @@ class EducationAutoCompleteApi(Resource): @cloud_edition_billing_enabled @marshal_with(data_fields) def get(self): - payload = request.args.to_dict(flat=True) # type: ignore + payload = request.args.to_dict(flat=True) args = EducationAutocompleteQuery.model_validate(payload) return BillingService.EducationIdentity.autocomplete(args.keywords, args.page, args.limit) diff --git a/api/controllers/console/workspace/model_providers.py b/api/controllers/console/workspace/model_providers.py index 8e0aefc9e3e..cbb9677309e 100644 --- a/api/controllers/console/workspace/model_providers.py +++ b/api/controllers/console/workspace/model_providers.py @@ -99,7 +99,7 @@ class ModelProviderListApi(Resource): _, current_tenant_id = current_account_with_tenant() tenant_id = current_tenant_id - payload = request.args.to_dict(flat=True) # type: ignore + payload = request.args.to_dict(flat=True) args = ParserModelList.model_validate(payload) model_provider_service = ModelProviderService() @@ -118,7 +118,7 @@ class ModelProviderCredentialApi(Resource): _, current_tenant_id = current_account_with_tenant() tenant_id = current_tenant_id # if credential_id is not provided, return current used credential - payload = request.args.to_dict(flat=True) # type: ignore + payload = request.args.to_dict(flat=True) args = ParserCredentialId.model_validate(payload) model_provider_service = ModelProviderService() diff --git a/api/controllers/console/workspace/workspace.py b/api/controllers/console/workspace/workspace.py index 88fd2c010f0..a06b4fd195a 100644 --- a/api/controllers/console/workspace/workspace.py +++ b/api/controllers/console/workspace/workspace.py @@ -155,7 +155,7 @@ class WorkspaceListApi(Resource): @setup_required @admin_required def get(self): - payload = request.args.to_dict(flat=True) # type: ignore + payload = request.args.to_dict(flat=True) args = WorkspaceListQuery.model_validate(payload) stmt = select(Tenant).order_by(Tenant.created_at.desc())