Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b45199611 | ||
|
|
03a20d17fa | ||
|
|
2c518c03f6 | ||
|
|
5aae5c15b3 | ||
|
|
b1724073fc | ||
|
|
904fadde20 | ||
|
|
77ae583b44 | ||
|
|
c3b1508712 | ||
|
|
8208b786ee | ||
|
|
d9c99daf29 | ||
|
|
93eb6d32b5 | ||
|
|
de5af6c2da | ||
|
|
586c8de1a0 |
@@ -29,13 +29,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@@ -91,13 +91,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@@ -142,13 +142,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
run: echo "autofix.ci updates pull request branches, not merge group refs."
|
||||
|
||||
- if: github.event_name != 'merge_group'
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Check Docker Compose inputs
|
||||
if: github.event_name != 'merge_group'
|
||||
@@ -73,12 +73,12 @@ jobs:
|
||||
dify-agent/pyproject.toml
|
||||
dify-agent/uv.lock
|
||||
- if: github.event_name != 'merge_group'
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- if: github.event_name != 'merge_group'
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
|
||||
- name: Generate Docker Compose
|
||||
if: github.event_name != 'merge_group' && steps.docker-compose-changes.outputs.any_changed == 'true'
|
||||
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
username: ${{ env.DOCKERHUB_USER }}
|
||||
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
with:
|
||||
images: ${{ env[matrix.image_name_env] }}
|
||||
|
||||
@@ -159,10 +159,10 @@ jobs:
|
||||
file: "docker/local-sandbox/Dockerfile"
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
- name: Validate Docker image
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
push: false
|
||||
context: ${{ matrix.build_context }}
|
||||
@@ -197,14 +197,14 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
username: ${{ env.DOCKERHUB_USER }}
|
||||
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
with:
|
||||
images: ${{ env[matrix.image_name_env] }}
|
||||
tags: |
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
ws2_app_id: ${{ steps.out.outputs.DIFY_E2E_WS2_APP_ID }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
with:
|
||||
ref: ${{ inputs.cli_ref || github.ref }}
|
||||
persist-credentials: false
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
with:
|
||||
ref: ${{ inputs.cli_ref || github.ref }}
|
||||
persist-credentials: false
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
with:
|
||||
ref: ${{ inputs.cli_ref || github.ref }}
|
||||
persist-credentials: false
|
||||
@@ -233,7 +233,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
with:
|
||||
ref: ${{ inputs.cli_ref || github.ref }}
|
||||
persist-credentials: false
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
with:
|
||||
ref: ${{ inputs.cli_ref || github.ref }}
|
||||
persist-credentials: false
|
||||
@@ -351,7 +351,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
with:
|
||||
ref: ${{ inputs.cli_ref || github.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
working-directory: ./cli
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
dify_tag: ${{ steps.resolve.outputs.dify_tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
DIFY_TAG: ${{ needs.validate.outputs.dify_tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout cli ref
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ inputs.cli_ref || github.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.12"
|
||||
@@ -63,13 +63,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -77,10 +77,10 @@ jobs:
|
||||
file: "web/Dockerfile"
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
push: false
|
||||
context: ${{ matrix.context }}
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
name: Require cherry-pick provenance
|
||||
runs-on: depot-ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -46,13 +46,17 @@ jobs:
|
||||
vdb-changed: ${{ steps.changes.outputs.vdb }}
|
||||
migration-changed: ${{ steps.changes.outputs.migration }}
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
api:
|
||||
- 'api/**'
|
||||
- 'scripts/check_no_new_getattr.py'
|
||||
- 'scripts/ast_grep_rules/no_new_getattr.yml'
|
||||
- '.github/workflows/style.yml'
|
||||
- '.github/workflows/main-ci.yml'
|
||||
- '.github/workflows/api-tests.yml'
|
||||
- 'docker/.env.example'
|
||||
- 'docker/envs/middleware.env.example'
|
||||
@@ -380,6 +384,8 @@ jobs:
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
uses: ./.github/workflows/style.yml
|
||||
with:
|
||||
base-rev: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
|
||||
|
||||
vdb-tests-run:
|
||||
name: Run VDB Tests
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
outputs:
|
||||
external-e2e-changed: ${{ steps.changes.outputs.external_e2e }}
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
|
||||
@@ -17,12 +17,12 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python & UV
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ jobs:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.pull_requests[0].head.repo.full_name != github.repository }}
|
||||
steps:
|
||||
- name: Checkout default branch (trusted code)
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup Python & UV
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
|
||||
@@ -17,12 +17,12 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python & UV
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
|
||||
+14
-17
@@ -2,6 +2,10 @@ name: Style check
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
base-rev:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: style-${{ github.head_ref || github.run_id }}
|
||||
@@ -19,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -33,10 +37,11 @@ jobs:
|
||||
scripts/check_no_new_getattr.py
|
||||
scripts/ast_grep_rules/no_new_getattr.yml
|
||||
.github/workflows/style.yml
|
||||
.github/workflows/main-ci.yml
|
||||
|
||||
- name: Setup UV and Python
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
with:
|
||||
enable-cache: false
|
||||
python-version: "3.12"
|
||||
@@ -54,17 +59,9 @@ jobs:
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: uv run --project api --dev python api/dev/lint_response_contracts.py --fail-on-mismatch
|
||||
|
||||
- name: Fetch merge target ref for getattr guard
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
|
||||
|
||||
- name: Bind merge target branch for getattr guard
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: git show-ref --verify --quiet refs/heads/main || git branch main origin/main
|
||||
|
||||
- name: Run No New Getattr Guard
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: uv run --project api python scripts/check_no_new_getattr.py --mode ci --merge-target main
|
||||
run: uv run --project api python scripts/check_no_new_getattr.py --base-rev "${{ inputs.base-rev }}"
|
||||
|
||||
- name: Run Type Checks
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
@@ -88,7 +85,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -139,7 +136,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -168,7 +165,7 @@ jobs:
|
||||
- name: Restore ESLint cache
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
id: eslint-cache-restore
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: .eslintcache
|
||||
key: ${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }}
|
||||
@@ -185,7 +182,7 @@ jobs:
|
||||
|
||||
- name: Save ESLint cache
|
||||
if: steps.changed-files.outputs.any_changed == 'true' && success() && steps.eslint-cache-restore.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: .eslintcache
|
||||
key: ${{ steps.eslint-cache-restore.outputs.cache-primary-key }}
|
||||
@@ -196,7 +193,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -214,7 +211,7 @@ jobs:
|
||||
.editorconfig
|
||||
|
||||
- name: Super-linter
|
||||
uses: super-linter/super-linter/slim@9e863354e3ff62e0727d37183162c4a88873df41 # v8.6.0
|
||||
uses: super-linter/super-linter/slim@4ce20838b8ab83717e78138c5b3a1407148e0918 # v8.7.0
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
env:
|
||||
BASH_SEVERITY: warning
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
working-directory: sdks/nodejs-client
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
|
||||
- name: Run Claude Code for Translation Sync
|
||||
if: steps.context.outputs.CHANGED_FILES != ''
|
||||
uses: anthropics/claude-code-action@806af32823ef69c8ef357086c573a902af641307 # v1.0.151
|
||||
uses: anthropics/claude-code-action@558b1d6cab4085c7753fe402c10bef0fbb92ac7a # v1.0.165
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
remove_tool_cache: true
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
remove_tool_cache: true
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
uses: ./.github/actions/setup-web
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from core.plugin.plugin_service import PluginService
|
||||
from core.tools.utils.system_encryption import encrypt_system_params
|
||||
from extensions.ext_database import db
|
||||
from models import Tenant
|
||||
from models.account import TenantPluginAutoUpgradeStrategy
|
||||
from models.account import TenantPluginAutoUpgradeCategory, TenantPluginAutoUpgradeStrategy
|
||||
from models.oauth import DatasourceOauthParamConfig, DatasourceProvider
|
||||
from models.provider_ids import DatasourceProviderID, ToolProviderID
|
||||
from models.source import DataSourceApiKeyAuthBinding, DataSourceOauthBinding
|
||||
@@ -406,7 +406,7 @@ def migrate_data_for_plugin():
|
||||
|
||||
|
||||
def _candidate_auto_upgrade_strategy_tenant_ids_stmt(limit: int | None = None):
|
||||
category_count = len(TenantPluginAutoUpgradeStrategy.PluginCategory)
|
||||
category_count = len(TenantPluginAutoUpgradeCategory)
|
||||
stmt = (
|
||||
select(TenantPluginAutoUpgradeStrategy.tenant_id)
|
||||
.group_by(TenantPluginAutoUpgradeStrategy.tenant_id)
|
||||
|
||||
@@ -9,7 +9,12 @@ from werkzeug.exceptions import Unauthorized
|
||||
import services
|
||||
from configs import dify_config
|
||||
from constants.languages import get_valid_language
|
||||
from controllers.common.fields import SimpleResultDataResponse, SimpleResultOptionalDataResponse, SimpleResultResponse
|
||||
from controllers.common.fields import (
|
||||
SimpleResultDataResponse,
|
||||
SimpleResultMessageResponse,
|
||||
SimpleResultOptionalDataResponse,
|
||||
SimpleResultResponse,
|
||||
)
|
||||
from controllers.common.schema import register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.auth.error import (
|
||||
@@ -87,6 +92,7 @@ register_schema_models(console_ns, LoginPayload, EmailPayload, EmailCodeLoginPay
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
SimpleResultDataResponse,
|
||||
SimpleResultMessageResponse,
|
||||
SimpleResultOptionalDataResponse,
|
||||
SimpleResultResponse,
|
||||
)
|
||||
@@ -154,16 +160,19 @@ class LoginApi(Resource):
|
||||
if system_features.is_allow_create_workspace and not system_features.license.workspaces.is_available():
|
||||
raise WorkspacesLimitExceeded()
|
||||
else:
|
||||
return {
|
||||
"result": "fail",
|
||||
"data": "workspace not found, please contact system admin to invite you to join in a workspace",
|
||||
}
|
||||
return SimpleResultOptionalDataResponse(
|
||||
result="fail",
|
||||
data="workspace not found, please contact system admin to invite you to join in a workspace",
|
||||
).model_dump(mode="json")
|
||||
|
||||
token_pair = AccountService.login(account=account, session=db.session, ip_address=extract_remote_ip(request))
|
||||
AccountService.reset_login_error_rate_limit(normalized_email)
|
||||
|
||||
# Create response with cookies instead of returning tokens in body
|
||||
response = make_response({"result": "success"})
|
||||
# response-contract:ignore cookie-bearing Flask response
|
||||
response = make_response(
|
||||
SimpleResultOptionalDataResponse(result="success").model_dump(mode="json", exclude_none=True)
|
||||
)
|
||||
|
||||
set_access_token_to_cookie(request, response, token_pair.access_token)
|
||||
set_refresh_token_to_cookie(request, response, token_pair.refresh_token)
|
||||
@@ -178,12 +187,11 @@ class LogoutApi(Resource):
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@with_current_user
|
||||
def post(self, account: Account):
|
||||
if isinstance(account, flask_login.AnonymousUserMixin):
|
||||
response = make_response({"result": "success"})
|
||||
else:
|
||||
# response-contract:ignore cookie-bearing Flask response
|
||||
response = make_response(SimpleResultResponse(result="success").model_dump(mode="json"))
|
||||
if not isinstance(account, flask_login.AnonymousUserMixin):
|
||||
AccountService.logout(account=account)
|
||||
flask_login.logout_user()
|
||||
response = make_response({"result": "success"})
|
||||
|
||||
# Clear cookies on logout
|
||||
clear_access_token_from_cookie(response)
|
||||
@@ -219,7 +227,7 @@ class ResetPasswordSendEmailApi(Resource):
|
||||
is_allow_register=FeatureService.get_system_features().is_allow_register,
|
||||
)
|
||||
|
||||
return {"result": "success", "data": token}
|
||||
return SimpleResultDataResponse(result="success", data=token).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/email-code-login")
|
||||
@@ -252,7 +260,7 @@ class EmailCodeLoginSendEmailApi(Resource):
|
||||
else:
|
||||
token = AccountService.send_email_code_login_email(account=account, language=language)
|
||||
|
||||
return {"result": "success", "data": token}
|
||||
return SimpleResultDataResponse(result="success", data=token).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/email-code-login/validity")
|
||||
@@ -326,7 +334,8 @@ class EmailCodeLoginApi(Resource):
|
||||
AccountService.reset_login_error_rate_limit(user_email)
|
||||
|
||||
# Create response with cookies instead of returning tokens in body
|
||||
response = make_response({"result": "success"})
|
||||
# response-contract:ignore cookie-bearing Flask response
|
||||
response = make_response(SimpleResultResponse(result="success").model_dump(mode="json"))
|
||||
|
||||
set_csrf_token_to_cookie(request, response, token_pair.csrf_token)
|
||||
# Set HTTP-only secure cookies for tokens
|
||||
@@ -338,18 +347,22 @@ class EmailCodeLoginApi(Resource):
|
||||
@console_ns.route("/refresh-token")
|
||||
class RefreshTokenApi(Resource):
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@console_ns.response(401, "Unauthorized", console_ns.models[SimpleResultMessageResponse.__name__])
|
||||
def post(self):
|
||||
# Get refresh token from cookie instead of request body
|
||||
refresh_token = extract_refresh_token(request)
|
||||
|
||||
if not refresh_token:
|
||||
return {"result": "fail", "message": "No refresh token provided"}, 401
|
||||
return SimpleResultMessageResponse(result="fail", message="No refresh token provided").model_dump(
|
||||
mode="json"
|
||||
), 401
|
||||
|
||||
try:
|
||||
new_token_pair = AccountService.refresh_token(refresh_token, session=db.session)
|
||||
|
||||
# Create response with new cookies
|
||||
response = make_response({"result": "success"})
|
||||
# response-contract:ignore cookie-bearing Flask response
|
||||
response = make_response(SimpleResultResponse(result="success").model_dump(mode="json"))
|
||||
|
||||
# Update cookies with new tokens
|
||||
set_csrf_token_to_cookie(request, response, new_token_pair.csrf_token)
|
||||
@@ -357,7 +370,7 @@ class RefreshTokenApi(Resource):
|
||||
set_refresh_token_to_cookie(request, response, new_token_pair.refresh_token)
|
||||
return response
|
||||
except Exception as e:
|
||||
return {"result": "fail", "message": str(e)}, 401
|
||||
return SimpleResultMessageResponse(result="fail", message=str(e)).model_dump(mode="json"), 401
|
||||
|
||||
|
||||
def _get_account_with_case_fallback(email: str):
|
||||
|
||||
@@ -6,7 +6,7 @@ from pydantic import BaseModel, Field
|
||||
from werkzeug.exceptions import Forbidden, NotFound
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.common.fields import RedirectResponse, SimpleResultResponse
|
||||
from controllers.common.fields import SimpleResultResponse
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import (
|
||||
@@ -19,11 +19,13 @@ from controllers.console.wraps import (
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
)
|
||||
from core.entities.provider_entities import ProviderConfig
|
||||
from core.plugin.entities.plugin_daemon import PluginOAuthAuthorizationUrlResponse
|
||||
from core.plugin.impl.oauth import OAuthHandler
|
||||
from core.tools.entities.common_entities import I18nObject
|
||||
from fields.base import ResponseModel
|
||||
from graphon.model_runtime.errors.validate import CredentialsValidateFailedError
|
||||
from graphon.model_runtime.utils.encoders import jsonable_encoder
|
||||
from libs.helper import dump_response
|
||||
from libs.login import login_required
|
||||
from models import Account
|
||||
from models.provider_ids import DatasourceProviderID
|
||||
@@ -33,7 +35,9 @@ from services.plugin.oauth_service import OAuthProxyService
|
||||
|
||||
class DatasourceCredentialPayload(BaseModel):
|
||||
name: str | None = Field(default=None, max_length=100)
|
||||
credentials: dict[str, Any]
|
||||
credentials: dict[str, Any] = Field(
|
||||
description="Plugin-defined credential parameters. The schema is declared by the datasource provider."
|
||||
)
|
||||
|
||||
|
||||
class DatasourceCredentialDeletePayload(BaseModel):
|
||||
@@ -43,11 +47,17 @@ class DatasourceCredentialDeletePayload(BaseModel):
|
||||
class DatasourceCredentialUpdatePayload(BaseModel):
|
||||
credential_id: str
|
||||
name: str | None = Field(default=None, max_length=100)
|
||||
credentials: dict[str, Any] | None = Field(default=None)
|
||||
credentials: dict[str, Any] | None = Field(
|
||||
default=None,
|
||||
description="Plugin-defined credential parameters. The schema is declared by the datasource provider.",
|
||||
)
|
||||
|
||||
|
||||
class DatasourceCustomClientPayload(BaseModel):
|
||||
client_params: dict[str, Any] | None = Field(default=None)
|
||||
client_params: dict[str, Any] | None = Field(
|
||||
default=None,
|
||||
description="Plugin-defined OAuth client parameters. The schema is declared by the datasource provider.",
|
||||
)
|
||||
enable_oauth_custom_client: bool | None = None
|
||||
|
||||
|
||||
@@ -71,8 +81,48 @@ class DatasourceOAuthCallbackQuery(BaseModel):
|
||||
context_id: str | None = Field(default=None, description="OAuth proxy context ID")
|
||||
|
||||
|
||||
class DatasourceCredentialsResponse(ResponseModel):
|
||||
result: Any
|
||||
class DatasourceCredentialResponse(ResponseModel):
|
||||
credential: dict[str, Any] = Field(
|
||||
description="Obfuscated plugin-defined credential parameters from the datasource provider."
|
||||
)
|
||||
type: str
|
||||
name: str
|
||||
avatar_url: str | None
|
||||
id: str
|
||||
is_default: bool
|
||||
|
||||
|
||||
class DatasourceCredentialListResponse(ResponseModel):
|
||||
result: list[DatasourceCredentialResponse]
|
||||
|
||||
|
||||
class DatasourceOAuthSchemaResponse(ResponseModel):
|
||||
client_schema: list[ProviderConfig]
|
||||
credentials_schema: list[ProviderConfig]
|
||||
oauth_custom_client_params: dict[str, Any] | None = Field(
|
||||
description="Masked plugin-defined OAuth client parameters, when configured for the tenant."
|
||||
)
|
||||
is_oauth_custom_client_enabled: bool
|
||||
is_system_oauth_params_exists: bool
|
||||
redirect_uri: str
|
||||
|
||||
|
||||
class DatasourceProviderAuthResponse(ResponseModel):
|
||||
author: str
|
||||
provider: str
|
||||
plugin_id: str
|
||||
plugin_unique_identifier: str
|
||||
icon: str
|
||||
name: str
|
||||
label: I18nObject
|
||||
description: I18nObject
|
||||
credential_schema: list[ProviderConfig]
|
||||
oauth_schema: DatasourceOAuthSchemaResponse | None
|
||||
credentials_list: list[DatasourceCredentialResponse]
|
||||
|
||||
|
||||
class DatasourceProviderAuthListResponse(ResponseModel):
|
||||
result: list[DatasourceProviderAuthResponse]
|
||||
|
||||
|
||||
register_schema_models(
|
||||
@@ -88,9 +138,9 @@ register_schema_models(
|
||||
)
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
DatasourceCredentialsResponse,
|
||||
DatasourceCredentialListResponse,
|
||||
DatasourceProviderAuthListResponse,
|
||||
PluginOAuthAuthorizationUrlResponse,
|
||||
RedirectResponse,
|
||||
SimpleResultResponse,
|
||||
)
|
||||
|
||||
@@ -100,7 +150,7 @@ class DatasourcePluginOAuthAuthorizationUrl(Resource):
|
||||
@console_ns.doc(params=query_params_from_model(DatasourceOAuthAuthorizationQuery))
|
||||
@console_ns.response(
|
||||
200,
|
||||
"Authorization URL retrieved successfully",
|
||||
"Datasource OAuth authorization URL generated successfully",
|
||||
console_ns.models[PluginOAuthAuthorizationUrlResponse.__name__],
|
||||
)
|
||||
@setup_required
|
||||
@@ -140,7 +190,8 @@ class DatasourcePluginOAuthAuthorizationUrl(Resource):
|
||||
redirect_uri=redirect_uri,
|
||||
system_credentials=oauth_config,
|
||||
)
|
||||
response = make_response(jsonable_encoder(authorization_url_response))
|
||||
# response-contract:ignore cookie-bearing Flask response
|
||||
response = make_response(dump_response(PluginOAuthAuthorizationUrlResponse, authorization_url_response))
|
||||
response.set_cookie(
|
||||
"context_id",
|
||||
context_id,
|
||||
@@ -154,11 +205,8 @@ class DatasourcePluginOAuthAuthorizationUrl(Resource):
|
||||
@console_ns.route("/oauth/plugin/<path:provider_id>/datasource/callback")
|
||||
class DatasourceOAuthCallback(Resource):
|
||||
@console_ns.doc(params=query_params_from_model(DatasourceOAuthCallbackQuery))
|
||||
@console_ns.response(
|
||||
302,
|
||||
"Redirect to console OAuth callback page",
|
||||
console_ns.models[RedirectResponse.__name__],
|
||||
)
|
||||
# response-contract:ignore redirect response
|
||||
@console_ns.response(302, "Redirect to OAuth callback page")
|
||||
@setup_required
|
||||
def get(self, provider_id: str):
|
||||
context_id = request.cookies.get("context_id") or request.args.get("context_id")
|
||||
@@ -217,7 +265,9 @@ class DatasourceOAuthCallback(Resource):
|
||||
@console_ns.route("/auth/plugin/datasource/<path:provider_id>")
|
||||
class DatasourceAuth(Resource):
|
||||
@console_ns.expect(console_ns.models[DatasourceCredentialPayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@console_ns.response(
|
||||
200, "Datasource credential created successfully", console_ns.models[SimpleResultResponse.__name__]
|
||||
)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -238,12 +288,16 @@ class DatasourceAuth(Resource):
|
||||
)
|
||||
except CredentialsValidateFailedError as ex:
|
||||
raise ValueError(str(ex))
|
||||
return {"result": "success"}, 200
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
|
||||
|
||||
@console_ns.response(
|
||||
200,
|
||||
"Datasource credentials retrieved successfully",
|
||||
console_ns.models[DatasourceCredentialListResponse.__name__],
|
||||
)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[DatasourceCredentialsResponse.__name__])
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, user: Account, provider_id: str):
|
||||
@@ -256,7 +310,7 @@ class DatasourceAuth(Resource):
|
||||
plugin_id=datasource_provider_id.plugin_id,
|
||||
user=user,
|
||||
)
|
||||
return {"result": datasources}, 200
|
||||
return dump_response(DatasourceCredentialListResponse, {"result": datasources}), 200
|
||||
|
||||
|
||||
@console_ns.route("/auth/plugin/datasource/<path:provider_id>/delete")
|
||||
@@ -282,13 +336,15 @@ class DatasourceAuthDeleteApi(Resource):
|
||||
provider=provider_name,
|
||||
plugin_id=plugin_id,
|
||||
)
|
||||
return {"result": "success"}, 200
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
|
||||
|
||||
|
||||
@console_ns.route("/auth/plugin/datasource/<path:provider_id>/update")
|
||||
class DatasourceAuthUpdateApi(Resource):
|
||||
@console_ns.expect(console_ns.models[DatasourceCredentialUpdatePayload.__name__])
|
||||
@console_ns.response(201, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@console_ns.response(
|
||||
201, "Datasource credential updated successfully", console_ns.models[SimpleResultResponse.__name__]
|
||||
)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -308,12 +364,16 @@ class DatasourceAuthUpdateApi(Resource):
|
||||
credentials=payload.credentials or {},
|
||||
name=payload.name,
|
||||
)
|
||||
return {"result": "success"}, 201
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json"), 201
|
||||
|
||||
|
||||
@console_ns.route("/auth/plugin/datasource/list")
|
||||
class DatasourceAuthListApi(Resource):
|
||||
@console_ns.response(200, "Success", console_ns.models[DatasourceCredentialsResponse.__name__])
|
||||
@console_ns.response(
|
||||
200,
|
||||
"Datasource credentials retrieved successfully",
|
||||
console_ns.models[DatasourceProviderAuthListResponse.__name__],
|
||||
)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -321,12 +381,16 @@ class DatasourceAuthListApi(Resource):
|
||||
def get(self, current_tenant_id: str):
|
||||
datasource_provider_service = DatasourceProviderService()
|
||||
datasources = datasource_provider_service.get_all_datasource_credentials(tenant_id=current_tenant_id)
|
||||
return {"result": jsonable_encoder(datasources)}, 200
|
||||
return dump_response(DatasourceProviderAuthListResponse, {"result": datasources}), 200
|
||||
|
||||
|
||||
@console_ns.route("/auth/plugin/datasource/default-list")
|
||||
class DatasourceHardCodeAuthListApi(Resource):
|
||||
@console_ns.response(200, "Success", console_ns.models[DatasourceCredentialsResponse.__name__])
|
||||
@console_ns.response(
|
||||
200,
|
||||
"Default datasource credentials retrieved successfully",
|
||||
console_ns.models[DatasourceProviderAuthListResponse.__name__],
|
||||
)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -334,13 +398,15 @@ class DatasourceHardCodeAuthListApi(Resource):
|
||||
def get(self, current_tenant_id: str):
|
||||
datasource_provider_service = DatasourceProviderService()
|
||||
datasources = datasource_provider_service.get_hard_code_datasource_credentials(tenant_id=current_tenant_id)
|
||||
return {"result": jsonable_encoder(datasources)}, 200
|
||||
return dump_response(DatasourceProviderAuthListResponse, {"result": datasources}), 200
|
||||
|
||||
|
||||
@console_ns.route("/auth/plugin/datasource/<path:provider_id>/custom-client")
|
||||
class DatasourceAuthOauthCustomClient(Resource):
|
||||
@console_ns.expect(console_ns.models[DatasourceCustomClientPayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@console_ns.response(
|
||||
200, "Datasource OAuth custom client saved successfully", console_ns.models[SimpleResultResponse.__name__]
|
||||
)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -357,7 +423,7 @@ class DatasourceAuthOauthCustomClient(Resource):
|
||||
client_params=payload.client_params or {},
|
||||
enabled=payload.enable_oauth_custom_client or False,
|
||||
)
|
||||
return {"result": "success"}, 200
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@@ -371,7 +437,7 @@ class DatasourceAuthOauthCustomClient(Resource):
|
||||
tenant_id=current_tenant_id,
|
||||
datasource_provider_id=datasource_provider_id,
|
||||
)
|
||||
return {"result": "success"}, 200
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
|
||||
|
||||
|
||||
@console_ns.route("/auth/plugin/datasource/<path:provider_id>/default")
|
||||
@@ -393,7 +459,7 @@ class DatasourceAuthDefaultApi(Resource):
|
||||
datasource_provider_id=datasource_provider_id,
|
||||
credential_id=payload.id,
|
||||
)
|
||||
return {"result": "success"}, 200
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
|
||||
|
||||
|
||||
@console_ns.route("/auth/plugin/datasource/<path:provider_id>/update-name")
|
||||
@@ -416,4 +482,4 @@ class DatasourceUpdateProviderNameApi(Resource):
|
||||
name=payload.name,
|
||||
credential_id=payload.credential_id,
|
||||
)
|
||||
return {"result": "success"}, 200
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
|
||||
|
||||
@@ -3,9 +3,9 @@ from typing import Any
|
||||
from flask_restx import ( # type: ignore
|
||||
Resource, # type: ignore
|
||||
)
|
||||
from pydantic import BaseModel, RootModel
|
||||
from pydantic import BaseModel
|
||||
|
||||
from controllers.common.schema import register_response_schema_models, register_schema_models
|
||||
from controllers.common.schema import register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.datasets.wraps import get_rag_pipeline
|
||||
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
|
||||
@@ -21,18 +21,13 @@ class Parser(BaseModel):
|
||||
credential_id: str | None = None
|
||||
|
||||
|
||||
class DataSourceContentPreviewResponse(RootModel[Any]):
|
||||
root: Any
|
||||
|
||||
|
||||
register_schema_models(console_ns, Parser)
|
||||
register_response_schema_models(console_ns, DataSourceContentPreviewResponse)
|
||||
|
||||
|
||||
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/published/datasource/nodes/<string:node_id>/preview")
|
||||
class DataSourceContentPreviewApi(Resource):
|
||||
@console_ns.expect(console_ns.models[Parser.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[DataSourceContentPreviewResponse.__name__])
|
||||
@console_ns.response(200, "Success")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
|
||||
@@ -13,7 +13,6 @@ import services
|
||||
from controllers.common.fields import (
|
||||
AudioBinaryResponse,
|
||||
AudioTranscriptResponse,
|
||||
GeneratedAppResponse,
|
||||
SimpleResultResponse,
|
||||
)
|
||||
from controllers.common.fields import Parameters as ParametersResponse
|
||||
@@ -391,7 +390,6 @@ register_response_schema_models(
|
||||
ParametersResponse,
|
||||
AudioBinaryResponse,
|
||||
AudioTranscriptResponse,
|
||||
GeneratedAppResponse,
|
||||
SimpleResultResponse,
|
||||
SiteResponse,
|
||||
SuggestedQuestionsResponse,
|
||||
@@ -406,7 +404,7 @@ simple_account_model = console_ns.models[TrialSimpleAccount.__name__]
|
||||
class TrialAppWorkflowRunApi(TrialAppResource):
|
||||
@trial_feature_enable
|
||||
@console_ns.expect(console_ns.models[WorkflowRunRequest.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[GeneratedAppResponse.__name__])
|
||||
@console_ns.response(200, "Success")
|
||||
@with_current_user
|
||||
@with_session
|
||||
def post(self, session: Session, current_user: Account, trial_app):
|
||||
@@ -434,6 +432,7 @@ class TrialAppWorkflowRunApi(TrialAppResource):
|
||||
streaming=True,
|
||||
)
|
||||
RecommendedAppService.add_trial_app_record(db.session, app_id, user_id)
|
||||
# response-contract:ignore compact_generate_response
|
||||
return helper.compact_generate_response(response)
|
||||
except ProviderTokenNotInitError as ex:
|
||||
raise ProviderNotInitializeError(ex.description)
|
||||
@@ -478,7 +477,7 @@ class TrialAppWorkflowTaskStopApi(TrialAppResource):
|
||||
|
||||
class TrialChatApi(TrialAppResource):
|
||||
@console_ns.expect(console_ns.models[ChatRequest.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[GeneratedAppResponse.__name__])
|
||||
@console_ns.response(200, "Success")
|
||||
@trial_feature_enable
|
||||
@with_current_user
|
||||
@with_session
|
||||
@@ -513,6 +512,7 @@ class TrialChatApi(TrialAppResource):
|
||||
streaming=True,
|
||||
)
|
||||
RecommendedAppService.add_trial_app_record(db.session, app_id, user_id)
|
||||
# response-contract:ignore compact_generate_response
|
||||
return helper.compact_generate_response(response)
|
||||
except services.errors.conversation.ConversationNotExistsError:
|
||||
raise NotFound("Conversation Not Exists.")
|
||||
@@ -680,7 +680,7 @@ class TrialChatTextApi(TrialAppResource):
|
||||
|
||||
class TrialCompletionApi(TrialAppResource):
|
||||
@console_ns.expect(console_ns.models[CompletionRequest.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[GeneratedAppResponse.__name__])
|
||||
@console_ns.response(200, "Success")
|
||||
@trial_feature_enable
|
||||
@with_current_user
|
||||
@with_session
|
||||
@@ -710,6 +710,7 @@ class TrialCompletionApi(TrialAppResource):
|
||||
)
|
||||
|
||||
RecommendedAppService.add_trial_app_record(db.session, app_id, user_id)
|
||||
# response-contract:ignore compact_generate_response
|
||||
return helper.compact_generate_response(response)
|
||||
except services.errors.conversation.ConversationNotExistsError:
|
||||
raise NotFound("Conversation Not Exists.")
|
||||
|
||||
@@ -8,7 +8,7 @@ from werkzeug.exceptions import Forbidden
|
||||
from configs import dify_config
|
||||
from extensions.ext_database import db
|
||||
from libs.login import current_account_with_tenant
|
||||
from models.account import TenantPluginPermission
|
||||
from models.account import TenantPluginDebugPermission, TenantPluginInstallPermission, TenantPluginPermission
|
||||
|
||||
|
||||
def plugin_permission_required(
|
||||
@@ -40,22 +40,22 @@ def plugin_permission_required(
|
||||
|
||||
if install_required:
|
||||
match permission.install_permission:
|
||||
case TenantPluginPermission.InstallPermission.NOBODY:
|
||||
case TenantPluginInstallPermission.NOBODY:
|
||||
raise Forbidden()
|
||||
case TenantPluginPermission.InstallPermission.ADMINS:
|
||||
case TenantPluginInstallPermission.ADMINS:
|
||||
if not user.is_admin_or_owner:
|
||||
raise Forbidden()
|
||||
case TenantPluginPermission.InstallPermission.EVERYONE:
|
||||
case TenantPluginInstallPermission.EVERYONE:
|
||||
pass
|
||||
|
||||
if debug_required:
|
||||
match permission.debug_permission:
|
||||
case TenantPluginPermission.DebugPermission.NOBODY:
|
||||
case TenantPluginDebugPermission.NOBODY:
|
||||
raise Forbidden()
|
||||
case TenantPluginPermission.DebugPermission.ADMINS:
|
||||
case TenantPluginDebugPermission.ADMINS:
|
||||
if not user.is_admin_or_owner:
|
||||
raise Forbidden()
|
||||
case TenantPluginPermission.DebugPermission.EVERYONE:
|
||||
case TenantPluginDebugPermission.EVERYONE:
|
||||
pass
|
||||
|
||||
return view(*args, **kwargs)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal
|
||||
from http import HTTPStatus
|
||||
from typing import Literal
|
||||
|
||||
import pytz
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, RootModel, field_validator, model_validator
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
from sqlalchemy import select
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
@@ -47,7 +48,7 @@ from controllers.console.wraps import (
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from fields.member_fields import Account as AccountResponse
|
||||
from fields.member_fields import AccountResponse
|
||||
from graphon.file import helpers as file_helpers
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from libs.helper import EmailStr, dump_response, extract_remote_ip, timezone, to_timestamp
|
||||
@@ -194,10 +195,6 @@ register_schema_models(
|
||||
)
|
||||
|
||||
|
||||
def _serialize_account(account) -> dict[str, Any]:
|
||||
return AccountResponse.model_validate(account, from_attributes=True).model_dump(mode="json")
|
||||
|
||||
|
||||
class AccountIntegrateResponse(ResponseModel):
|
||||
provider: str
|
||||
created_at: int | None = None
|
||||
@@ -236,23 +233,15 @@ class EducationAutocompleteResponse(ResponseModel):
|
||||
has_next: bool | None = None
|
||||
|
||||
|
||||
class EducationActivateResponse(RootModel[dict[str, Any]]):
|
||||
root: dict[str, Any]
|
||||
|
||||
|
||||
register_schema_models(
|
||||
console_ns,
|
||||
AccountIntegrateResponse,
|
||||
AccountIntegrateListResponse,
|
||||
EducationVerifyResponse,
|
||||
EducationStatusResponse,
|
||||
EducationAutocompleteResponse,
|
||||
)
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
AccountResponse,
|
||||
AccountIntegrateResponse,
|
||||
AccountIntegrateListResponse,
|
||||
AvatarUrlResponse,
|
||||
EducationActivateResponse,
|
||||
EducationVerifyResponse,
|
||||
EducationStatusResponse,
|
||||
EducationAutocompleteResponse,
|
||||
SimpleResultDataResponse,
|
||||
SimpleResultResponse,
|
||||
VerificationTokenResponse,
|
||||
@@ -262,7 +251,7 @@ register_response_schema_models(
|
||||
@console_ns.route("/account/init")
|
||||
class AccountInitApi(Resource):
|
||||
@console_ns.expect(console_ns.models[AccountInitPayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@with_current_user
|
||||
@@ -302,7 +291,7 @@ class AccountInitApi(Resource):
|
||||
account.initialized_at = naive_utc_now()
|
||||
db.session.commit()
|
||||
|
||||
return {"result": "success"}
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/account/profile")
|
||||
@@ -310,11 +299,11 @@ class AccountProfileApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
|
||||
@enterprise_license_required
|
||||
@with_current_user
|
||||
def get(self, current_user: Account):
|
||||
return _serialize_account(current_user)
|
||||
return dump_response(AccountResponse, current_user)
|
||||
|
||||
|
||||
@console_ns.route("/account/name")
|
||||
@@ -323,14 +312,14 @@ class AccountNameApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
|
||||
@with_current_user
|
||||
def post(self, current_user: Account):
|
||||
payload = console_ns.payload or {}
|
||||
args = AccountNamePayload.model_validate(payload)
|
||||
updated_account = AccountService.update_account(current_user, session=db.session, name=args.name)
|
||||
|
||||
return _serialize_account(updated_account)
|
||||
return dump_response(AccountResponse, updated_account)
|
||||
|
||||
|
||||
@console_ns.route("/account/avatar")
|
||||
@@ -338,7 +327,7 @@ class AccountAvatarApi(Resource):
|
||||
@console_ns.doc("get_account_avatar")
|
||||
@console_ns.doc(description="Get account avatar url")
|
||||
@console_ns.doc(params=query_params_from_model(AccountAvatarQuery))
|
||||
@console_ns.response(200, "Success", console_ns.models[AvatarUrlResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AvatarUrlResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -349,7 +338,7 @@ class AccountAvatarApi(Resource):
|
||||
avatar = args.avatar
|
||||
|
||||
if avatar.startswith(("http://", "https://")):
|
||||
return dump_response(AvatarUrlResponse, {"avatar_url": avatar})
|
||||
return AvatarUrlResponse(avatar_url=avatar).model_dump(mode="json")
|
||||
|
||||
upload_file = db.session.scalar(select(UploadFile).where(UploadFile.id == avatar).limit(1))
|
||||
if upload_file is None:
|
||||
@@ -362,13 +351,13 @@ class AccountAvatarApi(Resource):
|
||||
raise NotFound("Avatar file not found")
|
||||
|
||||
avatar_url = file_helpers.get_signed_file_url(upload_file_id=upload_file.id)
|
||||
return dump_response(AvatarUrlResponse, {"avatar_url": avatar_url})
|
||||
return AvatarUrlResponse(avatar_url=avatar_url).model_dump(mode="json")
|
||||
|
||||
@console_ns.expect(console_ns.models[AccountAvatarPayload.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
|
||||
@with_current_user
|
||||
def post(self, current_user: Account):
|
||||
payload = console_ns.payload or {}
|
||||
@@ -376,7 +365,7 @@ class AccountAvatarApi(Resource):
|
||||
|
||||
updated_account = AccountService.update_account(current_user, session=db.session, avatar=args.avatar)
|
||||
|
||||
return _serialize_account(updated_account)
|
||||
return dump_response(AccountResponse, updated_account)
|
||||
|
||||
|
||||
@console_ns.route("/account/interface-language")
|
||||
@@ -385,7 +374,7 @@ class AccountInterfaceLanguageApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
|
||||
@with_current_user
|
||||
def post(self, current_user: Account):
|
||||
payload = console_ns.payload or {}
|
||||
@@ -395,7 +384,7 @@ class AccountInterfaceLanguageApi(Resource):
|
||||
current_user, session=db.session, interface_language=args.interface_language
|
||||
)
|
||||
|
||||
return _serialize_account(updated_account)
|
||||
return dump_response(AccountResponse, updated_account)
|
||||
|
||||
|
||||
@console_ns.route("/account/interface-theme")
|
||||
@@ -404,7 +393,7 @@ class AccountInterfaceThemeApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
|
||||
@with_current_user
|
||||
def post(self, current_user: Account):
|
||||
payload = console_ns.payload or {}
|
||||
@@ -414,7 +403,7 @@ class AccountInterfaceThemeApi(Resource):
|
||||
current_user, session=db.session, interface_theme=args.interface_theme
|
||||
)
|
||||
|
||||
return _serialize_account(updated_account)
|
||||
return dump_response(AccountResponse, updated_account)
|
||||
|
||||
|
||||
@console_ns.route("/account/timezone")
|
||||
@@ -423,7 +412,7 @@ class AccountTimezoneApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
|
||||
@with_current_user
|
||||
def post(self, current_user: Account):
|
||||
payload = console_ns.payload or {}
|
||||
@@ -431,7 +420,7 @@ class AccountTimezoneApi(Resource):
|
||||
|
||||
updated_account = AccountService.update_account(current_user, session=db.session, timezone=args.timezone)
|
||||
|
||||
return _serialize_account(updated_account)
|
||||
return dump_response(AccountResponse, updated_account)
|
||||
|
||||
|
||||
@console_ns.route("/account/password")
|
||||
@@ -440,7 +429,7 @@ class AccountPasswordApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
|
||||
@with_current_user
|
||||
def post(self, current_user: Account):
|
||||
payload = console_ns.payload or {}
|
||||
@@ -452,7 +441,7 @@ class AccountPasswordApi(Resource):
|
||||
except ServiceCurrentPasswordIncorrectError:
|
||||
raise CurrentPasswordIncorrectError()
|
||||
|
||||
return _serialize_account(current_user)
|
||||
return dump_response(AccountResponse, current_user)
|
||||
|
||||
|
||||
@console_ns.route("/account/integrates")
|
||||
@@ -460,7 +449,7 @@ class AccountIntegrateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[AccountIntegrateListResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountIntegrateListResponse.__name__])
|
||||
@with_current_user
|
||||
def get(self, account: Account):
|
||||
account_integrates = db.session.scalars(
|
||||
@@ -471,33 +460,29 @@ class AccountIntegrateApi(Resource):
|
||||
oauth_base_path = "/console/api/oauth/login"
|
||||
providers = ["github", "google"]
|
||||
|
||||
integrate_data = []
|
||||
integrate_data: list[AccountIntegrateResponse] = []
|
||||
for provider in providers:
|
||||
existing_integrate = next((ai for ai in account_integrates if ai.provider == provider), None)
|
||||
if existing_integrate:
|
||||
integrate_data.append(
|
||||
{
|
||||
"id": existing_integrate.id,
|
||||
"provider": provider,
|
||||
"created_at": existing_integrate.created_at,
|
||||
"is_bound": True,
|
||||
"link": None,
|
||||
}
|
||||
AccountIntegrateResponse(
|
||||
provider=provider,
|
||||
created_at=to_timestamp(existing_integrate.created_at),
|
||||
is_bound=True,
|
||||
link=None,
|
||||
)
|
||||
)
|
||||
else:
|
||||
integrate_data.append(
|
||||
{
|
||||
"id": None,
|
||||
"provider": provider,
|
||||
"created_at": None,
|
||||
"is_bound": False,
|
||||
"link": f"{base_url}{oauth_base_path}/{provider}",
|
||||
}
|
||||
AccountIntegrateResponse(
|
||||
provider=provider,
|
||||
created_at=None,
|
||||
is_bound=False,
|
||||
link=f"{base_url}{oauth_base_path}/{provider}",
|
||||
)
|
||||
)
|
||||
|
||||
return AccountIntegrateListResponse(
|
||||
data=[AccountIntegrateResponse.model_validate(item) for item in integrate_data]
|
||||
).model_dump(mode="json")
|
||||
return AccountIntegrateListResponse(data=integrate_data).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/account/delete/verify")
|
||||
@@ -505,19 +490,19 @@ class AccountDeleteVerifyApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultDataResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultDataResponse.__name__])
|
||||
@with_current_user
|
||||
def get(self, account: Account):
|
||||
token, code = AccountService.generate_account_deletion_verification_code(account)
|
||||
AccountService.send_account_deletion_verification_email(account, code)
|
||||
|
||||
return {"result": "success", "data": token}
|
||||
return SimpleResultDataResponse(result="success", data=token).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/account/delete")
|
||||
class AccountDeleteApi(Resource):
|
||||
@console_ns.expect(console_ns.models[AccountDeletePayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -531,13 +516,13 @@ class AccountDeleteApi(Resource):
|
||||
|
||||
AccountService.delete_account(account)
|
||||
|
||||
return {"result": "success"}
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/account/delete/feedback")
|
||||
class AccountDeleteUpdateFeedbackApi(Resource):
|
||||
@console_ns.expect(console_ns.models[AccountDeletionFeedbackPayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@setup_required
|
||||
def post(self):
|
||||
payload = console_ns.payload or {}
|
||||
@@ -545,7 +530,7 @@ class AccountDeleteUpdateFeedbackApi(Resource):
|
||||
|
||||
BillingService.update_account_deletion_feedback(args.email, args.feedback)
|
||||
|
||||
return {"result": "success"}
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/account/education/verify")
|
||||
@@ -555,18 +540,19 @@ class EducationVerifyApi(Resource):
|
||||
@account_initialization_required
|
||||
@only_edition_cloud
|
||||
@cloud_edition_billing_enabled
|
||||
@console_ns.response(200, "Success", console_ns.models[EducationVerifyResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[EducationVerifyResponse.__name__])
|
||||
@with_current_user
|
||||
def get(self, account: Account):
|
||||
return EducationVerifyResponse.model_validate(
|
||||
BillingService.EducationIdentity.verify(account.id, account.email) or {}
|
||||
).model_dump(mode="json")
|
||||
return dump_response(
|
||||
EducationVerifyResponse, BillingService.EducationIdentity.verify(account.id, account.email) or {}
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/account/education")
|
||||
class EducationApi(Resource):
|
||||
@console_ns.expect(console_ns.models[EducationActivatePayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[EducationActivateResponse.__name__])
|
||||
# response-contract:ignore billing-service activation payload; TODO: model education activation result.
|
||||
@console_ns.response(HTTPStatus.OK, "Success")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -577,21 +563,22 @@ class EducationApi(Resource):
|
||||
payload = console_ns.payload or {}
|
||||
args = EducationActivatePayload.model_validate(payload)
|
||||
|
||||
return BillingService.EducationIdentity.activate(account, args.token, args.institution, args.role)
|
||||
result = BillingService.EducationIdentity.activate(account, args.token, args.institution, args.role)
|
||||
return result
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@only_edition_cloud
|
||||
@cloud_edition_billing_enabled
|
||||
@console_ns.response(200, "Success", console_ns.models[EducationStatusResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[EducationStatusResponse.__name__])
|
||||
@with_current_user
|
||||
def get(self, account: Account):
|
||||
res = BillingService.EducationIdentity.status(account.id) or {}
|
||||
# convert expire_at to UTC timestamp from isoformat
|
||||
if res and "expire_at" in res:
|
||||
res["expire_at"] = datetime.fromisoformat(res["expire_at"]).astimezone(pytz.utc)
|
||||
return EducationStatusResponse.model_validate(res).model_dump(mode="json")
|
||||
return dump_response(EducationStatusResponse, res)
|
||||
|
||||
|
||||
@console_ns.route("/account/education/autocomplete")
|
||||
@@ -602,20 +589,21 @@ class EducationAutoCompleteApi(Resource):
|
||||
@account_initialization_required
|
||||
@only_edition_cloud
|
||||
@cloud_edition_billing_enabled
|
||||
@console_ns.response(200, "Success", console_ns.models[EducationAutocompleteResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[EducationAutocompleteResponse.__name__])
|
||||
def get(self):
|
||||
payload = request.args.to_dict(flat=True)
|
||||
args = EducationAutocompleteQuery.model_validate(payload)
|
||||
|
||||
return EducationAutocompleteResponse.model_validate(
|
||||
BillingService.EducationIdentity.autocomplete(args.keywords, args.page, args.limit) or {}
|
||||
).model_dump(mode="json")
|
||||
return dump_response(
|
||||
EducationAutocompleteResponse,
|
||||
BillingService.EducationIdentity.autocomplete(args.keywords, args.page, args.limit) or {},
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/account/change-email")
|
||||
class ChangeEmailSendEmailApi(Resource):
|
||||
@console_ns.expect(console_ns.models[ChangeEmailSendPayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultDataResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultDataResponse.__name__])
|
||||
@enable_change_email
|
||||
@setup_required
|
||||
@login_required
|
||||
@@ -669,13 +657,13 @@ class ChangeEmailSendEmailApi(Resource):
|
||||
language=language,
|
||||
phase=send_phase,
|
||||
)
|
||||
return {"result": "success", "data": token}
|
||||
return SimpleResultDataResponse(result="success", data=token).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/account/change-email/validity")
|
||||
class ChangeEmailCheckApi(Resource):
|
||||
@console_ns.expect(console_ns.models[ChangeEmailValidityPayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[VerificationTokenResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[VerificationTokenResponse.__name__])
|
||||
@enable_change_email
|
||||
@setup_required
|
||||
@login_required
|
||||
@@ -716,7 +704,9 @@ class ChangeEmailCheckApi(Resource):
|
||||
new_token = AccountService.generate_change_email_token(refreshed_token_data, current_user)
|
||||
|
||||
AccountService.reset_change_email_error_rate_limit(user_email)
|
||||
return {"is_valid": True, "email": normalized_token_email, "token": new_token}
|
||||
return VerificationTokenResponse(is_valid=True, email=normalized_token_email, token=new_token).model_dump(
|
||||
mode="json"
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/account/change-email/reset")
|
||||
@@ -726,7 +716,7 @@ class ChangeEmailResetApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
|
||||
@with_current_user
|
||||
def post(self, current_user: Account):
|
||||
payload = console_ns.payload or {}
|
||||
@@ -768,13 +758,13 @@ class ChangeEmailResetApi(Resource):
|
||||
email=normalized_new_email,
|
||||
)
|
||||
|
||||
return _serialize_account(updated_account)
|
||||
return dump_response(AccountResponse, updated_account)
|
||||
|
||||
|
||||
@console_ns.route("/account/change-email/check-email-unique")
|
||||
class CheckEmailUnique(Resource):
|
||||
@console_ns.expect(console_ns.models[CheckEmailUniquePayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@setup_required
|
||||
def post(self):
|
||||
payload = console_ns.payload or {}
|
||||
@@ -784,4 +774,4 @@ class CheckEmailUnique(Resource):
|
||||
raise AccountInFreezeError()
|
||||
if not AccountService.check_email_unique(normalized_email, session=db.session):
|
||||
raise EmailAlreadyInUseError()
|
||||
return {"result": "success"}
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
|
||||
@@ -6,13 +6,17 @@ verb-based aliases stay available as deprecated resources so OpenAPI metadata
|
||||
marks only the legacy paths as deprecated.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from controllers.common.schema import query_params_from_model, register_schema_models
|
||||
from controllers.common.fields import SuccessResponse
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
@@ -24,8 +28,14 @@ from controllers.console.wraps import (
|
||||
with_current_tenant_id,
|
||||
with_current_user_id,
|
||||
)
|
||||
from core.entities.parameter_entities import (
|
||||
AppSelectorScope,
|
||||
ModelSelectorScope,
|
||||
ToolSelectorScope,
|
||||
)
|
||||
from core.entities.provider_entities import ProviderConfigType
|
||||
from core.plugin.impl.exc import PluginPermissionDeniedError
|
||||
from graphon.model_runtime.utils.encoders import jsonable_encoder
|
||||
from fields.base import ResponseModel
|
||||
from libs.login import login_required
|
||||
from services.plugin.endpoint_service import EndpointService
|
||||
|
||||
@@ -40,14 +50,17 @@ class EndpointIdPayload(BaseModel):
|
||||
endpoint_id: str
|
||||
|
||||
|
||||
class EndpointUpdatePayload(BaseModel):
|
||||
class EndpointSettingsPayload(BaseModel):
|
||||
settings: dict[str, Any]
|
||||
name: str = Field(min_length=1)
|
||||
|
||||
|
||||
class LegacyEndpointUpdatePayload(EndpointIdPayload):
|
||||
settings: dict[str, Any]
|
||||
name: str = Field(min_length=1)
|
||||
class EndpointUpdatePayload(EndpointSettingsPayload):
|
||||
pass
|
||||
|
||||
|
||||
class LegacyEndpointUpdatePayload(EndpointIdPayload, EndpointSettingsPayload):
|
||||
pass
|
||||
|
||||
|
||||
class EndpointListQuery(BaseModel):
|
||||
@@ -59,98 +72,158 @@ class EndpointListForPluginQuery(EndpointListQuery):
|
||||
plugin_id: str
|
||||
|
||||
|
||||
class EndpointCreateResponse(BaseModel):
|
||||
success: bool = Field(description="Operation success")
|
||||
class EndpointProviderConfigScope(StrEnum):
|
||||
ALL = AppSelectorScope.ALL.value
|
||||
CHAT = AppSelectorScope.CHAT.value
|
||||
WORKFLOW = AppSelectorScope.WORKFLOW.value
|
||||
COMPLETION = AppSelectorScope.COMPLETION.value
|
||||
LLM = ModelSelectorScope.LLM.value
|
||||
TEXT_EMBEDDING = ModelSelectorScope.TEXT_EMBEDDING.value
|
||||
RERANK = ModelSelectorScope.RERANK.value
|
||||
TTS = ModelSelectorScope.TTS.value
|
||||
SPEECH2TEXT = ModelSelectorScope.SPEECH2TEXT.value
|
||||
MODERATION = ModelSelectorScope.MODERATION.value
|
||||
VISION = ModelSelectorScope.VISION.value
|
||||
CUSTOM = ToolSelectorScope.CUSTOM.value
|
||||
BUILTIN = ToolSelectorScope.BUILTIN.value
|
||||
|
||||
|
||||
class EndpointListResponse(BaseModel):
|
||||
endpoints: list[dict[str, Any]] = Field(
|
||||
description="Endpoint information",
|
||||
)
|
||||
class EndpointProviderConfigI18nResponse(ResponseModel):
|
||||
en_US: str
|
||||
zh_Hans: str | None = None
|
||||
pt_BR: str | None = None
|
||||
ja_JP: str | None = None
|
||||
|
||||
|
||||
class PluginEndpointListResponse(BaseModel):
|
||||
endpoints: list[dict[str, Any]] = Field(
|
||||
description="Endpoint information",
|
||||
)
|
||||
class EndpointProviderConfigOptionResponse(ResponseModel):
|
||||
value: str
|
||||
label: EndpointProviderConfigI18nResponse
|
||||
|
||||
|
||||
class EndpointDeleteResponse(BaseModel):
|
||||
success: bool = Field(description="Operation success")
|
||||
class EndpointProviderConfigResponse(ResponseModel):
|
||||
type: ProviderConfigType
|
||||
name: str
|
||||
scope: EndpointProviderConfigScope | None = None
|
||||
required: bool = False
|
||||
default: int | str | float | bool | None = None
|
||||
options: list[EndpointProviderConfigOptionResponse] | None = None
|
||||
multiple: bool = False
|
||||
label: EndpointProviderConfigI18nResponse | None = None
|
||||
help: EndpointProviderConfigI18nResponse | None = None
|
||||
url: str | None = None
|
||||
placeholder: EndpointProviderConfigI18nResponse | None = None
|
||||
|
||||
|
||||
class EndpointUpdateResponse(BaseModel):
|
||||
success: bool = Field(description="Operation success")
|
||||
class EndpointDeclarationResponse(ResponseModel):
|
||||
path: str
|
||||
method: str
|
||||
hidden: bool = False
|
||||
|
||||
|
||||
class EndpointEnableResponse(BaseModel):
|
||||
success: bool = Field(description="Operation success")
|
||||
class EndpointProviderDeclarationResponse(ResponseModel):
|
||||
settings: list[EndpointProviderConfigResponse] = Field(default_factory=list)
|
||||
endpoints: list[EndpointDeclarationResponse] | None = Field(default_factory=list)
|
||||
|
||||
|
||||
class EndpointDisableResponse(BaseModel):
|
||||
success: bool = Field(description="Operation success")
|
||||
class EndpointListItemResponse(ResponseModel):
|
||||
id: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
tenant_id: str
|
||||
plugin_id: str
|
||||
settings: dict[str, Any]
|
||||
expired_at: datetime
|
||||
declaration: EndpointProviderDeclarationResponse = Field(default_factory=EndpointProviderDeclarationResponse)
|
||||
name: str
|
||||
enabled: bool
|
||||
url: str
|
||||
hook_id: str
|
||||
|
||||
|
||||
class EndpointListResponse(ResponseModel):
|
||||
endpoints: list[EndpointListItemResponse] = Field(description="Endpoint information")
|
||||
|
||||
|
||||
register_schema_models(
|
||||
console_ns,
|
||||
EndpointCreatePayload,
|
||||
EndpointIdPayload,
|
||||
EndpointSettingsPayload,
|
||||
EndpointUpdatePayload,
|
||||
LegacyEndpointUpdatePayload,
|
||||
EndpointListQuery,
|
||||
EndpointListForPluginQuery,
|
||||
EndpointCreateResponse,
|
||||
)
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
SuccessResponse,
|
||||
EndpointProviderConfigOptionResponse,
|
||||
EndpointProviderConfigResponse,
|
||||
EndpointDeclarationResponse,
|
||||
EndpointProviderDeclarationResponse,
|
||||
EndpointListItemResponse,
|
||||
EndpointListResponse,
|
||||
PluginEndpointListResponse,
|
||||
EndpointDeleteResponse,
|
||||
EndpointUpdateResponse,
|
||||
EndpointEnableResponse,
|
||||
EndpointDisableResponse,
|
||||
)
|
||||
|
||||
|
||||
def _create_endpoint(tenant_id: str, user_id: str) -> dict[str, bool]:
|
||||
def _create_endpoint(tenant_id: str, user_id: str) -> bool:
|
||||
"""Create a plugin endpoint for the injected workspace and user."""
|
||||
args = EndpointCreatePayload.model_validate(console_ns.payload)
|
||||
|
||||
try:
|
||||
return {
|
||||
"success": EndpointService.create_endpoint(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
plugin_unique_identifier=args.plugin_unique_identifier,
|
||||
name=args.name,
|
||||
settings=args.settings,
|
||||
)
|
||||
}
|
||||
return EndpointService.create_endpoint(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
plugin_unique_identifier=args.plugin_unique_identifier,
|
||||
name=args.name,
|
||||
settings=args.settings,
|
||||
)
|
||||
except PluginPermissionDeniedError as e:
|
||||
raise ValueError(e.description) from e
|
||||
|
||||
|
||||
def _update_endpoint(tenant_id: str, user_id: str, endpoint_id: str) -> dict[str, bool]:
|
||||
def _update_endpoint(tenant_id: str, user_id: str, endpoint_id: str) -> bool:
|
||||
"""Update a plugin endpoint identified by the canonical path parameter."""
|
||||
args = EndpointUpdatePayload.model_validate(console_ns.payload)
|
||||
|
||||
return {
|
||||
"success": EndpointService.update_endpoint(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
endpoint_id=endpoint_id,
|
||||
name=args.name,
|
||||
settings=args.settings,
|
||||
)
|
||||
}
|
||||
return EndpointService.update_endpoint(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
endpoint_id=endpoint_id,
|
||||
name=args.name,
|
||||
settings=args.settings,
|
||||
)
|
||||
|
||||
|
||||
def _delete_endpoint(tenant_id: str, user_id: str, endpoint_id: str) -> dict[str, bool]:
|
||||
def _legacy_update_endpoint(tenant_id: str, user_id: str) -> bool:
|
||||
args = LegacyEndpointUpdatePayload.model_validate(console_ns.payload)
|
||||
return EndpointService.update_endpoint(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
endpoint_id=args.endpoint_id,
|
||||
name=args.name,
|
||||
settings=args.settings,
|
||||
)
|
||||
|
||||
|
||||
def _delete_endpoint(tenant_id: str, user_id: str, endpoint_id: str) -> bool:
|
||||
"""Delete a plugin endpoint identified by the canonical path parameter."""
|
||||
return {
|
||||
"success": EndpointService.delete_endpoint(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
endpoint_id=endpoint_id,
|
||||
)
|
||||
}
|
||||
return EndpointService.delete_endpoint(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
endpoint_id=endpoint_id,
|
||||
)
|
||||
|
||||
|
||||
def _delete_endpoint_from_payload(tenant_id: str, user_id: str) -> bool:
|
||||
args = EndpointIdPayload.model_validate(console_ns.payload)
|
||||
return _delete_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id)
|
||||
|
||||
|
||||
def _set_endpoint_enabled(tenant_id: str, user_id: str, *, enabled: bool) -> bool:
|
||||
args = EndpointIdPayload.model_validate(console_ns.payload)
|
||||
action = EndpointService.enable_endpoint if enabled else EndpointService.disable_endpoint
|
||||
return action(tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/endpoints")
|
||||
@@ -161,11 +234,11 @@ class EndpointCollectionApi(Resource):
|
||||
@console_ns.doc(description="Create a new plugin endpoint")
|
||||
@console_ns.expect(console_ns.models[EndpointCreatePayload.__name__])
|
||||
@console_ns.response(
|
||||
200,
|
||||
HTTPStatus.OK,
|
||||
"Endpoint created successfully",
|
||||
console_ns.models[EndpointCreateResponse.__name__],
|
||||
console_ns.models[SuccessResponse.__name__],
|
||||
)
|
||||
@console_ns.response(403, "Admin privileges required")
|
||||
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@@ -174,7 +247,7 @@ class EndpointCollectionApi(Resource):
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, user_id: str):
|
||||
return _create_endpoint(tenant_id=tenant_id, user_id=user_id)
|
||||
return SuccessResponse(success=_create_endpoint(tenant_id=tenant_id, user_id=user_id)).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/endpoints/create")
|
||||
@@ -190,11 +263,11 @@ class DeprecatedEndpointCreateApi(Resource):
|
||||
)
|
||||
@console_ns.expect(console_ns.models[EndpointCreatePayload.__name__])
|
||||
@console_ns.response(
|
||||
200,
|
||||
HTTPStatus.OK,
|
||||
"Endpoint created successfully",
|
||||
console_ns.models[EndpointCreateResponse.__name__],
|
||||
console_ns.models[SuccessResponse.__name__],
|
||||
)
|
||||
@console_ns.response(403, "Admin privileges required")
|
||||
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@@ -203,7 +276,7 @@ class DeprecatedEndpointCreateApi(Resource):
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, user_id: str):
|
||||
return _create_endpoint(tenant_id=tenant_id, user_id=user_id)
|
||||
return SuccessResponse(success=_create_endpoint(tenant_id=tenant_id, user_id=user_id)).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/endpoints/list")
|
||||
@@ -212,7 +285,7 @@ class EndpointListApi(Resource):
|
||||
@console_ns.doc(description="List plugin endpoints with pagination")
|
||||
@console_ns.doc(params=query_params_from_model(EndpointListQuery))
|
||||
@console_ns.response(
|
||||
200,
|
||||
HTTPStatus.OK,
|
||||
"Success",
|
||||
console_ns.models[EndpointListResponse.__name__],
|
||||
)
|
||||
@@ -224,20 +297,15 @@ class EndpointListApi(Resource):
|
||||
def get(self, tenant_id: str, user_id: str):
|
||||
args = EndpointListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
page = args.page
|
||||
page_size = args.page_size
|
||||
|
||||
return jsonable_encoder(
|
||||
{
|
||||
"endpoints": EndpointService.list_endpoints(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
}
|
||||
endpoints = EndpointService.list_endpoints(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
page=args.page,
|
||||
page_size=args.page_size,
|
||||
)
|
||||
|
||||
return EndpointListResponse(endpoints=endpoints).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/endpoints/list/plugin")
|
||||
class EndpointListForSinglePluginApi(Resource):
|
||||
@@ -245,9 +313,9 @@ class EndpointListForSinglePluginApi(Resource):
|
||||
@console_ns.doc(description="List endpoints for a specific plugin")
|
||||
@console_ns.doc(params=query_params_from_model(EndpointListForPluginQuery))
|
||||
@console_ns.response(
|
||||
200,
|
||||
HTTPStatus.OK,
|
||||
"Success",
|
||||
console_ns.models[PluginEndpointListResponse.__name__],
|
||||
console_ns.models[EndpointListResponse.__name__],
|
||||
)
|
||||
@setup_required
|
||||
@login_required
|
||||
@@ -257,22 +325,16 @@ class EndpointListForSinglePluginApi(Resource):
|
||||
def get(self, tenant_id: str, user_id: str):
|
||||
args = EndpointListForPluginQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
page = args.page
|
||||
page_size = args.page_size
|
||||
plugin_id = args.plugin_id
|
||||
|
||||
return jsonable_encoder(
|
||||
{
|
||||
"endpoints": EndpointService.list_endpoints_for_single_plugin(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
plugin_id=plugin_id,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
}
|
||||
endpoints = EndpointService.list_endpoints_for_single_plugin(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
plugin_id=args.plugin_id,
|
||||
page=args.page,
|
||||
page_size=args.page_size,
|
||||
)
|
||||
|
||||
return EndpointListResponse(endpoints=endpoints).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/endpoints/<string:id>")
|
||||
class EndpointItemApi(Resource):
|
||||
@@ -282,11 +344,11 @@ class EndpointItemApi(Resource):
|
||||
@console_ns.doc(description="Delete a plugin endpoint")
|
||||
@console_ns.doc(params={"id": {"description": "Endpoint ID", "type": "string", "required": True}})
|
||||
@console_ns.response(
|
||||
200,
|
||||
HTTPStatus.OK,
|
||||
"Endpoint deleted successfully",
|
||||
console_ns.models[EndpointDeleteResponse.__name__],
|
||||
console_ns.models[SuccessResponse.__name__],
|
||||
)
|
||||
@console_ns.response(403, "Admin privileges required")
|
||||
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@@ -295,18 +357,20 @@ class EndpointItemApi(Resource):
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def delete(self, tenant_id: str, user_id: str, id: str):
|
||||
return _delete_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=id)
|
||||
return SuccessResponse(
|
||||
success=_delete_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=id)
|
||||
).model_dump(mode="json")
|
||||
|
||||
@console_ns.doc("update_endpoint")
|
||||
@console_ns.doc(description="Update a plugin endpoint")
|
||||
@console_ns.expect(console_ns.models[EndpointUpdatePayload.__name__])
|
||||
@console_ns.doc(params={"id": {"description": "Endpoint ID", "type": "string", "required": True}})
|
||||
@console_ns.response(
|
||||
200,
|
||||
HTTPStatus.OK,
|
||||
"Endpoint updated successfully",
|
||||
console_ns.models[EndpointUpdateResponse.__name__],
|
||||
console_ns.models[SuccessResponse.__name__],
|
||||
)
|
||||
@console_ns.response(403, "Admin privileges required")
|
||||
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@@ -315,7 +379,9 @@ class EndpointItemApi(Resource):
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def patch(self, tenant_id: str, user_id: str, id: str):
|
||||
return _update_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=id)
|
||||
return SuccessResponse(
|
||||
success=_update_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=id)
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/endpoints/delete")
|
||||
@@ -332,11 +398,11 @@ class DeprecatedEndpointDeleteApi(Resource):
|
||||
)
|
||||
@console_ns.expect(console_ns.models[EndpointIdPayload.__name__])
|
||||
@console_ns.response(
|
||||
200,
|
||||
HTTPStatus.OK,
|
||||
"Endpoint deleted successfully",
|
||||
console_ns.models[EndpointDeleteResponse.__name__],
|
||||
console_ns.models[SuccessResponse.__name__],
|
||||
)
|
||||
@console_ns.response(403, "Admin privileges required")
|
||||
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@@ -345,8 +411,9 @@ class DeprecatedEndpointDeleteApi(Resource):
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, user_id: str):
|
||||
args = EndpointIdPayload.model_validate(console_ns.payload)
|
||||
return _delete_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id)
|
||||
return SuccessResponse(success=_delete_endpoint_from_payload(tenant_id=tenant_id, user_id=user_id)).model_dump(
|
||||
mode="json"
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/endpoints/update")
|
||||
@@ -363,11 +430,11 @@ class DeprecatedEndpointUpdateApi(Resource):
|
||||
)
|
||||
@console_ns.expect(console_ns.models[LegacyEndpointUpdatePayload.__name__])
|
||||
@console_ns.response(
|
||||
200,
|
||||
HTTPStatus.OK,
|
||||
"Endpoint updated successfully",
|
||||
console_ns.models[EndpointUpdateResponse.__name__],
|
||||
console_ns.models[SuccessResponse.__name__],
|
||||
)
|
||||
@console_ns.response(403, "Admin privileges required")
|
||||
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@@ -376,8 +443,9 @@ class DeprecatedEndpointUpdateApi(Resource):
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, user_id: str):
|
||||
args = LegacyEndpointUpdatePayload.model_validate(console_ns.payload)
|
||||
return _update_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id)
|
||||
return SuccessResponse(success=_legacy_update_endpoint(tenant_id=tenant_id, user_id=user_id)).model_dump(
|
||||
mode="json"
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/endpoints/enable")
|
||||
@@ -386,11 +454,11 @@ class EndpointEnableApi(Resource):
|
||||
@console_ns.doc(description="Enable a plugin endpoint")
|
||||
@console_ns.expect(console_ns.models[EndpointIdPayload.__name__])
|
||||
@console_ns.response(
|
||||
200,
|
||||
HTTPStatus.OK,
|
||||
"Endpoint enabled successfully",
|
||||
console_ns.models[EndpointEnableResponse.__name__],
|
||||
console_ns.models[SuccessResponse.__name__],
|
||||
)
|
||||
@console_ns.response(403, "Admin privileges required")
|
||||
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@@ -399,13 +467,9 @@ class EndpointEnableApi(Resource):
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, user_id: str):
|
||||
args = EndpointIdPayload.model_validate(console_ns.payload)
|
||||
|
||||
return {
|
||||
"success": EndpointService.enable_endpoint(
|
||||
tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id
|
||||
)
|
||||
}
|
||||
return SuccessResponse(
|
||||
success=_set_endpoint_enabled(tenant_id=tenant_id, user_id=user_id, enabled=True)
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/endpoints/disable")
|
||||
@@ -414,11 +478,11 @@ class EndpointDisableApi(Resource):
|
||||
@console_ns.doc(description="Disable a plugin endpoint")
|
||||
@console_ns.expect(console_ns.models[EndpointIdPayload.__name__])
|
||||
@console_ns.response(
|
||||
200,
|
||||
HTTPStatus.OK,
|
||||
"Endpoint disabled successfully",
|
||||
console_ns.models[EndpointDisableResponse.__name__],
|
||||
console_ns.models[SuccessResponse.__name__],
|
||||
)
|
||||
@console_ns.response(403, "Admin privileges required")
|
||||
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@@ -427,10 +491,6 @@ class EndpointDisableApi(Resource):
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, user_id: str):
|
||||
args = EndpointIdPayload.model_validate(console_ns.payload)
|
||||
|
||||
return {
|
||||
"success": EndpointService.disable_endpoint(
|
||||
tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id
|
||||
)
|
||||
}
|
||||
return SuccessResponse(
|
||||
success=_set_endpoint_enabled(tenant_id=tenant_id, user_id=user_id, enabled=False)
|
||||
).model_dump(mode="json")
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from http import HTTPStatus
|
||||
from urllib import parse
|
||||
from uuid import UUID
|
||||
|
||||
from flask import abort, request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, TypeAdapter
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy import func, select
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
import services
|
||||
from configs import dify_config
|
||||
@@ -30,8 +32,8 @@ from controllers.console.wraps import (
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from fields.base import ResponseModel
|
||||
from fields.member_fields import AccountWithRole, AccountWithRoleList
|
||||
from libs.helper import extract_remote_ip
|
||||
from fields.member_fields import AccountWithRoleListResponse, AccountWithRoleResponse
|
||||
from libs.helper import dump_response, extract_remote_ip
|
||||
from libs.login import current_account_with_tenant, login_required
|
||||
from models.account import Account, TenantAccountJoin, TenantAccountRole
|
||||
from services.account_service import AccountService, RegisterService, TenantService
|
||||
@@ -45,6 +47,11 @@ class MemberInvitePayload(BaseModel):
|
||||
role: str
|
||||
language: str | None = None
|
||||
|
||||
@field_validator("emails")
|
||||
@classmethod
|
||||
def normalize_emails(cls, emails: list[str]) -> list[str]:
|
||||
return list(dict.fromkeys(email.lower() for email in emails))
|
||||
|
||||
|
||||
class MemberRoleUpdatePayload(BaseModel):
|
||||
role: str
|
||||
@@ -70,14 +77,14 @@ class MemberInviteResultResponse(ResponseModel):
|
||||
message: str | None = None
|
||||
|
||||
|
||||
class MemberInviteResponse(ResponseModel):
|
||||
class MemberActionResponse(ResponseModel):
|
||||
result: str
|
||||
invitation_results: list[MemberInviteResultResponse]
|
||||
tenant_id: str
|
||||
|
||||
|
||||
class MemberActionTenantResponse(ResponseModel):
|
||||
class MemberInviteResponse(ResponseModel):
|
||||
result: str
|
||||
invitation_results: list[MemberInviteResultResponse]
|
||||
tenant_id: str
|
||||
|
||||
|
||||
@@ -92,13 +99,14 @@ register_schema_models(
|
||||
)
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
AccountWithRole,
|
||||
AccountWithRoleList,
|
||||
AccountWithRoleResponse,
|
||||
AccountWithRoleListResponse,
|
||||
MemberActionResponse,
|
||||
MemberInviteResponse,
|
||||
MemberInviteResultResponse,
|
||||
SimpleResultDataResponse,
|
||||
SimpleResultResponse,
|
||||
VerificationTokenResponse,
|
||||
MemberInviteResponse,
|
||||
MemberActionTenantResponse,
|
||||
)
|
||||
|
||||
|
||||
@@ -124,10 +132,6 @@ def _normalize_enum_value(value: object) -> str:
|
||||
return str(normalized) if normalized is not None else ""
|
||||
|
||||
|
||||
def _normalize_invitee_emails(emails: list[str]) -> list[str]:
|
||||
return list(dict.fromkeys(email.lower() for email in emails))
|
||||
|
||||
|
||||
def _count_new_member_invites(tenant_id: str, emails: list[str]) -> int:
|
||||
new_member_count = 0
|
||||
for email in emails:
|
||||
@@ -179,7 +183,7 @@ class MemberListApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[AccountWithRoleList.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountWithRoleListResponse.__name__])
|
||||
@with_current_user
|
||||
def get(self, current_user: Account | None = None):
|
||||
if current_user is None:
|
||||
@@ -216,9 +220,7 @@ class MemberListApi(Resource):
|
||||
}
|
||||
)
|
||||
|
||||
member_models = TypeAdapter(list[AccountWithRole]).validate_python(serialized_members)
|
||||
response = AccountWithRoleList(accounts=member_models)
|
||||
return response.model_dump(mode="json"), 200
|
||||
return dump_response(AccountWithRoleListResponse, {"accounts": serialized_members}), HTTPStatus.OK
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/members/invite-email")
|
||||
@@ -226,7 +228,7 @@ class MemberInviteEmailApi(Resource):
|
||||
"""Invite a new member by email."""
|
||||
|
||||
@console_ns.expect(console_ns.models[MemberInvitePayload.__name__])
|
||||
@console_ns.response(201, "Success", console_ns.models[MemberInviteResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.CREATED, "Success", console_ns.models[MemberInviteResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -235,26 +237,26 @@ class MemberInviteEmailApi(Resource):
|
||||
payload = console_ns.payload or {}
|
||||
args = MemberInvitePayload.model_validate(payload)
|
||||
|
||||
invitee_emails = _normalize_invitee_emails(args.emails)
|
||||
invitee_emails = args.emails
|
||||
invitee_role = args.role
|
||||
interface_language = args.language
|
||||
if not dify_config.RBAC_ENABLED:
|
||||
if not TenantAccountRole.is_valid_role(invitee_role):
|
||||
return {"code": "invalid-role", "message": "Invalid role"}, 400
|
||||
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
|
||||
if not TenantAccountRole.is_non_owner_role(TenantAccountRole(invitee_role)):
|
||||
return {"code": "invalid-role", "message": "Invalid role"}, 400
|
||||
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
|
||||
inviter = current_user
|
||||
if not inviter.current_tenant:
|
||||
raise ValueError("No current tenant")
|
||||
if not _is_role_enabled(invitee_role, inviter.current_tenant.id):
|
||||
return {"code": "invalid-role", "message": "Invalid role"}, 400
|
||||
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
|
||||
|
||||
# Check workspace permission for member invitations
|
||||
from libs.workspace_permission import check_workspace_member_invite_permission
|
||||
|
||||
check_workspace_member_invite_permission(inviter.current_tenant.id)
|
||||
|
||||
invitation_results = []
|
||||
invitation_results: list[MemberInviteResultResponse] = []
|
||||
console_web_url = dify_config.CONSOLE_WEB_URL
|
||||
|
||||
tenant_id = inviter.current_tenant.id
|
||||
@@ -277,63 +279,65 @@ class MemberInviteEmailApi(Resource):
|
||||
)
|
||||
encoded_invitee_email = parse.quote(invitee_email)
|
||||
invitation_results.append(
|
||||
{
|
||||
"status": "success",
|
||||
"email": invitee_email,
|
||||
"url": f"{console_web_url}/activate?email={encoded_invitee_email}&token={token}",
|
||||
}
|
||||
MemberInviteResultResponse(
|
||||
status="success",
|
||||
email=invitee_email,
|
||||
url=f"{console_web_url}/activate?email={encoded_invitee_email}&token={token}",
|
||||
)
|
||||
)
|
||||
except AccountAlreadyInTenantError:
|
||||
invitation_results.append(
|
||||
{
|
||||
"status": "already_member",
|
||||
"email": invitee_email,
|
||||
"message": "Account already in workspace.",
|
||||
}
|
||||
MemberInviteResultResponse(
|
||||
status="already_member",
|
||||
email=invitee_email,
|
||||
message="Account already in workspace.",
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
invitation_results.append({"status": "failed", "email": invitee_email, "message": str(e)})
|
||||
invitation_results.append(
|
||||
MemberInviteResultResponse(status="failed", email=invitee_email, message=str(e))
|
||||
)
|
||||
|
||||
return {
|
||||
"result": "success",
|
||||
"invitation_results": invitation_results,
|
||||
"tenant_id": str(inviter.current_tenant.id) if inviter.current_tenant else "",
|
||||
}, 201
|
||||
return MemberInviteResponse(
|
||||
result="success",
|
||||
invitation_results=invitation_results,
|
||||
tenant_id=inviter.current_tenant.id if inviter.current_tenant else "",
|
||||
).model_dump(mode="json"), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/members/<uuid:member_id>")
|
||||
class MemberCancelInviteApi(Resource):
|
||||
"""Cancel an invitation by member id."""
|
||||
|
||||
@console_ns.response(200, "Success", console_ns.models[MemberActionTenantResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[MemberActionResponse.__name__])
|
||||
@with_current_user
|
||||
def delete(self, current_user: Account, member_id: UUID):
|
||||
if not current_user.current_tenant:
|
||||
raise ValueError("No current tenant")
|
||||
member = db.session.get(Account, str(member_id))
|
||||
if member is None:
|
||||
abort(404)
|
||||
abort(HTTPStatus.NOT_FOUND)
|
||||
else:
|
||||
try:
|
||||
TenantService.remove_member_from_tenant(
|
||||
current_user.current_tenant, member, current_user, session=db.session
|
||||
)
|
||||
except services.errors.account.CannotOperateSelfError as e:
|
||||
return {"code": "cannot-operate-self", "message": str(e)}, 400
|
||||
return {"code": "cannot-operate-self", "message": str(e)}, HTTPStatus.BAD_REQUEST
|
||||
except services.errors.account.NoPermissionError as e:
|
||||
return {"code": "forbidden", "message": str(e)}, 403
|
||||
return {"code": "forbidden", "message": str(e)}, HTTPStatus.FORBIDDEN
|
||||
except services.errors.account.MemberNotInTenantError as e:
|
||||
return {"code": "member-not-found", "message": str(e)}, 404
|
||||
return {"code": "member-not-found", "message": str(e)}, HTTPStatus.NOT_FOUND
|
||||
except Exception as e:
|
||||
raise ValueError(str(e))
|
||||
|
||||
return {
|
||||
"result": "success",
|
||||
"tenant_id": str(current_user.current_tenant.id) if current_user.current_tenant else "",
|
||||
}, 200
|
||||
return MemberActionResponse(
|
||||
result="success",
|
||||
tenant_id=current_user.current_tenant.id if current_user.current_tenant else "",
|
||||
).model_dump(mode="json"), HTTPStatus.OK
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/members/<uuid:member_id>/update-role")
|
||||
@@ -341,7 +345,7 @@ class MemberUpdateRoleApi(Resource):
|
||||
"""Update member role."""
|
||||
|
||||
@console_ns.expect(console_ns.models[MemberRoleUpdatePayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -352,14 +356,14 @@ class MemberUpdateRoleApi(Resource):
|
||||
new_role = args.role
|
||||
|
||||
if not TenantAccountRole.is_valid_role(new_role):
|
||||
return {"code": "invalid-role", "message": "Invalid role"}, 400
|
||||
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
|
||||
if not current_user.current_tenant:
|
||||
raise ValueError("No current tenant")
|
||||
if not _is_role_enabled(new_role, current_user.current_tenant.id):
|
||||
return {"code": "invalid-role", "message": "Invalid role"}, 400
|
||||
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
|
||||
member = db.session.get(Account, str(member_id))
|
||||
if not member:
|
||||
abort(404)
|
||||
abort(HTTPStatus.NOT_FOUND)
|
||||
|
||||
try:
|
||||
assert member is not None, "Member not found"
|
||||
@@ -367,17 +371,17 @@ class MemberUpdateRoleApi(Resource):
|
||||
current_user.current_tenant, member, new_role, current_user, session=db.session
|
||||
)
|
||||
except services.errors.account.CannotOperateSelfError as e:
|
||||
return {"code": "cannot-operate-self", "message": str(e)}, 400
|
||||
return {"code": "cannot-operate-self", "message": str(e)}, HTTPStatus.BAD_REQUEST
|
||||
except services.errors.account.NoPermissionError as e:
|
||||
return {"code": "forbidden", "message": str(e)}, 403
|
||||
return {"code": "forbidden", "message": str(e)}, HTTPStatus.FORBIDDEN
|
||||
except services.errors.account.MemberNotInTenantError as e:
|
||||
return {"code": "member-not-found", "message": str(e)}, 404
|
||||
return {"code": "member-not-found", "message": str(e)}, HTTPStatus.NOT_FOUND
|
||||
except services.errors.account.RoleAlreadyAssignedError as e:
|
||||
return {"code": "role-already-assigned", "message": str(e)}, 400
|
||||
return {"code": "role-already-assigned", "message": str(e)}, HTTPStatus.BAD_REQUEST
|
||||
except Exception as e:
|
||||
raise ValueError(str(e))
|
||||
|
||||
return {"result": "success"}
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/dataset-operators")
|
||||
@@ -387,15 +391,13 @@ class DatasetOperatorMemberListApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[AccountWithRoleList.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountWithRoleListResponse.__name__])
|
||||
@with_current_user
|
||||
def get(self, current_user: Account):
|
||||
if not current_user.current_tenant:
|
||||
raise ValueError("No current tenant")
|
||||
members = TenantService.get_dataset_operator_members(current_user.current_tenant, session=db.session)
|
||||
member_models = TypeAdapter(list[AccountWithRole]).validate_python(members, from_attributes=True)
|
||||
response = AccountWithRoleList(accounts=member_models)
|
||||
return response.model_dump(mode="json"), 200
|
||||
return dump_response(AccountWithRoleListResponse, {"accounts": members}), HTTPStatus.OK
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/members/send-owner-transfer-confirm-email")
|
||||
@@ -403,7 +405,7 @@ class SendOwnerTransferEmailApi(Resource):
|
||||
"""Send owner transfer email."""
|
||||
|
||||
@console_ns.expect(console_ns.models[OwnerTransferEmailPayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultDataResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultDataResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -435,13 +437,13 @@ class SendOwnerTransferEmailApi(Resource):
|
||||
workspace_name=current_user.current_tenant.name if current_user.current_tenant else "",
|
||||
)
|
||||
|
||||
return {"result": "success", "data": token}
|
||||
return SimpleResultDataResponse(result="success", data=token).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/members/owner-transfer-check")
|
||||
class OwnerTransferCheckApi(Resource):
|
||||
@console_ns.expect(console_ns.models[OwnerTransferCheckPayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[VerificationTokenResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[VerificationTokenResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -480,13 +482,13 @@ class OwnerTransferCheckApi(Resource):
|
||||
_, new_token = AccountService.generate_owner_transfer_token(user_email, code=args.code, additional_data={})
|
||||
|
||||
AccountService.reset_owner_transfer_error_rate_limit(user_email)
|
||||
return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
|
||||
return VerificationTokenResponse(is_valid=True, email=user_email, token=new_token).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/members/<uuid:member_id>/owner-transfer")
|
||||
class OwnerTransfer(Resource):
|
||||
@console_ns.expect(console_ns.models[OwnerTransferPayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -516,8 +518,7 @@ class OwnerTransfer(Resource):
|
||||
|
||||
member = db.session.get(Account, str(member_id))
|
||||
if not member:
|
||||
abort(404)
|
||||
return # Never reached, but helps type checker
|
||||
raise NotFound()
|
||||
|
||||
if not current_user.current_tenant:
|
||||
raise ValueError("No current tenant")
|
||||
@@ -546,4 +547,4 @@ class OwnerTransfer(Resource):
|
||||
except Exception as e:
|
||||
raise ValueError(str(e))
|
||||
|
||||
return {"result": "success"}
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
|
||||
@@ -42,7 +42,15 @@ from fields.base import ResponseModel
|
||||
from graphon.model_runtime.utils.encoders import jsonable_encoder
|
||||
from libs.helper import dump_response
|
||||
from libs.login import login_required
|
||||
from models.account import Account, TenantPluginAutoUpgradeStrategy, TenantPluginPermission
|
||||
from models.account import (
|
||||
Account,
|
||||
TenantPluginAutoUpgradeCategory,
|
||||
TenantPluginAutoUpgradeMode,
|
||||
TenantPluginAutoUpgradeStrategy,
|
||||
TenantPluginAutoUpgradeStrategySetting,
|
||||
TenantPluginDebugPermission,
|
||||
TenantPluginInstallPermission,
|
||||
)
|
||||
from models.provider_ids import ToolProviderID
|
||||
from services.entities.model_provider_entities import ProviderEntityResponse
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
@@ -52,9 +60,9 @@ from services.tools.tools_transform_service import ToolTransformService
|
||||
|
||||
|
||||
class AutoUpgradeSettingsResponse(TypedDict):
|
||||
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting
|
||||
strategy_setting: TenantPluginAutoUpgradeStrategySetting
|
||||
upgrade_time_of_day: int
|
||||
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode
|
||||
upgrade_mode: TenantPluginAutoUpgradeMode
|
||||
exclude_plugins: list[str]
|
||||
include_plugins: list[str]
|
||||
|
||||
@@ -127,8 +135,8 @@ class ParserUninstall(BaseModel):
|
||||
|
||||
|
||||
class ParserPermissionChange(BaseModel):
|
||||
install_permission: TenantPluginPermission.InstallPermission = TenantPluginPermission.InstallPermission.EVERYONE
|
||||
debug_permission: TenantPluginPermission.DebugPermission = TenantPluginPermission.DebugPermission.EVERYONE
|
||||
install_permission: TenantPluginInstallPermission = TenantPluginInstallPermission.EVERYONE
|
||||
debug_permission: TenantPluginDebugPermission = TenantPluginDebugPermission.EVERYONE
|
||||
|
||||
|
||||
class ParserDynamicOptions(BaseModel):
|
||||
@@ -150,16 +158,14 @@ class ParserDynamicOptionsWithCredentials(BaseModel):
|
||||
|
||||
|
||||
class PluginPermissionSettingsPayload(BaseModel):
|
||||
install_permission: TenantPluginPermission.InstallPermission = TenantPluginPermission.InstallPermission.EVERYONE
|
||||
debug_permission: TenantPluginPermission.DebugPermission = TenantPluginPermission.DebugPermission.EVERYONE
|
||||
install_permission: TenantPluginInstallPermission = TenantPluginInstallPermission.EVERYONE
|
||||
debug_permission: TenantPluginDebugPermission = TenantPluginDebugPermission.EVERYONE
|
||||
|
||||
|
||||
class PluginAutoUpgradeSettingsPayload(BaseModel):
|
||||
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting = (
|
||||
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY
|
||||
)
|
||||
strategy_setting: TenantPluginAutoUpgradeStrategySetting = TenantPluginAutoUpgradeStrategySetting.FIX_ONLY
|
||||
upgrade_time_of_day: int = 0
|
||||
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode = TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
|
||||
upgrade_mode: TenantPluginAutoUpgradeMode = TenantPluginAutoUpgradeMode.EXCLUDE
|
||||
exclude_plugins: list[str] = Field(default_factory=list)
|
||||
include_plugins: list[str] = Field(default_factory=list)
|
||||
|
||||
@@ -170,15 +176,15 @@ class PluginAutoUpgradeChangeResponse(ResponseModel):
|
||||
|
||||
|
||||
class PluginAutoUpgradeSettingsResponseModel(ResponseModel):
|
||||
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting
|
||||
strategy_setting: TenantPluginAutoUpgradeStrategySetting
|
||||
upgrade_time_of_day: int
|
||||
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode
|
||||
upgrade_mode: TenantPluginAutoUpgradeMode
|
||||
exclude_plugins: list[str]
|
||||
include_plugins: list[str]
|
||||
|
||||
|
||||
class PluginAutoUpgradeFetchResponse(ResponseModel):
|
||||
category: TenantPluginAutoUpgradeStrategy.PluginCategory
|
||||
category: TenantPluginAutoUpgradeCategory
|
||||
auto_upgrade: PluginAutoUpgradeSettingsResponseModel
|
||||
|
||||
|
||||
@@ -209,19 +215,19 @@ class PluginDeclarationResponse(ResponseModel):
|
||||
class ParserAutoUpgradeChange(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
category: TenantPluginAutoUpgradeStrategy.PluginCategory
|
||||
category: TenantPluginAutoUpgradeCategory
|
||||
auto_upgrade: PluginAutoUpgradeSettingsPayload
|
||||
|
||||
|
||||
class ParserAutoUpgradeFetch(BaseModel):
|
||||
category: TenantPluginAutoUpgradeStrategy.PluginCategory
|
||||
category: TenantPluginAutoUpgradeCategory
|
||||
|
||||
|
||||
class ParserExcludePlugin(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
plugin_id: str
|
||||
category: TenantPluginAutoUpgradeStrategy.PluginCategory
|
||||
category: TenantPluginAutoUpgradeCategory
|
||||
|
||||
|
||||
class ParserReadme(BaseModel):
|
||||
@@ -339,8 +345,8 @@ class PluginTaskResponse(ResponseModel):
|
||||
|
||||
|
||||
class PluginPermissionResponse(ResponseModel):
|
||||
install_permission: TenantPluginPermission.InstallPermission
|
||||
debug_permission: TenantPluginPermission.DebugPermission
|
||||
install_permission: TenantPluginInstallPermission
|
||||
debug_permission: TenantPluginDebugPermission
|
||||
|
||||
|
||||
class PluginDynamicOptionsResponse(ResponseModel):
|
||||
@@ -408,22 +414,22 @@ register_response_schema_models(
|
||||
|
||||
register_enum_models(
|
||||
console_ns,
|
||||
TenantPluginPermission.DebugPermission,
|
||||
TenantPluginAutoUpgradeStrategy.PluginCategory,
|
||||
TenantPluginAutoUpgradeStrategy.UpgradeMode,
|
||||
TenantPluginAutoUpgradeStrategy.StrategySetting,
|
||||
TenantPluginPermission.InstallPermission,
|
||||
TenantPluginDebugPermission,
|
||||
TenantPluginAutoUpgradeCategory,
|
||||
TenantPluginAutoUpgradeMode,
|
||||
TenantPluginAutoUpgradeStrategySetting,
|
||||
TenantPluginInstallPermission,
|
||||
)
|
||||
|
||||
|
||||
def _default_auto_upgrade_settings(
|
||||
tenant_id: str,
|
||||
category: TenantPluginAutoUpgradeStrategy.PluginCategory,
|
||||
category: TenantPluginAutoUpgradeCategory,
|
||||
) -> AutoUpgradeSettingsResponse:
|
||||
return {
|
||||
"strategy_setting": PluginAutoUpgradeService.default_strategy_setting_for_category(category),
|
||||
"upgrade_time_of_day": PluginAutoUpgradeService.default_upgrade_time_of_day(tenant_id),
|
||||
"upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
"upgrade_mode": TenantPluginAutoUpgradeMode.EXCLUDE,
|
||||
"exclude_plugins": [],
|
||||
"include_plugins": [],
|
||||
}
|
||||
@@ -987,8 +993,8 @@ class PluginFetchPermissionApi(Resource):
|
||||
if not permission:
|
||||
return jsonable_encoder(
|
||||
{
|
||||
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
|
||||
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
|
||||
"install_permission": TenantPluginInstallPermission.EVERYONE,
|
||||
"debug_permission": TenantPluginDebugPermission.EVERYONE,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
from flask import Response, request
|
||||
from flask_restx import Resource, marshal
|
||||
from flask_restx import Resource
|
||||
from pydantic import Field as PydanticField
|
||||
from pydantic import field_validator
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
@@ -37,8 +37,7 @@ from controllers.console.wraps import (
|
||||
from core.plugin.entities.plugin import PluginDependency
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from fields.snippet_fields import snippet_fields, snippet_list_fields
|
||||
from libs.helper import to_timestamp
|
||||
from libs.helper import dump_response, to_timestamp
|
||||
from libs.login import login_required
|
||||
from models import Account
|
||||
from models.snippet import SnippetType
|
||||
@@ -198,13 +197,16 @@ class CustomizedSnippetsApi(Resource):
|
||||
tag_ids=query.tag_ids,
|
||||
)
|
||||
|
||||
return {
|
||||
"data": marshal(snippets, snippet_list_fields),
|
||||
"page": query.page,
|
||||
"limit": query.limit,
|
||||
"total": total,
|
||||
"has_more": has_more,
|
||||
}, 200
|
||||
return dump_response(
|
||||
SnippetPaginationResponse,
|
||||
{
|
||||
"data": snippets,
|
||||
"page": query.page,
|
||||
"limit": query.limit,
|
||||
"total": total,
|
||||
"has_more": has_more,
|
||||
},
|
||||
), 200
|
||||
|
||||
@console_ns.doc("create_customized_snippet")
|
||||
@console_ns.expect(console_ns.models.get(CreateSnippetPayload.__name__))
|
||||
@@ -245,7 +247,7 @@ class CustomizedSnippetsApi(Resource):
|
||||
except ValueError as e:
|
||||
return {"message": str(e)}, 400
|
||||
|
||||
return marshal(snippet, snippet_fields), 201
|
||||
return dump_response(SnippetResponse, snippet), 201
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/customized-snippets/<uuid:snippet_id>")
|
||||
@@ -268,7 +270,7 @@ class CustomizedSnippetDetailApi(Resource):
|
||||
if not snippet:
|
||||
raise NotFound("Snippet not found")
|
||||
|
||||
return marshal(snippet, snippet_fields), 200
|
||||
return dump_response(SnippetResponse, snippet), 200
|
||||
|
||||
@console_ns.doc("update_customized_snippet")
|
||||
@console_ns.expect(console_ns.models.get(UpdateSnippetPayload.__name__))
|
||||
@@ -317,7 +319,7 @@ class CustomizedSnippetDetailApi(Resource):
|
||||
except ValueError as e:
|
||||
return {"message": str(e)}, 400
|
||||
|
||||
return marshal(snippet, snippet_fields), 200
|
||||
return dump_response(SnippetResponse, snippet), 200
|
||||
|
||||
@console_ns.doc("delete_customized_snippet")
|
||||
@console_ns.response(204, "Snippet deleted successfully")
|
||||
@@ -533,4 +535,4 @@ class CustomizedSnippetUseCountIncrementApi(Resource):
|
||||
session.commit()
|
||||
session.refresh(snippet)
|
||||
|
||||
return {"result": "success", "use_count": snippet.use_count}, 200
|
||||
return SnippetUseCountResponse(result="success", use_count=snippet.use_count).model_dump(mode="json"), 200
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from http import HTTPStatus
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource, fields, marshal
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy import select
|
||||
from werkzeug.exceptions import Unauthorized
|
||||
@@ -16,7 +17,12 @@ from controllers.common.errors import (
|
||||
TooManyFilesError,
|
||||
UnsupportedFileTypeError,
|
||||
)
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.common.schema import (
|
||||
query_params_from_model,
|
||||
query_params_from_request,
|
||||
register_response_schema_models,
|
||||
register_schema_models,
|
||||
)
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.admin import admin_required
|
||||
from controllers.console.error import AccountNotLinkTenantError
|
||||
@@ -31,7 +37,7 @@ from controllers.console.wraps import (
|
||||
from enums.cloud_plan import CloudPlan
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from libs.helper import OptionalTimestampField, TimestampField, dump_response, to_timestamp
|
||||
from libs.helper import dump_response, to_timestamp
|
||||
from libs.login import login_required
|
||||
from libs.pagination import paginate_query
|
||||
from models.account import Account, Tenant, TenantAccountJoin, TenantCustomConfigDict, TenantStatus
|
||||
@@ -133,7 +139,7 @@ class WorkspaceListItemResponse(ResponseModel):
|
||||
|
||||
@field_validator("status", mode="before")
|
||||
@classmethod
|
||||
def _normalize_status(cls, value):
|
||||
def _normalize_enum_like(cls, value):
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
@@ -146,7 +152,7 @@ class WorkspaceListItemResponse(ResponseModel):
|
||||
return to_timestamp(value)
|
||||
|
||||
|
||||
class WorkspaceListResponse(ResponseModel):
|
||||
class WorkspacePaginationResponse(ResponseModel):
|
||||
data: list[WorkspaceListItemResponse]
|
||||
has_more: bool
|
||||
limit: int
|
||||
@@ -159,7 +165,7 @@ class SwitchWorkspaceResponse(ResponseModel):
|
||||
new_tenant: TenantInfoResponse
|
||||
|
||||
|
||||
class WorkspaceMutationResponse(ResponseModel):
|
||||
class WorkspaceTenantResultResponse(ResponseModel):
|
||||
result: str
|
||||
tenant: TenantInfoResponse
|
||||
|
||||
@@ -174,6 +180,16 @@ class WorkspacePermissionResponse(ResponseModel):
|
||||
allow_owner_transfer: bool
|
||||
|
||||
|
||||
WORKSPACE_LOGO_UPLOAD_PARAMS = {
|
||||
"file": {
|
||||
"in": "formData",
|
||||
"type": "file",
|
||||
"required": True,
|
||||
"description": "Workspace web app logo file. Only SVG and PNG files are supported.",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
register_schema_models(
|
||||
console_ns,
|
||||
WorkspaceListQuery,
|
||||
@@ -184,53 +200,21 @@ register_schema_models(
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
TenantInfoResponse,
|
||||
TenantListItemResponse,
|
||||
TenantListResponse,
|
||||
WorkspaceListResponse,
|
||||
SwitchWorkspaceResponse,
|
||||
WorkspaceMutationResponse,
|
||||
WorkspaceLogoUploadResponse,
|
||||
WorkspaceCustomConfigResponse,
|
||||
WorkspaceListItemResponse,
|
||||
WorkspacePaginationResponse,
|
||||
SwitchWorkspaceResponse,
|
||||
WorkspaceTenantResultResponse,
|
||||
WorkspaceLogoUploadResponse,
|
||||
WorkspacePermissionResponse,
|
||||
)
|
||||
|
||||
provider_fields = {
|
||||
"provider_name": fields.String,
|
||||
"provider_type": fields.String,
|
||||
"is_valid": fields.Boolean,
|
||||
"token_is_set": fields.Boolean,
|
||||
}
|
||||
|
||||
tenant_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"plan": fields.String,
|
||||
"status": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"role": fields.String,
|
||||
"in_trial": fields.Boolean,
|
||||
"trial_end_reason": fields.String,
|
||||
"custom_config": fields.Raw(attribute="custom_config"),
|
||||
"trial_credits": fields.Integer,
|
||||
"trial_credits_used": fields.Integer,
|
||||
"next_credit_reset_date": fields.Integer,
|
||||
}
|
||||
|
||||
tenants_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"plan": fields.String,
|
||||
"status": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"last_opened_at": OptionalTimestampField,
|
||||
"current": fields.Boolean,
|
||||
}
|
||||
|
||||
workspace_fields = {"id": fields.String, "name": fields.String, "status": fields.String, "created_at": TimestampField}
|
||||
|
||||
|
||||
@console_ns.route("/workspaces")
|
||||
class TenantListApi(Resource):
|
||||
@console_ns.response(200, "Success", console_ns.models[TenantListResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[TenantListResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -281,18 +265,17 @@ class TenantListApi(Resource):
|
||||
|
||||
tenant_dicts.append(tenant_dict)
|
||||
|
||||
return {"workspaces": marshal(tenant_dicts, tenants_fields)}, 200
|
||||
return dump_response(TenantListResponse, {"workspaces": tenant_dicts}), HTTPStatus.OK
|
||||
|
||||
|
||||
@console_ns.route("/all-workspaces")
|
||||
class WorkspaceListApi(Resource):
|
||||
@console_ns.doc(params=query_params_from_model(WorkspaceListQuery))
|
||||
@console_ns.response(200, "Success", console_ns.models[WorkspaceListResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[WorkspacePaginationResponse.__name__])
|
||||
@setup_required
|
||||
@admin_required
|
||||
def get(self):
|
||||
payload = request.args.to_dict(flat=True)
|
||||
args = WorkspaceListQuery.model_validate(payload)
|
||||
args = query_params_from_request(WorkspaceListQuery)
|
||||
|
||||
stmt = select(Tenant).order_by(Tenant.created_at.desc())
|
||||
tenants = paginate_query(stmt, page=args.page, per_page=args.limit)
|
||||
@@ -301,13 +284,9 @@ class WorkspaceListApi(Resource):
|
||||
if tenants.has_next:
|
||||
has_more = True
|
||||
|
||||
return {
|
||||
"data": marshal(tenants.items, workspace_fields),
|
||||
"has_more": has_more,
|
||||
"limit": args.limit,
|
||||
"page": args.page,
|
||||
"total": tenants.total,
|
||||
}, 200
|
||||
return WorkspacePaginationResponse(
|
||||
data=tenants.items, has_more=has_more, limit=args.limit, page=args.page, total=tenants.total or 0
|
||||
).model_dump(mode="json"), HTTPStatus.OK
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current", endpoint="workspaces_current")
|
||||
@@ -316,7 +295,7 @@ class TenantApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[TenantInfoResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[TenantInfoResponse.__name__])
|
||||
@with_current_user
|
||||
def post(self, current_user: Account):
|
||||
if request.path == "/info":
|
||||
@@ -336,13 +315,13 @@ class TenantApi(Resource):
|
||||
else:
|
||||
raise Unauthorized("workspace is archived")
|
||||
|
||||
return dump_response(TenantInfoResponse, WorkspaceService.get_tenant_info(tenant)), 200
|
||||
return dump_response(TenantInfoResponse, WorkspaceService.get_tenant_info(tenant)), HTTPStatus.OK
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/switch")
|
||||
class SwitchWorkspaceApi(Resource):
|
||||
@console_ns.expect(console_ns.models[SwitchWorkspacePayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[SwitchWorkspaceResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SwitchWorkspaceResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -351,7 +330,7 @@ class SwitchWorkspaceApi(Resource):
|
||||
payload = console_ns.payload or {}
|
||||
args = SwitchWorkspacePayload.model_validate(payload)
|
||||
|
||||
# check if tenant_id is valid, 403 if not
|
||||
# Check whether the tenant_id belongs to the current account.
|
||||
try:
|
||||
TenantService.switch_tenant(current_user, args.tenant_id, session=db.session)
|
||||
except Exception:
|
||||
@@ -361,13 +340,15 @@ class SwitchWorkspaceApi(Resource):
|
||||
if new_tenant is None:
|
||||
raise ValueError("Tenant not found")
|
||||
|
||||
return {"result": "success", "new_tenant": marshal(WorkspaceService.get_tenant_info(new_tenant), tenant_fields)}
|
||||
return SwitchWorkspaceResponse(
|
||||
result="success", new_tenant=WorkspaceService.get_tenant_info(new_tenant)
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/custom-config")
|
||||
class CustomConfigWorkspaceApi(Resource):
|
||||
@console_ns.expect(console_ns.models[WorkspaceCustomConfigPayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[WorkspaceMutationResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[WorkspaceTenantResultResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -390,12 +371,15 @@ class CustomConfigWorkspaceApi(Resource):
|
||||
tenant.custom_config_dict = custom_config_dict
|
||||
db.session.commit()
|
||||
|
||||
return {"result": "success", "tenant": marshal(WorkspaceService.get_tenant_info(tenant), tenant_fields)}
|
||||
return WorkspaceTenantResultResponse(
|
||||
result="success", tenant=WorkspaceService.get_tenant_info(tenant)
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/custom-config/webapp-logo/upload")
|
||||
class WebappLogoWorkspaceApi(Resource):
|
||||
@console_ns.response(201, "Logo uploaded", console_ns.models[WorkspaceLogoUploadResponse.__name__])
|
||||
@console_ns.doc(consumes=["multipart/form-data"], params=WORKSPACE_LOGO_UPLOAD_PARAMS)
|
||||
@console_ns.response(HTTPStatus.CREATED, "Logo uploaded", console_ns.models[WorkspaceLogoUploadResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -431,13 +415,13 @@ class WebappLogoWorkspaceApi(Resource):
|
||||
except services.errors.file.UnsupportedFileTypeError:
|
||||
raise UnsupportedFileTypeError()
|
||||
|
||||
return {"id": upload_file.id}, 201
|
||||
return WorkspaceLogoUploadResponse(id=upload_file.id).model_dump(mode="json"), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/info")
|
||||
class WorkspaceInfoApi(Resource):
|
||||
@console_ns.expect(console_ns.models[WorkspaceInfoPayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[WorkspaceMutationResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[WorkspaceTenantResultResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -453,14 +437,16 @@ class WorkspaceInfoApi(Resource):
|
||||
tenant.name = args.name
|
||||
db.session.commit()
|
||||
|
||||
return {"result": "success", "tenant": marshal(WorkspaceService.get_tenant_info(tenant), tenant_fields)}
|
||||
return WorkspaceTenantResultResponse(
|
||||
result="success", tenant=WorkspaceService.get_tenant_info(tenant)
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/permission")
|
||||
class WorkspacePermissionApi(Resource):
|
||||
"""Get workspace permissions for the current workspace."""
|
||||
|
||||
@console_ns.response(200, "Success", console_ns.models[WorkspacePermissionResponse.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[WorkspacePermissionResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -477,8 +463,8 @@ class WorkspacePermissionApi(Resource):
|
||||
# Get workspace permissions from enterprise service
|
||||
permission = EnterpriseService.WorkspacePermissionService.get_permission(current_tenant_id)
|
||||
|
||||
return {
|
||||
"workspace_id": permission.workspace_id,
|
||||
"allow_member_invite": permission.allow_member_invite,
|
||||
"allow_owner_transfer": permission.allow_owner_transfer,
|
||||
}, 200
|
||||
return WorkspacePermissionResponse(
|
||||
workspace_id=permission.workspace_id,
|
||||
allow_member_invite=permission.allow_member_invite,
|
||||
allow_owner_transfer=permission.allow_owner_transfer,
|
||||
).model_dump(mode="json"), HTTPStatus.OK
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from collections.abc import Generator
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from flask import request
|
||||
from pydantic import BaseModel, Field, RootModel
|
||||
from pydantic import BaseModel, Field, RootModel, field_validator
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import Forbidden, NotFound
|
||||
@@ -13,24 +14,20 @@ from controllers.common.errors import FilenameNotExistsError, NoFileUploadedErro
|
||||
from controllers.common.fields import GeneratedAppResponse
|
||||
from controllers.common.schema import (
|
||||
query_params_from_model,
|
||||
query_params_from_request,
|
||||
register_response_schema_models,
|
||||
register_schema_model,
|
||||
register_schema_models,
|
||||
)
|
||||
from controllers.console.app.wraps import with_session
|
||||
from controllers.service_api import service_api_ns
|
||||
from controllers.service_api.dataset.error import PipelineRunError
|
||||
from controllers.service_api.dataset.rag_pipeline.serializers import serialize_upload_file
|
||||
from controllers.service_api.schema import (
|
||||
event_stream_response,
|
||||
json_or_event_stream_response,
|
||||
multipart_file_params,
|
||||
)
|
||||
from controllers.service_api.schema import event_stream_response, json_or_event_stream_response, multipart_file_params
|
||||
from controllers.service_api.wraps import DatasetApiResource
|
||||
from core.app.apps.pipeline.pipeline_generator import PipelineGenerator
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from fields.base import ResponseModel
|
||||
from libs import helper
|
||||
from libs.helper import dump_response
|
||||
from libs.login import current_user
|
||||
from models import Account
|
||||
from models.dataset import Dataset, Pipeline
|
||||
@@ -84,7 +81,7 @@ class DatasourcePluginResponse(ResponseModel):
|
||||
datasource_type: str | None = None
|
||||
title: str | None = None
|
||||
user_input_variables: list[dict[str, Any]] = Field(default_factory=list)
|
||||
credentials: list[DatasourceCredentialInfoResponse]
|
||||
credentials: list[DatasourceCredentialInfoResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class DatasourcePluginListResponse(RootModel[list[DatasourcePluginResponse]]):
|
||||
@@ -100,14 +97,22 @@ class PipelineUploadFileResponse(ResponseModel):
|
||||
created_by: str
|
||||
created_at: str | None = None
|
||||
|
||||
@field_validator("created_at", mode="before")
|
||||
@classmethod
|
||||
def _normalize_created_at(cls, value: datetime | str | None) -> str | None:
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat()
|
||||
return value
|
||||
|
||||
|
||||
register_schema_model(service_api_ns, DatasourceNodeRunPayload)
|
||||
register_schema_model(service_api_ns, DatasourcePluginsQuery)
|
||||
register_schema_model(service_api_ns, PipelineRunApiEntity)
|
||||
register_schema_models(service_api_ns, DatasourcePluginsQuery)
|
||||
register_response_schema_models(
|
||||
service_api_ns,
|
||||
DatasourceCredentialInfoResponse,
|
||||
DatasourcePluginResponse,
|
||||
DatasourcePluginListResponse,
|
||||
GeneratedAppResponse,
|
||||
PipelineUploadFileResponse,
|
||||
)
|
||||
|
||||
@@ -119,8 +124,8 @@ class DatasourcePluginsApi(DatasetApiResource):
|
||||
@service_api_ns.doc(
|
||||
summary="List Datasource Plugins",
|
||||
description=(
|
||||
"List the datasource nodes configured in the knowledge pipeline. Each node includes the "
|
||||
"plugin it uses plus the metadata needed to run it."
|
||||
"List the datasource nodes configured in the knowledge pipeline. Each node includes the plugin it uses "
|
||||
"plus the metadata needed to run it."
|
||||
),
|
||||
tags=["Knowledge Pipeline"],
|
||||
responses={
|
||||
@@ -152,14 +157,13 @@ class DatasourcePluginsApi(DatasetApiResource):
|
||||
if not dataset:
|
||||
raise NotFound("Dataset not found.")
|
||||
|
||||
# Get query parameter to determine published or draft
|
||||
is_published: bool = request.args.get("is_published", default=True, type=bool)
|
||||
query = query_params_from_request(DatasourcePluginsQuery)
|
||||
|
||||
rag_pipeline_service: RagPipelineService = RagPipelineService()
|
||||
datasource_plugins: list[dict[Any, Any]] = rag_pipeline_service.get_datasource_plugins(
|
||||
tenant_id=tenant_id, dataset_id=dataset_id_str, is_published=is_published
|
||||
tenant_id=tenant_id, dataset_id=dataset_id_str, is_published=query.is_published
|
||||
)
|
||||
return datasource_plugins, 200
|
||||
return dump_response(DatasourcePluginListResponse, datasource_plugins), 200
|
||||
|
||||
|
||||
@service_api_ns.route("/datasets/<uuid:dataset_id>/pipeline/datasource/nodes/<string:node_id>/run")
|
||||
@@ -169,8 +173,8 @@ class DatasourceNodeRunApi(DatasetApiResource):
|
||||
@service_api_ns.doc(
|
||||
summary="Run Datasource Node",
|
||||
description=(
|
||||
"Execute a single datasource node within the knowledge pipeline. Returns a streaming "
|
||||
"response with the node execution results."
|
||||
"Execute a single datasource node within the knowledge pipeline. Returns a streaming response with the "
|
||||
"node execution results."
|
||||
),
|
||||
tags=["Knowledge Pipeline"],
|
||||
responses={
|
||||
@@ -189,11 +193,6 @@ class DatasourceNodeRunApi(DatasetApiResource):
|
||||
}
|
||||
)
|
||||
@service_api_ns.expect(service_api_ns.models[DatasourceNodeRunPayload.__name__])
|
||||
@service_api_ns.response(
|
||||
200,
|
||||
"Datasource node run successfully",
|
||||
service_api_ns.models[GeneratedAppResponse.__name__],
|
||||
)
|
||||
def post(self, tenant_id: str, dataset_id: UUID, node_id: str):
|
||||
"""Resource for getting datasource plugins."""
|
||||
dataset_id_str = str(dataset_id)
|
||||
@@ -210,10 +209,11 @@ class DatasourceNodeRunApi(DatasetApiResource):
|
||||
datasource_node_run_api_entity = DatasourceNodeRunApiEntity.model_validate(
|
||||
{
|
||||
**payload.model_dump(exclude_none=True),
|
||||
"pipeline_id": str(pipeline.id),
|
||||
"pipeline_id": pipeline.id,
|
||||
"node_id": node_id,
|
||||
}
|
||||
)
|
||||
# response-contract:ignore compact_generate_response
|
||||
return helper.compact_generate_response(
|
||||
PipelineGenerator.convert_to_event_stream(
|
||||
rag_pipeline_service.run_datasource_workflow_node(
|
||||
@@ -236,8 +236,8 @@ class PipelineRunApi(DatasetApiResource):
|
||||
@service_api_ns.doc(
|
||||
summary="Run Pipeline",
|
||||
description=(
|
||||
"Execute the full knowledge pipeline for a knowledge base. Supports both streaming and "
|
||||
"blocking response modes."
|
||||
"Execute the full knowledge pipeline for a knowledge base. Supports both streaming and blocking response "
|
||||
"modes."
|
||||
),
|
||||
tags=["Knowledge Pipeline"],
|
||||
responses={
|
||||
@@ -293,6 +293,7 @@ class PipelineRunApi(DatasetApiResource):
|
||||
streaming=payload.response_mode == "streaming",
|
||||
)
|
||||
|
||||
# response-contract:ignore compact_generate_response
|
||||
return helper.compact_generate_response(response)
|
||||
except Exception as ex:
|
||||
raise PipelineRunError(description=str(ex))
|
||||
@@ -368,4 +369,4 @@ class KnowledgebasePipelineFileUploadApi(DatasetApiResource):
|
||||
except services.errors.file.UnsupportedFileTypeError:
|
||||
raise UnsupportedFileTypeError()
|
||||
|
||||
return serialize_upload_file(upload_file), 201
|
||||
return dump_response(PipelineUploadFileResponse, upload_file), 201
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
"""
|
||||
Serialization helpers for Service API knowledge pipeline endpoints.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, TypedDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from models.model import UploadFile
|
||||
|
||||
|
||||
class UploadFileDict(TypedDict):
|
||||
id: str
|
||||
name: str
|
||||
size: int
|
||||
extension: str
|
||||
mime_type: str | None
|
||||
created_by: str
|
||||
created_at: str | None
|
||||
|
||||
|
||||
def serialize_upload_file(upload_file: UploadFile) -> UploadFileDict:
|
||||
return {
|
||||
"id": upload_file.id,
|
||||
"name": upload_file.name,
|
||||
"size": upload_file.size,
|
||||
"extension": upload_file.extension,
|
||||
"mime_type": upload_file.mime_type,
|
||||
"created_by": upload_file.created_by,
|
||||
"created_at": upload_file.created_at.isoformat() if upload_file.created_at else None,
|
||||
}
|
||||
@@ -9,6 +9,7 @@ from werkzeug.exceptions import Unauthorized
|
||||
import services
|
||||
from configs import dify_config
|
||||
from controllers.common.fields import (
|
||||
AccessTokenData,
|
||||
AccessTokenResultResponse,
|
||||
LoginStatusResponse,
|
||||
SimpleResultDataResponse,
|
||||
@@ -115,9 +116,10 @@ class LoginApi(Resource):
|
||||
raise AuthenticationFailedError()
|
||||
|
||||
token = WebAppAuthService.login(account=account)
|
||||
response = make_response({"result": "success", "data": {"access_token": token}})
|
||||
# set_access_token_to_cookie(request, response, token, samesite="None", httponly=False)
|
||||
return response
|
||||
return AccessTokenResultResponse(result="success", data=AccessTokenData(access_token=token)).model_dump(
|
||||
mode="json"
|
||||
)
|
||||
|
||||
|
||||
# this api helps frontend to check whether user is authenticated
|
||||
@@ -136,14 +138,12 @@ class LoginStatusApi(Resource):
|
||||
)
|
||||
@web_ns.response(200, "Login status", web_ns.models[LoginStatusResponse.__name__])
|
||||
def get(self):
|
||||
app_code = request.args.get("app_code")
|
||||
user_id = request.args.get("user_id")
|
||||
query = LoginStatusQuery.model_validate(request.args.to_dict(flat=True))
|
||||
app_code = query.app_code
|
||||
user_id = query.user_id
|
||||
token = extract_webapp_access_token(request)
|
||||
if not app_code:
|
||||
return {
|
||||
"logged_in": bool(token),
|
||||
"app_logged_in": False,
|
||||
}
|
||||
return LoginStatusResponse(logged_in=bool(token), app_logged_in=False).model_dump(mode="json")
|
||||
app_id = AppService.get_app_id_by_code(app_code)
|
||||
is_public = not dify_config.ENTERPRISE_ENABLED or not WebAppAuthService.is_app_require_permission_check(
|
||||
app_id=app_id
|
||||
@@ -165,10 +165,7 @@ class LoginStatusApi(Resource):
|
||||
except Exception:
|
||||
app_logged_in = False
|
||||
|
||||
return {
|
||||
"logged_in": user_logged_in,
|
||||
"app_logged_in": app_logged_in,
|
||||
}
|
||||
return LoginStatusResponse(logged_in=user_logged_in, app_logged_in=app_logged_in).model_dump(mode="json")
|
||||
|
||||
|
||||
@web_ns.route("/logout")
|
||||
@@ -183,7 +180,8 @@ class LogoutApi(Resource):
|
||||
)
|
||||
@web_ns.response(200, "Logout successful", web_ns.models[SimpleResultResponse.__name__])
|
||||
def post(self):
|
||||
response = make_response({"result": "success"})
|
||||
# response-contract:ignore hand-crafted response
|
||||
response = make_response(SimpleResultResponse(result="success").model_dump(mode="json"))
|
||||
# enterprise SSO sets same site to None in https deployment
|
||||
# so we need to logout by calling api
|
||||
clear_webapp_access_token_from_cookie(response, samesite="None")
|
||||
@@ -216,9 +214,8 @@ class EmailCodeLoginSendEmailApi(Resource):
|
||||
account = WebAppAuthService.get_user_through_email(payload.email)
|
||||
if account is None:
|
||||
raise AuthenticationFailedError()
|
||||
else:
|
||||
token = WebAppAuthService.send_email_code_login_email(account=account, language=language)
|
||||
return {"result": "success", "data": token}
|
||||
token = WebAppAuthService.send_email_code_login_email(account=account, language=language)
|
||||
return SimpleResultDataResponse(result="success", data=token).model_dump(mode="json")
|
||||
|
||||
|
||||
@web_ns.route("/email-code-login/validity")
|
||||
@@ -277,9 +274,10 @@ class EmailCodeLoginApi(Resource):
|
||||
|
||||
token = WebAppAuthService.login(account=account)
|
||||
AccountService.reset_login_error_rate_limit(user_email)
|
||||
response = make_response({"result": "success", "data": {"access_token": token}})
|
||||
# set_access_token_to_cookie(request, response, token, samesite="None", httponly=False)
|
||||
return response
|
||||
return AccessTokenResultResponse(result="success", data=AccessTokenData(access_token=token)).model_dump(
|
||||
mode="json"
|
||||
)
|
||||
|
||||
|
||||
def _log_web_login_failure(*, email: str, reason: LoginFailureReason) -> None:
|
||||
|
||||
@@ -2,7 +2,7 @@ import uuid
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from flask import make_response, request
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import func, select
|
||||
@@ -10,11 +10,12 @@ from werkzeug.exceptions import NotFound, Unauthorized
|
||||
|
||||
from configs import dify_config
|
||||
from constants import HEADER_NAME_APP_CODE
|
||||
from controllers.common.fields import AccessTokenData
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.web import web_ns
|
||||
from controllers.web.error import WebAppAuthRequiredError
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from libs.helper import dump_response
|
||||
from libs.passport import PassportService
|
||||
from libs.token import extract_webapp_access_token
|
||||
from models.enums import EndUserType
|
||||
@@ -28,7 +29,13 @@ class PassportQuery(BaseModel):
|
||||
|
||||
|
||||
register_schema_models(web_ns, PassportQuery)
|
||||
register_response_schema_models(web_ns, AccessTokenData)
|
||||
|
||||
|
||||
class PassportAccessTokenResponse(ResponseModel):
|
||||
access_token: str
|
||||
|
||||
|
||||
register_response_schema_models(web_ns, PassportAccessTokenResponse)
|
||||
|
||||
|
||||
@web_ns.route("/passport")
|
||||
@@ -45,7 +52,7 @@ class PassportResource(Resource):
|
||||
404: "Application or user not found",
|
||||
}
|
||||
)
|
||||
@web_ns.response(200, "Passport retrieved successfully", web_ns.models[AccessTokenData.__name__])
|
||||
@web_ns.response(200, "Passport retrieved successfully", web_ns.models[PassportAccessTokenResponse.__name__])
|
||||
def get(self):
|
||||
system_features = FeatureService.get_system_features()
|
||||
app_code = request.headers.get(HEADER_NAME_APP_CODE)
|
||||
@@ -59,8 +66,11 @@ class PassportResource(Resource):
|
||||
if app_auth_type != WebAppAuthType.PUBLIC:
|
||||
if not enterprise_user_decoded:
|
||||
raise WebAppAuthRequiredError()
|
||||
return exchange_token_for_existing_web_user(
|
||||
app_code=app_code, enterprise_user_decoded=enterprise_user_decoded, auth_type=app_auth_type
|
||||
return dump_response(
|
||||
PassportAccessTokenResponse,
|
||||
exchange_token_for_existing_web_user(
|
||||
app_code=app_code, enterprise_user_decoded=enterprise_user_decoded, auth_type=app_auth_type
|
||||
),
|
||||
)
|
||||
|
||||
# get site from db and check if it is normal
|
||||
@@ -110,12 +120,7 @@ class PassportResource(Resource):
|
||||
|
||||
tk = PassportService().issue(payload)
|
||||
|
||||
response = make_response(
|
||||
{
|
||||
"access_token": tk,
|
||||
}
|
||||
)
|
||||
return response
|
||||
return dump_response(PassportAccessTokenResponse, {"access_token": tk})
|
||||
|
||||
|
||||
def decode_enterprise_webapp_user_id(jwt_token: str | None) -> dict[str, Any] | None:
|
||||
@@ -206,12 +211,7 @@ def exchange_token_for_existing_web_user(
|
||||
"exp": exp,
|
||||
}
|
||||
token: str = PassportService().issue(payload)
|
||||
resp = make_response(
|
||||
{
|
||||
"access_token": token,
|
||||
}
|
||||
)
|
||||
return resp
|
||||
return {"access_token": token}
|
||||
|
||||
|
||||
def _exchange_for_public_app_token(app_model, site, token_decoded):
|
||||
@@ -244,12 +244,7 @@ def _exchange_for_public_app_token(app_model, site, token_decoded):
|
||||
|
||||
tk = PassportService().issue(payload)
|
||||
|
||||
resp = make_response(
|
||||
{
|
||||
"access_token": tk,
|
||||
}
|
||||
)
|
||||
return resp
|
||||
return {"access_token": tk}
|
||||
|
||||
|
||||
def generate_session_id():
|
||||
|
||||
@@ -61,10 +61,17 @@ _sensitive_word_avoidance_adapter: TypeAdapter[SensitiveWordAvoidanceConfig] = T
|
||||
|
||||
def _normalize_raw(raw: Any) -> Any:
|
||||
if isinstance(raw, dict):
|
||||
if raw.get("enabled") is None:
|
||||
enabled = raw.get("enabled")
|
||||
if enabled is None:
|
||||
raw = {**raw, "enabled": False}
|
||||
elif raw.get("enabled") is True and raw.get("config") is None:
|
||||
raw = {**raw, "config": {}}
|
||||
elif enabled is True:
|
||||
if raw.get("config") is None:
|
||||
raw = {**raw, "config": {}}
|
||||
else:
|
||||
# enabled is False or any falsy value —
|
||||
# drop extra fields (type, config) so they don't
|
||||
# violate SensitiveWordAvoidanceDisabledConfig.extra="forbid"
|
||||
raw = {"enabled": False}
|
||||
return raw
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ from collections.abc import Generator, Mapping, Sequence
|
||||
from mimetypes import guess_extension
|
||||
from typing import TYPE_CHECKING, Any, Union
|
||||
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from core.app.app_config.entities import ExternalDataVariableEntity, PromptTemplateEntity
|
||||
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
|
||||
from core.app.apps.exc import GenerateTaskStoppedError
|
||||
@@ -423,7 +425,9 @@ class AppRunner:
|
||||
_logger.exception("Failed to save image file")
|
||||
return
|
||||
|
||||
# Create MessageFile record
|
||||
# Create MessageFile record.
|
||||
# Use an independent session so this side-effect write does not
|
||||
# commit or close the caller's request-scoped session.
|
||||
message_file = MessageFile(
|
||||
message_id=message_id,
|
||||
type=FileType.IMAGE,
|
||||
@@ -437,9 +441,8 @@ class AppRunner:
|
||||
created_by=user_id,
|
||||
)
|
||||
|
||||
db.session.add(message_file)
|
||||
db.session.commit()
|
||||
db.session.refresh(message_file)
|
||||
with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session:
|
||||
session.add(message_file)
|
||||
|
||||
# Publish QueueMessageFileEvent
|
||||
queue_manager.publish(
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
from collections.abc import Sequence
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.orm import scoped_session
|
||||
from sqlalchemy.orm import Session, scoped_session, sessionmaker
|
||||
|
||||
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
@@ -10,6 +10,7 @@ from core.app.entities.queue_entities import QueueRetrieverResourcesEvent
|
||||
from core.rag.entities import RetrievalSourceMetadata
|
||||
from core.rag.index_processor.constant.index_type import IndexStructureType
|
||||
from core.rag.models.document import Document
|
||||
from extensions.ext_database import db
|
||||
from models.dataset import ChildChunk, DatasetQuery, DocumentSegment
|
||||
from models.dataset import Document as DatasetDocument
|
||||
from models.enums import CreatorUserRole, DatasetQuerySource
|
||||
@@ -46,47 +47,52 @@ class DatasetIndexToolCallbackHandler:
|
||||
created_by=self._user_id,
|
||||
)
|
||||
|
||||
session.add(dataset_query)
|
||||
session.commit()
|
||||
# Use an independent session so this audit-log side effect does
|
||||
# not commit or close the caller's request-scoped session.
|
||||
with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as independent_session:
|
||||
independent_session.add(dataset_query)
|
||||
|
||||
def on_tool_end(self, documents: list[Document], session: scoped_session):
|
||||
"""Handle tool end."""
|
||||
for document in documents:
|
||||
if document.metadata is not None:
|
||||
document_id = document.metadata["document_id"]
|
||||
dataset_document_stmt = select(DatasetDocument).where(DatasetDocument.id == document_id)
|
||||
dataset_document = session.scalar(dataset_document_stmt)
|
||||
if not dataset_document:
|
||||
_logger.warning(
|
||||
"Expected DatasetDocument record to exist, but none was found, document_id=%s",
|
||||
document_id,
|
||||
)
|
||||
continue
|
||||
if dataset_document.doc_form == IndexStructureType.PARENT_CHILD_INDEX:
|
||||
child_chunk_stmt = select(ChildChunk).where(
|
||||
ChildChunk.index_node_id == document.metadata["doc_id"],
|
||||
ChildChunk.dataset_id == dataset_document.dataset_id,
|
||||
ChildChunk.document_id == dataset_document.id,
|
||||
)
|
||||
child_chunk = session.scalar(child_chunk_stmt)
|
||||
if child_chunk:
|
||||
session.execute(
|
||||
update(DocumentSegment)
|
||||
.where(DocumentSegment.id == child_chunk.segment_id)
|
||||
.values(hit_count=DocumentSegment.hit_count + 1)
|
||||
# Use an independent session so hit-count updates do not
|
||||
# interfere with the caller's request-scoped session.
|
||||
with Session(db.engine, expire_on_commit=False) as independent_session:
|
||||
for document in documents:
|
||||
if document.metadata is not None:
|
||||
document_id = document.metadata["document_id"]
|
||||
dataset_document_stmt = select(DatasetDocument).where(DatasetDocument.id == document_id)
|
||||
dataset_document = independent_session.scalar(dataset_document_stmt)
|
||||
if not dataset_document:
|
||||
_logger.warning(
|
||||
"Expected DatasetDocument record to exist, but none was found, document_id=%s",
|
||||
document_id,
|
||||
)
|
||||
else:
|
||||
conditions = [DocumentSegment.index_node_id == document.metadata["doc_id"]]
|
||||
continue
|
||||
if dataset_document.doc_form == IndexStructureType.PARENT_CHILD_INDEX:
|
||||
child_chunk_stmt = select(ChildChunk).where(
|
||||
ChildChunk.index_node_id == document.metadata["doc_id"],
|
||||
ChildChunk.dataset_id == dataset_document.dataset_id,
|
||||
ChildChunk.document_id == dataset_document.id,
|
||||
)
|
||||
child_chunk = independent_session.scalar(child_chunk_stmt)
|
||||
if child_chunk:
|
||||
independent_session.execute(
|
||||
update(DocumentSegment)
|
||||
.where(DocumentSegment.id == child_chunk.segment_id)
|
||||
.values(hit_count=DocumentSegment.hit_count + 1)
|
||||
)
|
||||
else:
|
||||
conditions = [DocumentSegment.index_node_id == document.metadata["doc_id"]]
|
||||
|
||||
if "dataset_id" in document.metadata:
|
||||
conditions.append(DocumentSegment.dataset_id == document.metadata["dataset_id"])
|
||||
if "dataset_id" in document.metadata:
|
||||
conditions.append(DocumentSegment.dataset_id == document.metadata["dataset_id"])
|
||||
|
||||
# add hit count to document segment
|
||||
session.execute(
|
||||
update(DocumentSegment).where(*conditions).values(hit_count=DocumentSegment.hit_count + 1)
|
||||
)
|
||||
# add hit count to document segment
|
||||
independent_session.execute(
|
||||
update(DocumentSegment).where(*conditions).values(hit_count=DocumentSegment.hit_count + 1)
|
||||
)
|
||||
|
||||
session.commit()
|
||||
independent_session.commit()
|
||||
|
||||
# TODO(-LAN-): Improve type check
|
||||
def return_retriever_resource_info(self, resource: Sequence[RetrievalSourceMetadata]):
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Any
|
||||
|
||||
from core.datasource.__base.datasource_plugin import DatasourcePlugin
|
||||
from core.datasource.entities.datasource_entities import DatasourceProviderEntityWithPlugin, DatasourceProviderType
|
||||
from core.entities.provider_entities import ProviderConfig
|
||||
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
|
||||
from core.plugin.impl.tool import PluginToolManager
|
||||
from core.tools.errors import ToolProviderCredentialValidationError
|
||||
|
||||
@@ -78,11 +78,11 @@ class DatasourcePluginProviderController(ABC):
|
||||
if not credential_schema.required and credentials[credential_name] is None:
|
||||
continue
|
||||
|
||||
if credential_schema.type in {ProviderConfig.Type.SECRET_INPUT, ProviderConfig.Type.TEXT_INPUT}:
|
||||
if credential_schema.type in {ProviderConfigType.SECRET_INPUT, ProviderConfigType.TEXT_INPUT}:
|
||||
if not isinstance(credentials[credential_name], str):
|
||||
raise ToolProviderCredentialValidationError(f"credential {credential_name} should be string")
|
||||
|
||||
elif credential_schema.type == ProviderConfig.Type.SELECT:
|
||||
elif credential_schema.type == ProviderConfigType.SELECT:
|
||||
if not isinstance(credentials[credential_name], str):
|
||||
raise ToolProviderCredentialValidationError(f"credential {credential_name} should be string")
|
||||
|
||||
@@ -107,9 +107,9 @@ class DatasourcePluginProviderController(ABC):
|
||||
default_value = credential_schema.default
|
||||
# parse default value into the correct type
|
||||
if credential_schema.type in {
|
||||
ProviderConfig.Type.SECRET_INPUT,
|
||||
ProviderConfig.Type.TEXT_INPUT,
|
||||
ProviderConfig.Type.SELECT,
|
||||
ProviderConfigType.SECRET_INPUT,
|
||||
ProviderConfigType.TEXT_INPUT,
|
||||
ProviderConfigType.SELECT,
|
||||
}:
|
||||
default_value = str(default_value)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from urllib.parse import urlparse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from configs import dify_config
|
||||
from core.entities.provider_entities import BasicProviderConfig
|
||||
from core.entities.provider_entities import BasicProviderConfig, ProviderConfigType
|
||||
from core.helper import encrypter
|
||||
from core.helper.provider_cache import NoOpProviderCredentialCache
|
||||
from core.mcp.types import OAuthClientInformation, OAuthClientMetadata, OAuthTokens
|
||||
@@ -315,7 +315,7 @@ class MCPProviderEntity(BaseModel):
|
||||
return data
|
||||
|
||||
# Create dynamic config only for encrypted fields
|
||||
config = [BasicProviderConfig(type=BasicProviderConfig.Type.SECRET_INPUT, name=key) for key in encrypted_fields]
|
||||
config = [BasicProviderConfig(type=ProviderConfigType.SECRET_INPUT, name=key) for key in encrypted_fields]
|
||||
|
||||
encrypter_instance, _ = create_provider_encrypter(
|
||||
tenant_id=self.tenant_id,
|
||||
|
||||
@@ -165,34 +165,35 @@ class ModelSettings(BaseModel):
|
||||
model_config = ConfigDict(protected_namespaces=())
|
||||
|
||||
|
||||
class ProviderConfigType(StrEnum):
|
||||
SECRET_INPUT = CommonParameterType.SECRET_INPUT
|
||||
TEXT_INPUT = CommonParameterType.TEXT_INPUT
|
||||
SELECT = CommonParameterType.SELECT
|
||||
BOOLEAN = CommonParameterType.BOOLEAN
|
||||
APP_SELECTOR = CommonParameterType.APP_SELECTOR
|
||||
MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR
|
||||
TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR
|
||||
|
||||
@classmethod
|
||||
def value_of(cls, value: str) -> ProviderConfigType:
|
||||
"""
|
||||
Get value of given mode.
|
||||
|
||||
:param value: mode value
|
||||
:return: mode
|
||||
"""
|
||||
for mode in cls:
|
||||
if mode.value == value:
|
||||
return mode
|
||||
raise ValueError(f"invalid mode value {value}")
|
||||
|
||||
|
||||
class BasicProviderConfig(BaseModel):
|
||||
"""
|
||||
Base model class for common provider settings like credentials
|
||||
"""
|
||||
|
||||
class Type(StrEnum):
|
||||
SECRET_INPUT = CommonParameterType.SECRET_INPUT
|
||||
TEXT_INPUT = CommonParameterType.TEXT_INPUT
|
||||
SELECT = CommonParameterType.SELECT
|
||||
BOOLEAN = CommonParameterType.BOOLEAN
|
||||
APP_SELECTOR = CommonParameterType.APP_SELECTOR
|
||||
MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR
|
||||
TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR
|
||||
|
||||
@classmethod
|
||||
def value_of(cls, value: str) -> ProviderConfig.Type:
|
||||
"""
|
||||
Get value of given mode.
|
||||
|
||||
:param value: mode value
|
||||
:return: mode
|
||||
"""
|
||||
for mode in cls:
|
||||
if mode.value == value:
|
||||
return mode
|
||||
raise ValueError(f"invalid mode value {value}")
|
||||
|
||||
type: Type = Field(..., description="The type of the credentials")
|
||||
type: ProviderConfigType = Field(..., description="The type of the credentials")
|
||||
name: str = Field(..., description="The name of the credentials")
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from collections.abc import Mapping
|
||||
from copy import deepcopy
|
||||
from typing import Any, Protocol
|
||||
|
||||
from core.entities.provider_entities import BasicProviderConfig
|
||||
from core.entities.provider_entities import BasicProviderConfig, ProviderConfigType
|
||||
from core.helper import encrypter
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ class ProviderConfigEncrypter:
|
||||
fields[credential.name] = credential
|
||||
|
||||
for field_name, field in fields.items():
|
||||
if field.type == BasicProviderConfig.Type.SECRET_INPUT:
|
||||
if field.type == ProviderConfigType.SECRET_INPUT:
|
||||
if field_name in data:
|
||||
encrypted = encrypter.encrypt_token(self.tenant_id, data[field_name] or "")
|
||||
data[field_name] = encrypted
|
||||
@@ -81,7 +81,7 @@ class ProviderConfigEncrypter:
|
||||
fields[credential.name] = credential
|
||||
|
||||
for field_name, field in fields.items():
|
||||
if field.type == BasicProviderConfig.Type.SECRET_INPUT:
|
||||
if field.type == ProviderConfigType.SECRET_INPUT:
|
||||
if field_name in data:
|
||||
if len(data[field_name]) > 6:
|
||||
data[field_name] = (
|
||||
@@ -112,7 +112,7 @@ class ProviderConfigEncrypter:
|
||||
fields[credential.name] = credential
|
||||
|
||||
for field_name, field in fields.items():
|
||||
if field.type == BasicProviderConfig.Type.SECRET_INPUT:
|
||||
if field.type == ProviderConfigType.SECRET_INPUT:
|
||||
if field_name in data:
|
||||
with contextlib.suppress(Exception):
|
||||
# if the value is None or empty string, skip decrypt
|
||||
|
||||
@@ -5,12 +5,13 @@ from pydantic import BaseModel
|
||||
from core.plugin.entities.plugin import PluginDeclaration, PluginInstallationSource
|
||||
|
||||
|
||||
class PluginBundleDependency(BaseModel):
|
||||
class Type(StrEnum):
|
||||
Github = PluginInstallationSource.Github.value
|
||||
Marketplace = PluginInstallationSource.Marketplace.value
|
||||
Package = PluginInstallationSource.Package.value
|
||||
class PluginBundleDependencyType(StrEnum):
|
||||
Github = PluginInstallationSource.Github.value
|
||||
Marketplace = PluginInstallationSource.Marketplace.value
|
||||
Package = PluginInstallationSource.Package.value
|
||||
|
||||
|
||||
class PluginBundleDependency(BaseModel):
|
||||
class Github(BaseModel):
|
||||
repo_address: str
|
||||
repo: str
|
||||
@@ -26,5 +27,5 @@ class PluginBundleDependency(BaseModel):
|
||||
unique_identifier: str
|
||||
manifest: PluginDeclaration
|
||||
|
||||
type: Type
|
||||
type: PluginBundleDependencyType
|
||||
value: Github | Marketplace | Package
|
||||
|
||||
@@ -32,6 +32,7 @@ class EndpointEntity(BasePluginEntity):
|
||||
entity of an endpoint
|
||||
"""
|
||||
|
||||
# TODO: Confirm daemon masks secret-input settings before endpoint list responses expose them.
|
||||
settings: dict[str, Any]
|
||||
tenant_id: str
|
||||
plugin_id: str
|
||||
|
||||
@@ -57,11 +57,12 @@ class MCPServerParameterType(StrEnum):
|
||||
OBJECT = auto()
|
||||
|
||||
|
||||
class PluginParameterAutoGenerate(BaseModel):
|
||||
class Type(StrEnum):
|
||||
PROMPT_INSTRUCTION = auto()
|
||||
class PluginParameterAutoGenerateType(StrEnum):
|
||||
PROMPT_INSTRUCTION = auto()
|
||||
|
||||
type: Type
|
||||
|
||||
class PluginParameterAutoGenerate(BaseModel):
|
||||
type: PluginParameterAutoGenerateType
|
||||
|
||||
|
||||
class PluginParameterTemplate(BaseModel):
|
||||
|
||||
@@ -166,12 +166,13 @@ class PluginEntity(PluginInstallation):
|
||||
return self
|
||||
|
||||
|
||||
class PluginDependency(BaseModel):
|
||||
class Type(StrEnum):
|
||||
Github = PluginInstallationSource.Github
|
||||
Marketplace = PluginInstallationSource.Marketplace
|
||||
Package = PluginInstallationSource.Package
|
||||
class PluginDependencyType(StrEnum):
|
||||
Github = PluginInstallationSource.Github
|
||||
Marketplace = PluginInstallationSource.Marketplace
|
||||
Package = PluginInstallationSource.Package
|
||||
|
||||
|
||||
class PluginDependency(BaseModel):
|
||||
class Github(BaseModel):
|
||||
repo: str
|
||||
version: str
|
||||
@@ -194,7 +195,7 @@ class PluginDependency(BaseModel):
|
||||
plugin_unique_identifier: str
|
||||
version: str | None = None
|
||||
|
||||
type: Type
|
||||
type: PluginDependencyType
|
||||
value: Github | Marketplace | Package
|
||||
current_identifier: str | None = None
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
from core.entities.provider_entities import ProviderConfig
|
||||
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
|
||||
from core.tools.__base.tool import Tool
|
||||
from core.tools.entities.tool_entities import (
|
||||
ToolProviderEntity,
|
||||
@@ -71,11 +71,11 @@ class ToolProviderController[ToolProviderEntityT: ToolProviderEntity, ToolProvid
|
||||
if not credential_schema.required and credentials[credential_name] is None:
|
||||
continue
|
||||
|
||||
if credential_schema.type in {ProviderConfig.Type.SECRET_INPUT, ProviderConfig.Type.TEXT_INPUT}:
|
||||
if credential_schema.type in {ProviderConfigType.SECRET_INPUT, ProviderConfigType.TEXT_INPUT}:
|
||||
if not isinstance(credentials[credential_name], str):
|
||||
raise ToolProviderCredentialValidationError(f"credential {credential_name} should be string")
|
||||
|
||||
elif credential_schema.type == ProviderConfig.Type.SELECT:
|
||||
elif credential_schema.type == ProviderConfigType.SELECT:
|
||||
if not isinstance(credentials[credential_name], str):
|
||||
raise ToolProviderCredentialValidationError(f"credential {credential_name} should be string")
|
||||
|
||||
@@ -100,9 +100,9 @@ class ToolProviderController[ToolProviderEntityT: ToolProviderEntity, ToolProvid
|
||||
default_value = credential_schema.default
|
||||
# parse default value into the correct type
|
||||
if credential_schema.type in {
|
||||
ProviderConfig.Type.SECRET_INPUT,
|
||||
ProviderConfig.Type.TEXT_INPUT,
|
||||
ProviderConfig.Type.SELECT,
|
||||
ProviderConfigType.SECRET_INPUT,
|
||||
ProviderConfigType.TEXT_INPUT,
|
||||
ProviderConfigType.SELECT,
|
||||
}:
|
||||
default_value = str(default_value)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import override
|
||||
from pydantic import Field
|
||||
from sqlalchemy import select
|
||||
|
||||
from core.entities.provider_entities import ProviderConfig
|
||||
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
|
||||
from core.tools.__base.tool_provider import ToolProviderController
|
||||
from core.tools.__base.tool_runtime import ToolRuntime
|
||||
from core.tools.custom_tool.tool import ApiTool
|
||||
@@ -41,7 +41,7 @@ class ApiToolProviderController(ToolProviderController[ToolProviderEntity, ApiTo
|
||||
ProviderConfig(
|
||||
name="auth_type",
|
||||
required=True,
|
||||
type=ProviderConfig.Type.SELECT,
|
||||
type=ProviderConfigType.SELECT,
|
||||
options=[
|
||||
ProviderConfig.Option(value="none", label=I18nObject(en_US="None", zh_Hans="无")),
|
||||
ProviderConfig.Option(value="api_key_header", label=I18nObject(en_US="Header", zh_Hans="请求头")),
|
||||
@@ -60,20 +60,20 @@ class ApiToolProviderController(ToolProviderController[ToolProviderEntity, ApiTo
|
||||
name="api_key_header",
|
||||
required=False,
|
||||
default="Authorization",
|
||||
type=ProviderConfig.Type.TEXT_INPUT,
|
||||
type=ProviderConfigType.TEXT_INPUT,
|
||||
help=I18nObject(en_US="The header name of the api key", zh_Hans="携带 api key 的 header 名称"),
|
||||
),
|
||||
ProviderConfig(
|
||||
name="api_key_value",
|
||||
required=True,
|
||||
type=ProviderConfig.Type.SECRET_INPUT,
|
||||
type=ProviderConfigType.SECRET_INPUT,
|
||||
help=I18nObject(en_US="The api key", zh_Hans="api key 的值"),
|
||||
),
|
||||
ProviderConfig(
|
||||
name="api_key_header_prefix",
|
||||
required=False,
|
||||
default="basic",
|
||||
type=ProviderConfig.Type.SELECT,
|
||||
type=ProviderConfigType.SELECT,
|
||||
help=I18nObject(en_US="The prefix of the api key header", zh_Hans="api key header 的前缀"),
|
||||
options=[
|
||||
ProviderConfig.Option(value="basic", label=I18nObject(en_US="Basic", zh_Hans="Basic")),
|
||||
@@ -89,7 +89,7 @@ class ApiToolProviderController(ToolProviderController[ToolProviderEntity, ApiTo
|
||||
name="api_key_query_param",
|
||||
required=False,
|
||||
default="key",
|
||||
type=ProviderConfig.Type.TEXT_INPUT,
|
||||
type=ProviderConfigType.TEXT_INPUT,
|
||||
help=I18nObject(
|
||||
en_US="The query parameter name of the api key", zh_Hans="携带 api key 的查询参数名称"
|
||||
),
|
||||
@@ -97,7 +97,7 @@ class ApiToolProviderController(ToolProviderController[ToolProviderEntity, ApiTo
|
||||
ProviderConfig(
|
||||
name="api_key_value",
|
||||
required=True,
|
||||
type=ProviderConfig.Type.SECRET_INPUT,
|
||||
type=ProviderConfigType.SECRET_INPUT,
|
||||
help=I18nObject(en_US="The api key", zh_Hans="api key 的值"),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from collections.abc import Mapping
|
||||
from typing import Union, override
|
||||
|
||||
from core.entities.provider_entities import BasicProviderConfig, ProviderConfig
|
||||
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
|
||||
from core.helper.provider_cache import ProviderCredentialsCache
|
||||
from core.helper.provider_encryption import ProviderConfigCache, ProviderConfigEncrypter, create_provider_encrypter
|
||||
from core.plugin.entities.plugin_daemon import CredentialType
|
||||
@@ -142,7 +142,7 @@ def masked_credentials(
|
||||
if not config:
|
||||
masked_credentials[key] = value
|
||||
continue
|
||||
if config.type == BasicProviderConfig.Type.SECRET_INPUT:
|
||||
if config.type == ProviderConfigType.SECRET_INPUT:
|
||||
if len(value) <= 4:
|
||||
masked_credentials[key] = "*" * len(value)
|
||||
else:
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
import json
|
||||
from typing import override
|
||||
|
||||
from flask_restx import fields
|
||||
|
||||
from fields.workflow_fields import workflow_partial_fields
|
||||
from libs.helper import AppIconUrlField, TimestampField
|
||||
|
||||
|
||||
class JsonStringField(fields.Raw):
|
||||
@override
|
||||
def format(self, value):
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return json.loads(value)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
class OpaqueRawField(fields.Raw):
|
||||
@override
|
||||
def schema(self) -> dict[str, object]:
|
||||
return {"type": "object"}
|
||||
|
||||
|
||||
class StringListRawField(fields.Raw):
|
||||
@override
|
||||
def schema(self) -> dict[str, object]:
|
||||
return {"type": "array", "items": {"type": "string"}}
|
||||
|
||||
|
||||
class ObjectListRawField(fields.Raw):
|
||||
@override
|
||||
def schema(self) -> dict[str, object]:
|
||||
return {"type": "array", "items": {"type": "object"}}
|
||||
|
||||
|
||||
app_detail_kernel_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"description": fields.String,
|
||||
"mode": fields.String(attribute="mode_compatible_with_agent"),
|
||||
"icon_type": fields.String,
|
||||
"icon": fields.String,
|
||||
"icon_background": fields.String,
|
||||
"icon_url": AppIconUrlField,
|
||||
}
|
||||
|
||||
related_app_list = {
|
||||
"data": fields.List(fields.Nested(app_detail_kernel_fields)),
|
||||
"total": fields.Integer,
|
||||
}
|
||||
|
||||
model_config_fields = {
|
||||
"opening_statement": fields.String,
|
||||
"suggested_questions": StringListRawField(attribute="suggested_questions_list"),
|
||||
"suggested_questions_after_answer": OpaqueRawField(attribute="suggested_questions_after_answer_dict"),
|
||||
"speech_to_text": OpaqueRawField(attribute="speech_to_text_dict"),
|
||||
"text_to_speech": OpaqueRawField(attribute="text_to_speech_dict"),
|
||||
"retriever_resource": OpaqueRawField(attribute="retriever_resource_dict"),
|
||||
"annotation_reply": OpaqueRawField(attribute="annotation_reply_dict"),
|
||||
"more_like_this": OpaqueRawField(attribute="more_like_this_dict"),
|
||||
"sensitive_word_avoidance": OpaqueRawField(attribute="sensitive_word_avoidance_dict"),
|
||||
"external_data_tools": ObjectListRawField(attribute="external_data_tools_list"),
|
||||
"model": OpaqueRawField(attribute="model_dict"),
|
||||
"user_input_form": ObjectListRawField(attribute="user_input_form_list"),
|
||||
"dataset_query_variable": fields.String,
|
||||
"pre_prompt": fields.String,
|
||||
"agent_mode": OpaqueRawField(attribute="agent_mode_dict"),
|
||||
"prompt_type": fields.String,
|
||||
"chat_prompt_config": OpaqueRawField(attribute="chat_prompt_config_dict"),
|
||||
"completion_prompt_config": OpaqueRawField(attribute="completion_prompt_config_dict"),
|
||||
"dataset_configs": OpaqueRawField(attribute="dataset_configs_dict"),
|
||||
"file_upload": OpaqueRawField(attribute="file_upload_dict"),
|
||||
"created_by": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
}
|
||||
|
||||
tag_fields = {"id": fields.String, "name": fields.String, "type": fields.String}
|
||||
|
||||
app_detail_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"description": fields.String,
|
||||
"mode": fields.String(attribute="mode_compatible_with_agent"),
|
||||
"icon": fields.String,
|
||||
"icon_background": fields.String,
|
||||
"enable_site": fields.Boolean,
|
||||
"enable_api": fields.Boolean,
|
||||
"model_config": fields.Nested(model_config_fields, attribute="app_model_config", allow_null=True),
|
||||
"workflow": fields.Nested(workflow_partial_fields, allow_null=True),
|
||||
"tracing": OpaqueRawField,
|
||||
"use_icon_as_answer_icon": fields.Boolean,
|
||||
"created_by": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
"access_mode": fields.String,
|
||||
"tags": fields.List(fields.Nested(tag_fields)),
|
||||
"permission_keys": fields.List(fields.String()),
|
||||
}
|
||||
|
||||
prompt_config_fields = {
|
||||
"prompt_template": fields.String,
|
||||
}
|
||||
|
||||
model_config_partial_fields = {
|
||||
"model": OpaqueRawField(attribute="model_dict"),
|
||||
"pre_prompt": fields.String,
|
||||
"created_by": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
}
|
||||
|
||||
app_partial_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"max_active_requests": OpaqueRawField(),
|
||||
"description": fields.String(attribute="desc_or_prompt"),
|
||||
"mode": fields.String(attribute="mode_compatible_with_agent"),
|
||||
"icon_type": fields.String,
|
||||
"icon": fields.String,
|
||||
"icon_background": fields.String,
|
||||
"icon_url": AppIconUrlField,
|
||||
"model_config": fields.Nested(model_config_partial_fields, attribute="app_model_config", allow_null=True),
|
||||
"workflow": fields.Nested(workflow_partial_fields, allow_null=True),
|
||||
"use_icon_as_answer_icon": fields.Boolean,
|
||||
"created_by": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
"tags": fields.List(fields.Nested(tag_fields)),
|
||||
"access_mode": fields.String,
|
||||
"create_user_name": fields.String,
|
||||
"author_name": fields.String,
|
||||
"has_draft_trigger": fields.Boolean,
|
||||
"permission_keys": fields.List(fields.String()),
|
||||
}
|
||||
|
||||
|
||||
app_pagination_fields = {
|
||||
"page": fields.Integer,
|
||||
"limit": fields.Integer(attribute="per_page"),
|
||||
"total": fields.Integer,
|
||||
"has_more": fields.Boolean(attribute="has_next"),
|
||||
"data": fields.List(fields.Nested(app_partial_fields), attribute="items"),
|
||||
}
|
||||
|
||||
template_fields = {
|
||||
"name": fields.String,
|
||||
"icon": fields.String,
|
||||
"icon_background": fields.String,
|
||||
"description": fields.String,
|
||||
"mode": fields.String,
|
||||
"model_config": fields.Nested(model_config_fields),
|
||||
}
|
||||
|
||||
template_list_fields = {
|
||||
"data": fields.List(fields.Nested(template_fields)),
|
||||
}
|
||||
|
||||
site_fields = {
|
||||
"access_token": fields.String(attribute="code"),
|
||||
"code": fields.String,
|
||||
"title": fields.String,
|
||||
"icon_type": fields.String,
|
||||
"icon": fields.String,
|
||||
"icon_background": fields.String,
|
||||
"icon_url": AppIconUrlField,
|
||||
"description": fields.String,
|
||||
"default_language": fields.String,
|
||||
"chat_color_theme": fields.String,
|
||||
"chat_color_theme_inverted": fields.Boolean,
|
||||
"customize_domain": fields.String,
|
||||
"copyright": fields.String,
|
||||
"privacy_policy": fields.String,
|
||||
"custom_disclaimer": fields.String,
|
||||
"customize_token_strategy": fields.String,
|
||||
"prompt_public": fields.Boolean,
|
||||
"app_base_url": fields.String,
|
||||
"show_workflow_steps": fields.Boolean,
|
||||
"use_icon_as_answer_icon": fields.Boolean,
|
||||
"created_by": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
}
|
||||
|
||||
deleted_tool_fields = {
|
||||
"type": fields.String,
|
||||
"tool_name": fields.String,
|
||||
"provider_id": fields.String,
|
||||
}
|
||||
|
||||
app_detail_fields_with_site = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"description": fields.String,
|
||||
"mode": fields.String(attribute="mode_compatible_with_agent"),
|
||||
"icon_type": fields.String,
|
||||
"icon": fields.String,
|
||||
"icon_background": fields.String,
|
||||
"icon_url": AppIconUrlField,
|
||||
"enable_site": fields.Boolean,
|
||||
"enable_api": fields.Boolean,
|
||||
"model_config": fields.Nested(model_config_fields, attribute="app_model_config", allow_null=True),
|
||||
"workflow": fields.Nested(workflow_partial_fields, allow_null=True),
|
||||
"api_base_url": fields.String,
|
||||
"use_icon_as_answer_icon": fields.Boolean,
|
||||
"max_active_requests": fields.Integer,
|
||||
"created_by": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
"deleted_tools": fields.List(fields.Nested(deleted_tool_fields)),
|
||||
"access_mode": fields.String,
|
||||
"tags": fields.List(fields.Nested(tag_fields)),
|
||||
"permission_keys": fields.List(fields.String()),
|
||||
"site": fields.Nested(site_fields),
|
||||
}
|
||||
|
||||
|
||||
app_site_fields = {
|
||||
"app_id": fields.String,
|
||||
"access_token": fields.String(attribute="code"),
|
||||
"code": fields.String,
|
||||
"title": fields.String,
|
||||
"icon": fields.String,
|
||||
"icon_background": fields.String,
|
||||
"description": fields.String,
|
||||
"default_language": fields.String,
|
||||
"customize_domain": fields.String,
|
||||
"copyright": fields.String,
|
||||
"privacy_policy": fields.String,
|
||||
"custom_disclaimer": fields.String,
|
||||
"customize_token_strategy": fields.String,
|
||||
"prompt_public": fields.Boolean,
|
||||
"show_workflow_steps": fields.Boolean,
|
||||
"use_icon_as_answer_icon": fields.Boolean,
|
||||
}
|
||||
|
||||
leaked_dependency_fields = {"type": fields.String, "value": OpaqueRawField, "current_identifier": fields.String}
|
||||
|
||||
app_import_fields = {
|
||||
"id": fields.String,
|
||||
"status": fields.String,
|
||||
"app_id": fields.String,
|
||||
"app_mode": fields.String,
|
||||
"current_dsl_version": fields.String,
|
||||
"imported_dsl_version": fields.String,
|
||||
"error": fields.String,
|
||||
}
|
||||
|
||||
app_import_check_dependencies_fields = {
|
||||
"leaked_dependencies": fields.List(fields.Nested(leaked_dependency_fields)),
|
||||
}
|
||||
|
||||
app_server_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"server_code": fields.String,
|
||||
"description": fields.String,
|
||||
"status": fields.String,
|
||||
"parameters": JsonStringField,
|
||||
"created_at": TimestampField,
|
||||
"updated_at": TimestampField,
|
||||
}
|
||||
@@ -2,18 +2,11 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from flask_restx import fields
|
||||
from pydantic import Field, computed_field, field_validator
|
||||
|
||||
from fields.base import ResponseModel
|
||||
from libs.helper import build_avatar_url, to_timestamp
|
||||
|
||||
simple_account_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"email": fields.String,
|
||||
}
|
||||
|
||||
|
||||
class SimpleAccountResponse(ResponseModel):
|
||||
id: str
|
||||
|
||||
@@ -1,61 +1,74 @@
|
||||
from typing import override
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from flask_restx import fields
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from fields.member_fields import simple_account_fields
|
||||
from libs.helper import TimestampField
|
||||
from fields.base import ResponseModel
|
||||
from fields.member_fields import SimpleAccountResponse
|
||||
from libs.helper import to_timestamp
|
||||
from models.snippet import SnippetType
|
||||
|
||||
|
||||
class OpaqueRawField(fields.Raw):
|
||||
@override
|
||||
def schema(self) -> dict[str, object]:
|
||||
return {"type": "object"}
|
||||
class SnippetTagResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
type: str
|
||||
|
||||
|
||||
tag_fields = {"id": fields.String, "name": fields.String, "type": fields.String}
|
||||
class SnippetListItemResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str | None
|
||||
type: SnippetType
|
||||
version: int
|
||||
use_count: int
|
||||
is_published: bool
|
||||
icon_info: dict[str, Any] | None
|
||||
tags: list[SnippetTagResponse]
|
||||
created_by: str | None
|
||||
author_name: str | None
|
||||
created_at: int
|
||||
updated_by: str | None
|
||||
updated_at: int
|
||||
|
||||
# Snippet list item fields (lightweight for list display)
|
||||
snippet_list_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"description": fields.String,
|
||||
"type": fields.String,
|
||||
"version": fields.Integer,
|
||||
"use_count": fields.Integer,
|
||||
"is_published": fields.Boolean,
|
||||
"icon_info": OpaqueRawField,
|
||||
"tags": fields.List(fields.Nested(tag_fields)),
|
||||
"created_by": fields.String,
|
||||
"author_name": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
}
|
||||
@field_validator("created_at", "updated_at", mode="before")
|
||||
@classmethod
|
||||
def _normalize_timestamp(cls, value: datetime | int | None) -> int:
|
||||
timestamp = to_timestamp(value)
|
||||
if timestamp is None:
|
||||
raise ValueError("timestamp is required")
|
||||
return timestamp
|
||||
|
||||
# Full snippet fields (includes creator info and graph data)
|
||||
snippet_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"description": fields.String,
|
||||
"type": fields.String,
|
||||
"version": fields.Integer,
|
||||
"use_count": fields.Integer,
|
||||
"is_published": fields.Boolean,
|
||||
"icon_info": OpaqueRawField,
|
||||
"graph": OpaqueRawField(attribute="graph_dict"),
|
||||
"input_fields": OpaqueRawField(attribute="input_fields_list"),
|
||||
"tags": fields.List(fields.Nested(tag_fields)),
|
||||
"created_by": fields.Nested(simple_account_fields, attribute="created_by_account", allow_null=True),
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.Nested(simple_account_fields, attribute="updated_by_account", allow_null=True),
|
||||
"updated_at": TimestampField,
|
||||
}
|
||||
|
||||
# Pagination response fields
|
||||
snippet_pagination_fields = {
|
||||
"data": fields.List(fields.Nested(snippet_list_fields)),
|
||||
"page": fields.Integer,
|
||||
"limit": fields.Integer,
|
||||
"total": fields.Integer,
|
||||
"has_more": fields.Boolean,
|
||||
}
|
||||
class SnippetResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str | None
|
||||
type: SnippetType
|
||||
version: int
|
||||
use_count: int
|
||||
is_published: bool
|
||||
icon_info: dict[str, Any] | None
|
||||
graph: dict[str, Any] = Field(validation_alias="graph_dict")
|
||||
input_fields: list[dict[str, Any]] = Field(validation_alias="input_fields_list")
|
||||
tags: list[SnippetTagResponse]
|
||||
created_by: SimpleAccountResponse | None = Field(validation_alias="created_by_account")
|
||||
created_at: int
|
||||
updated_by: SimpleAccountResponse | None = Field(validation_alias="updated_by_account")
|
||||
updated_at: int
|
||||
|
||||
@field_validator("created_at", "updated_at", mode="before")
|
||||
@classmethod
|
||||
def _normalize_timestamp(cls, value: datetime | int | None) -> int:
|
||||
timestamp = to_timestamp(value)
|
||||
if timestamp is None:
|
||||
raise ValueError("timestamp is required")
|
||||
return timestamp
|
||||
|
||||
|
||||
class SnippetPaginationResponse(ResponseModel):
|
||||
data: list[SnippetListItemResponse]
|
||||
page: int
|
||||
limit: int
|
||||
total: int
|
||||
has_more: bool
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
from typing import override
|
||||
|
||||
from flask_restx import fields
|
||||
|
||||
from core.helper import encrypter
|
||||
from fields.member_fields import simple_account_fields
|
||||
from graphon.variables import SecretVariable, SegmentType, VariableBase
|
||||
from libs.helper import TimestampField
|
||||
|
||||
from ._value_type_serializer import serialize_value_type
|
||||
|
||||
ENVIRONMENT_VARIABLE_SUPPORTED_TYPES = (SegmentType.STRING, SegmentType.NUMBER, SegmentType.SECRET)
|
||||
|
||||
|
||||
class OpaqueRawField(fields.Raw):
|
||||
@override
|
||||
def schema(self) -> dict[str, object]:
|
||||
return {"type": "object"}
|
||||
|
||||
|
||||
class JsonValueRawField(fields.Raw):
|
||||
@override
|
||||
def schema(self) -> dict[str, object]:
|
||||
return {
|
||||
"anyOf": [
|
||||
{"type": "string"},
|
||||
{"type": "integer"},
|
||||
{"type": "number"},
|
||||
{"type": "boolean"},
|
||||
{"type": "object", "additionalProperties": True},
|
||||
{"type": "array", "items": {}},
|
||||
{"type": "null"},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class EnvironmentVariableField(fields.Raw):
|
||||
@override
|
||||
def schema(self) -> dict[str, object]:
|
||||
return {"type": "object"}
|
||||
|
||||
@override
|
||||
def format(self, value):
|
||||
# Mask secret variables values in environment_variables
|
||||
if isinstance(value, SecretVariable):
|
||||
return {
|
||||
"id": value.id,
|
||||
"name": value.name,
|
||||
"value": encrypter.full_mask_token(),
|
||||
"value_type": value.value_type.value,
|
||||
"description": value.description,
|
||||
}
|
||||
if isinstance(value, VariableBase):
|
||||
return {
|
||||
"id": value.id,
|
||||
"name": value.name,
|
||||
"value": value.value,
|
||||
"value_type": str(value.value_type.exposed_type()),
|
||||
"description": value.description,
|
||||
}
|
||||
if isinstance(value, dict):
|
||||
value_type_str = value.get("value_type")
|
||||
if not isinstance(value_type_str, str):
|
||||
raise TypeError(
|
||||
f"unexpected type for value_type field, value={value_type_str}, type={type(value_type_str)}"
|
||||
)
|
||||
value_type = SegmentType(value_type_str).exposed_type()
|
||||
if value_type not in ENVIRONMENT_VARIABLE_SUPPORTED_TYPES:
|
||||
raise ValueError(f"Unsupported environment variable value type: {value_type}")
|
||||
return value
|
||||
|
||||
|
||||
conversation_variable_fields = {
|
||||
"id": fields.String,
|
||||
"name": fields.String,
|
||||
"value_type": fields.String(attribute=serialize_value_type),
|
||||
"value": JsonValueRawField,
|
||||
"description": fields.String,
|
||||
}
|
||||
|
||||
pipeline_variable_fields = {
|
||||
"label": fields.String,
|
||||
"variable": fields.String,
|
||||
"type": fields.String,
|
||||
"belong_to_node_id": fields.String,
|
||||
"max_length": fields.Integer,
|
||||
"required": fields.Boolean,
|
||||
"unit": fields.String,
|
||||
"default_value": JsonValueRawField,
|
||||
"options": fields.List(fields.String),
|
||||
"placeholder": fields.String,
|
||||
"tooltips": fields.String,
|
||||
"allowed_file_types": fields.List(fields.String),
|
||||
"allow_file_extension": fields.List(fields.String),
|
||||
"allow_file_upload_methods": fields.List(fields.String),
|
||||
}
|
||||
|
||||
workflow_fields = {
|
||||
"id": fields.String,
|
||||
"graph": OpaqueRawField(attribute="graph_dict"),
|
||||
"features": OpaqueRawField(attribute="features_dict"),
|
||||
"hash": fields.String(attribute="unique_hash"),
|
||||
"version": fields.String,
|
||||
"marked_name": fields.String,
|
||||
"marked_comment": fields.String,
|
||||
"created_by": fields.Nested(simple_account_fields, attribute="created_by_account"),
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.Nested(simple_account_fields, attribute="updated_by_account", allow_null=True),
|
||||
"updated_at": TimestampField,
|
||||
"tool_published": fields.Boolean,
|
||||
"environment_variables": fields.List(EnvironmentVariableField()),
|
||||
"conversation_variables": fields.List(fields.Nested(conversation_variable_fields)),
|
||||
"rag_pipeline_variables": fields.List(fields.Nested(pipeline_variable_fields)),
|
||||
}
|
||||
|
||||
workflow_partial_fields = {
|
||||
"id": fields.String,
|
||||
"created_by": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
}
|
||||
|
||||
workflow_pagination_fields = {
|
||||
"items": fields.List(fields.Nested(workflow_fields), attribute="items"),
|
||||
"page": fields.Integer,
|
||||
"limit": fields.Integer(attribute="limit"),
|
||||
"has_more": fields.Boolean(attribute="has_more"),
|
||||
}
|
||||
+52
-41
@@ -369,17 +369,19 @@ class InvitationCode(TypeBase):
|
||||
)
|
||||
|
||||
|
||||
class TenantPluginInstallPermission(enum.StrEnum):
|
||||
EVERYONE = "everyone"
|
||||
ADMINS = "admins"
|
||||
NOBODY = "noone"
|
||||
|
||||
|
||||
class TenantPluginDebugPermission(enum.StrEnum):
|
||||
EVERYONE = "everyone"
|
||||
ADMINS = "admins"
|
||||
NOBODY = "noone"
|
||||
|
||||
|
||||
class TenantPluginPermission(TypeBase):
|
||||
class InstallPermission(enum.StrEnum):
|
||||
EVERYONE = "everyone"
|
||||
ADMINS = "admins"
|
||||
NOBODY = "noone"
|
||||
|
||||
class DebugPermission(enum.StrEnum):
|
||||
EVERYONE = "everyone"
|
||||
ADMINS = "admins"
|
||||
NOBODY = "noone"
|
||||
|
||||
__tablename__ = "account_plugin_permissions"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="account_plugin_permission_pkey"),
|
||||
@@ -390,36 +392,42 @@ class TenantPluginPermission(TypeBase):
|
||||
StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False
|
||||
)
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
install_permission: Mapped[InstallPermission] = mapped_column(
|
||||
EnumText(InstallPermission, length=16),
|
||||
install_permission: Mapped[TenantPluginInstallPermission] = mapped_column(
|
||||
EnumText(TenantPluginInstallPermission, length=16),
|
||||
nullable=False,
|
||||
server_default="everyone",
|
||||
default=InstallPermission.EVERYONE,
|
||||
default=TenantPluginInstallPermission.EVERYONE,
|
||||
)
|
||||
debug_permission: Mapped[DebugPermission] = mapped_column(
|
||||
EnumText(DebugPermission, length=16), nullable=False, server_default="noone", default=DebugPermission.NOBODY
|
||||
debug_permission: Mapped[TenantPluginDebugPermission] = mapped_column(
|
||||
EnumText(TenantPluginDebugPermission, length=16),
|
||||
nullable=False,
|
||||
server_default="noone",
|
||||
default=TenantPluginDebugPermission.NOBODY,
|
||||
)
|
||||
|
||||
|
||||
class TenantPluginAutoUpgradeCategory(enum.StrEnum):
|
||||
TOOL = "tool"
|
||||
MODEL = "model"
|
||||
EXTENSION = "extension"
|
||||
AGENT_STRATEGY = "agent-strategy"
|
||||
DATASOURCE = "datasource"
|
||||
TRIGGER = "trigger"
|
||||
|
||||
|
||||
class TenantPluginAutoUpgradeStrategySetting(enum.StrEnum):
|
||||
DISABLED = "disabled"
|
||||
FIX_ONLY = "fix_only"
|
||||
LATEST = "latest"
|
||||
|
||||
|
||||
class TenantPluginAutoUpgradeMode(enum.StrEnum):
|
||||
ALL = "all"
|
||||
PARTIAL = "partial"
|
||||
EXCLUDE = "exclude"
|
||||
|
||||
|
||||
class TenantPluginAutoUpgradeStrategy(TypeBase):
|
||||
class PluginCategory(enum.StrEnum):
|
||||
TOOL = "tool"
|
||||
MODEL = "model"
|
||||
EXTENSION = "extension"
|
||||
AGENT_STRATEGY = "agent-strategy"
|
||||
DATASOURCE = "datasource"
|
||||
TRIGGER = "trigger"
|
||||
|
||||
class StrategySetting(enum.StrEnum):
|
||||
DISABLED = "disabled"
|
||||
FIX_ONLY = "fix_only"
|
||||
LATEST = "latest"
|
||||
|
||||
class UpgradeMode(enum.StrEnum):
|
||||
ALL = "all"
|
||||
PARTIAL = "partial"
|
||||
EXCLUDE = "exclude"
|
||||
|
||||
__tablename__ = "tenant_plugin_auto_upgrade_strategies"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="tenant_plugin_auto_upgrade_strategy_pkey"),
|
||||
@@ -431,20 +439,23 @@ class TenantPluginAutoUpgradeStrategy(TypeBase):
|
||||
StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False
|
||||
)
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
category: Mapped[PluginCategory] = mapped_column(
|
||||
EnumText(PluginCategory, length=32),
|
||||
category: Mapped[TenantPluginAutoUpgradeCategory] = mapped_column(
|
||||
EnumText(TenantPluginAutoUpgradeCategory, length=32),
|
||||
nullable=False,
|
||||
server_default="tool",
|
||||
default=PluginCategory.TOOL,
|
||||
default=TenantPluginAutoUpgradeCategory.TOOL,
|
||||
)
|
||||
strategy_setting: Mapped[StrategySetting] = mapped_column(
|
||||
EnumText(StrategySetting, length=16),
|
||||
strategy_setting: Mapped[TenantPluginAutoUpgradeStrategySetting] = mapped_column(
|
||||
EnumText(TenantPluginAutoUpgradeStrategySetting, length=16),
|
||||
nullable=False,
|
||||
server_default="fix_only",
|
||||
default=StrategySetting.FIX_ONLY,
|
||||
default=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
|
||||
)
|
||||
upgrade_mode: Mapped[UpgradeMode] = mapped_column(
|
||||
EnumText(UpgradeMode, length=16), nullable=False, server_default="exclude", default=UpgradeMode.EXCLUDE
|
||||
upgrade_mode: Mapped[TenantPluginAutoUpgradeMode] = mapped_column(
|
||||
EnumText(TenantPluginAutoUpgradeMode, length=16),
|
||||
nullable=False,
|
||||
server_default="exclude",
|
||||
default=TenantPluginAutoUpgradeMode.EXCLUDE,
|
||||
)
|
||||
exclude_plugins: Mapped[list[str]] = mapped_column(sa.JSON, nullable=False, default_factory=list)
|
||||
include_plugins: Mapped[list[str]] = mapped_column(sa.JSON, nullable=False, default_factory=list)
|
||||
|
||||
@@ -141,9 +141,9 @@ Get account avatar url
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [EducationActivateResponse](#educationactivateresponse)<br> |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### [GET] /account/education/autocomplete
|
||||
#### Parameters
|
||||
@@ -1411,7 +1411,7 @@ Infer CLI tool + ENV suggestions from a standardized Agent App skill
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [WorkspaceListResponse](#workspacelistresponse)<br> |
|
||||
| 200 | Success | **application/json**: [WorkspacePaginationResponse](#workspacepaginationresponse)<br> |
|
||||
|
||||
### [GET] /api-based-extension
|
||||
Get all API-based extensions for current tenant
|
||||
@@ -5106,14 +5106,14 @@ Refresh MCP server configuration and regenerate server code
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [DatasourceCredentialsResponse](#datasourcecredentialsresponse)<br> |
|
||||
| 200 | Default datasource credentials retrieved successfully | **application/json**: [DatasourceProviderAuthListResponse](#datasourceproviderauthlistresponse)<br> |
|
||||
|
||||
### [GET] /auth/plugin/datasource/list
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [DatasourceCredentialsResponse](#datasourcecredentialsresponse)<br> |
|
||||
| 200 | Datasource credentials retrieved successfully | **application/json**: [DatasourceProviderAuthListResponse](#datasourceproviderauthlistresponse)<br> |
|
||||
|
||||
### [GET] /auth/plugin/datasource/{provider_id}
|
||||
#### Parameters
|
||||
@@ -5126,7 +5126,7 @@ Refresh MCP server configuration and regenerate server code
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [DatasourceCredentialsResponse](#datasourcecredentialsresponse)<br> |
|
||||
| 200 | Datasource credentials retrieved successfully | **application/json**: [DatasourceCredentialListResponse](#datasourcecredentiallistresponse)<br> |
|
||||
|
||||
### [POST] /auth/plugin/datasource/{provider_id}
|
||||
#### Parameters
|
||||
@@ -5145,7 +5145,7 @@ Refresh MCP server configuration and regenerate server code
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
|
||||
| 200 | Datasource credential created successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
|
||||
|
||||
### [DELETE] /auth/plugin/datasource/{provider_id}/custom-client
|
||||
#### Parameters
|
||||
@@ -5177,7 +5177,7 @@ Refresh MCP server configuration and regenerate server code
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
|
||||
| 200 | Datasource OAuth custom client saved successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
|
||||
|
||||
### [POST] /auth/plugin/datasource/{provider_id}/default
|
||||
#### Parameters
|
||||
@@ -5234,7 +5234,7 @@ Refresh MCP server configuration and regenerate server code
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
|
||||
| 201 | Datasource credential updated successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
|
||||
|
||||
### [POST] /auth/plugin/datasource/{provider_id}/update-name
|
||||
#### Parameters
|
||||
@@ -7638,9 +7638,9 @@ Initiate OAuth login process
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 302 | Redirect to console OAuth callback page | **application/json**: [RedirectResponse](#redirectresponse)<br> |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 302 | Redirect to OAuth callback page |
|
||||
|
||||
### [GET] /oauth/plugin/{provider_id}/datasource/get-authorization-url
|
||||
#### Parameters
|
||||
@@ -7654,7 +7654,7 @@ Initiate OAuth login process
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Authorization URL retrieved successfully | **application/json**: [PluginOAuthAuthorizationUrlResponse](#pluginoauthauthorizationurlresponse)<br> |
|
||||
| 200 | Datasource OAuth authorization URL generated successfully | **application/json**: [PluginOAuthAuthorizationUrlResponse](#pluginoauthauthorizationurlresponse)<br> |
|
||||
|
||||
### [GET] /oauth/plugin/{provider}/tool/authorization-url
|
||||
#### Parameters
|
||||
@@ -8477,9 +8477,9 @@ Initiate OAuth login process
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [DataSourceContentPreviewResponse](#datasourcecontentpreviewresponse)<br> |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### [POST] /rag/pipelines/{pipeline_id}/workflows/published/datasource/nodes/{node_id}/run
|
||||
**Run rag pipeline datasource**
|
||||
@@ -8617,6 +8617,7 @@ Initiate OAuth login process
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
|
||||
| 401 | Unauthorized | **application/json**: [SimpleResultMessageResponse](#simpleresultmessageresponse)<br> |
|
||||
|
||||
### [POST] /remote-files/upload
|
||||
#### Request Body
|
||||
@@ -9432,9 +9433,9 @@ Bedrock retrieval test (internal use only)
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [GeneratedAppResponse](#generatedappresponse)<br> |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### [POST] /trial-apps/{app_id}/completion-messages
|
||||
#### Parameters
|
||||
@@ -9451,9 +9452,9 @@ Bedrock retrieval test (internal use only)
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [GeneratedAppResponse](#generatedappresponse)<br> |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### [GET] /trial-apps/{app_id}/datasets
|
||||
#### Parameters
|
||||
@@ -9568,9 +9569,9 @@ Returns the site configuration for the application including theme, icons, and t
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [GeneratedAppResponse](#generatedappresponse)<br> |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### [POST] /trial-apps/{app_id}/workflows/tasks/{task_id}/stop
|
||||
**Stop workflow task**
|
||||
@@ -9983,7 +9984,7 @@ Create a new plugin endpoint
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Endpoint created successfully | **application/json**: [EndpointCreateResponse](#endpointcreateresponse)<br> |
|
||||
| 200 | Endpoint created successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
|
||||
| 403 | Admin privileges required | |
|
||||
|
||||
### ~~[POST] /workspaces/current/endpoints/create~~
|
||||
@@ -10002,7 +10003,7 @@ Deprecated legacy alias for creating a plugin endpoint. Use POST /workspaces/cur
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Endpoint created successfully | **application/json**: [EndpointCreateResponse](#endpointcreateresponse)<br> |
|
||||
| 200 | Endpoint created successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
|
||||
| 403 | Admin privileges required | |
|
||||
|
||||
### ~~[POST] /workspaces/current/endpoints/delete~~
|
||||
@@ -10021,7 +10022,7 @@ Deprecated legacy alias for deleting a plugin endpoint. Use DELETE /workspaces/c
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Endpoint deleted successfully | **application/json**: [EndpointDeleteResponse](#endpointdeleteresponse)<br> |
|
||||
| 200 | Endpoint deleted successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
|
||||
| 403 | Admin privileges required | |
|
||||
|
||||
### [POST] /workspaces/current/endpoints/disable
|
||||
@@ -10037,7 +10038,7 @@ Disable a plugin endpoint
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Endpoint disabled successfully | **application/json**: [EndpointDisableResponse](#endpointdisableresponse)<br> |
|
||||
| 200 | Endpoint disabled successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
|
||||
| 403 | Admin privileges required | |
|
||||
|
||||
### [POST] /workspaces/current/endpoints/enable
|
||||
@@ -10053,7 +10054,7 @@ Enable a plugin endpoint
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Endpoint enabled successfully | **application/json**: [EndpointEnableResponse](#endpointenableresponse)<br> |
|
||||
| 200 | Endpoint enabled successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
|
||||
| 403 | Admin privileges required | |
|
||||
|
||||
### [GET] /workspaces/current/endpoints/list
|
||||
@@ -10087,7 +10088,7 @@ List endpoints for a specific plugin
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [PluginEndpointListResponse](#pluginendpointlistresponse)<br> |
|
||||
| 200 | Success | **application/json**: [EndpointListResponse](#endpointlistresponse)<br> |
|
||||
|
||||
### ~~[POST] /workspaces/current/endpoints/update~~
|
||||
|
||||
@@ -10105,7 +10106,7 @@ Deprecated legacy alias for updating a plugin endpoint. Use PATCH /workspaces/cu
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Endpoint updated successfully | **application/json**: [EndpointUpdateResponse](#endpointupdateresponse)<br> |
|
||||
| 200 | Endpoint updated successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
|
||||
| 403 | Admin privileges required | |
|
||||
|
||||
### [DELETE] /workspaces/current/endpoints/{id}
|
||||
@@ -10121,7 +10122,7 @@ Delete a plugin endpoint
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Endpoint deleted successfully | **application/json**: [EndpointDeleteResponse](#endpointdeleteresponse)<br> |
|
||||
| 200 | Endpoint deleted successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
|
||||
| 403 | Admin privileges required | |
|
||||
|
||||
### [PATCH] /workspaces/current/endpoints/{id}
|
||||
@@ -10143,7 +10144,7 @@ Update a plugin endpoint
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Endpoint updated successfully | **application/json**: [EndpointUpdateResponse](#endpointupdateresponse)<br> |
|
||||
| 200 | Endpoint updated successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
|
||||
| 403 | Admin privileges required | |
|
||||
|
||||
### [GET] /workspaces/current/members
|
||||
@@ -10203,7 +10204,7 @@ Update a plugin endpoint
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [MemberActionTenantResponse](#memberactiontenantresponse)<br> |
|
||||
| 200 | Success | **application/json**: [MemberActionResponse](#memberactionresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current/members/{member_id}/owner-transfer
|
||||
#### Parameters
|
||||
@@ -12471,9 +12472,15 @@ Returns permission flags that control workspace features like member invitations
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [WorkspaceMutationResponse](#workspacemutationresponse)<br> |
|
||||
| 200 | Success | **application/json**: [WorkspaceTenantResultResponse](#workspacetenantresultresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/custom-config/webapp-logo/upload
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **multipart/form-data**: { **"file"**: binary }<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
@@ -12491,7 +12498,7 @@ Returns permission flags that control workspace features like member invitations
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [WorkspaceMutationResponse](#workspacemutationresponse)<br> |
|
||||
| 200 | Success | **application/json**: [WorkspaceTenantResultResponse](#workspacetenantresultresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/switch
|
||||
#### Request Body
|
||||
@@ -16103,12 +16110,6 @@ Model class for provider custom model configuration.
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| info_list | [InfoList](#infolist) | | Yes |
|
||||
|
||||
#### DataSourceContentPreviewResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| DataSourceContentPreviewResponse | | | |
|
||||
|
||||
#### DataSourceIntegrateIconResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -16629,32 +16630,43 @@ Model class for provider custom model configuration.
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| credential_id | string | | Yes |
|
||||
|
||||
#### DatasourceCredentialListResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | [ [DatasourceCredentialResponse](#datasourcecredentialresponse) ] | | Yes |
|
||||
|
||||
#### DatasourceCredentialPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| credentials | object | | Yes |
|
||||
| credentials | object | Plugin-defined credential parameters. The schema is declared by the datasource provider. | Yes |
|
||||
| name | string | | No |
|
||||
|
||||
#### DatasourceCredentialResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| avatar_url | string | | Yes |
|
||||
| credential | object | Obfuscated plugin-defined credential parameters from the datasource provider. | Yes |
|
||||
| id | string | | Yes |
|
||||
| is_default | boolean | | Yes |
|
||||
| name | string | | Yes |
|
||||
| type | string | | Yes |
|
||||
|
||||
#### DatasourceCredentialUpdatePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| credential_id | string | | Yes |
|
||||
| credentials | object | | No |
|
||||
| credentials | object | Plugin-defined credential parameters. The schema is declared by the datasource provider. | No |
|
||||
| name | string | | No |
|
||||
|
||||
#### DatasourceCredentialsResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | | | Yes |
|
||||
|
||||
#### DatasourceCustomClientPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| client_params | object | | No |
|
||||
| client_params | object | Plugin-defined OAuth client parameters. The schema is declared by the datasource provider. | No |
|
||||
| enable_oauth_custom_client | boolean | | No |
|
||||
|
||||
#### DatasourceDefaultPayload
|
||||
@@ -16686,6 +16698,39 @@ Model class for provider custom model configuration.
|
||||
| error | string | Error message from OAuth provider | No |
|
||||
| state | string | OAuth state parameter | No |
|
||||
|
||||
#### DatasourceOAuthSchemaResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| client_schema | [ [ProviderConfig](#providerconfig) ] | | Yes |
|
||||
| credentials_schema | [ [ProviderConfig](#providerconfig) ] | | Yes |
|
||||
| is_oauth_custom_client_enabled | boolean | | Yes |
|
||||
| is_system_oauth_params_exists | boolean | | Yes |
|
||||
| oauth_custom_client_params | object | Masked plugin-defined OAuth client parameters, when configured for the tenant. | Yes |
|
||||
| redirect_uri | string | | Yes |
|
||||
|
||||
#### DatasourceProviderAuthListResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | [ [DatasourceProviderAuthResponse](#datasourceproviderauthresponse) ] | | Yes |
|
||||
|
||||
#### DatasourceProviderAuthResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| author | string | | Yes |
|
||||
| credential_schema | [ [ProviderConfig](#providerconfig) ] | | Yes |
|
||||
| credentials_list | [ [DatasourceCredentialResponse](#datasourcecredentialresponse) ] | | Yes |
|
||||
| description | [I18nObject](#i18nobject) | | Yes |
|
||||
| icon | string | | Yes |
|
||||
| label | [I18nObject](#i18nobject) | | Yes |
|
||||
| name | string | | Yes |
|
||||
| oauth_schema | [DatasourceOAuthSchemaResponse](#datasourceoauthschemaresponse) | | Yes |
|
||||
| plugin_id | string | | Yes |
|
||||
| plugin_unique_identifier | string | | Yes |
|
||||
| provider | string | | Yes |
|
||||
|
||||
#### DatasourceUpdateNamePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -16702,12 +16747,6 @@ Model class for provider custom model configuration.
|
||||
| start_node_id | string | | Yes |
|
||||
| start_node_title | string | | Yes |
|
||||
|
||||
#### DebugPermission
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| DebugPermission | string | | |
|
||||
|
||||
#### DeclaredArrayItem
|
||||
|
||||
Per-item shape for an ``array``-typed declared output.
|
||||
@@ -17034,12 +17073,6 @@ Request payload for bulk downloading documents as a zip archive.
|
||||
| role | string | | Yes |
|
||||
| token | string | | Yes |
|
||||
|
||||
#### EducationActivateResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| EducationActivateResponse | object | | |
|
||||
|
||||
#### EducationAutocompleteQuery
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -17149,29 +17182,13 @@ Request payload for bulk downloading documents as a zip archive.
|
||||
| plugin_unique_identifier | string | | Yes |
|
||||
| settings | object | | Yes |
|
||||
|
||||
#### EndpointCreateResponse
|
||||
#### EndpointDeclarationResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| success | boolean | Operation success | Yes |
|
||||
|
||||
#### EndpointDeleteResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| success | boolean | Operation success | Yes |
|
||||
|
||||
#### EndpointDisableResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| success | boolean | Operation success | Yes |
|
||||
|
||||
#### EndpointEnableResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| success | boolean | Operation success | Yes |
|
||||
| hidden | boolean | | No |
|
||||
| method | string | | Yes |
|
||||
| path | string | | Yes |
|
||||
|
||||
#### EndpointIdPayload
|
||||
|
||||
@@ -17187,6 +17204,23 @@ Request payload for bulk downloading documents as a zip archive.
|
||||
| page_size | integer | | Yes |
|
||||
| plugin_id | string | | Yes |
|
||||
|
||||
#### EndpointListItemResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| created_at | dateTime | | Yes |
|
||||
| declaration | [EndpointProviderDeclarationResponse](#endpointproviderdeclarationresponse) | | No |
|
||||
| enabled | boolean | | Yes |
|
||||
| expired_at | dateTime | | Yes |
|
||||
| hook_id | string | | Yes |
|
||||
| id | string | | Yes |
|
||||
| name | string | | Yes |
|
||||
| plugin_id | string | | Yes |
|
||||
| settings | object | | Yes |
|
||||
| tenant_id | string | | Yes |
|
||||
| updated_at | dateTime | | Yes |
|
||||
| url | string | | Yes |
|
||||
|
||||
#### EndpointListQuery
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -17198,7 +17232,59 @@ Request payload for bulk downloading documents as a zip archive.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| endpoints | [ object ] | Endpoint information | Yes |
|
||||
| endpoints | [ [EndpointListItemResponse](#endpointlistitemresponse) ] | Endpoint information | Yes |
|
||||
|
||||
#### EndpointProviderConfigI18nResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| en_US | string | | Yes |
|
||||
| ja_JP | string | | No |
|
||||
| pt_BR | string | | No |
|
||||
| zh_Hans | string | | No |
|
||||
|
||||
#### EndpointProviderConfigOptionResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| label | [EndpointProviderConfigI18nResponse](#endpointproviderconfigi18nresponse) | | Yes |
|
||||
| value | string | | Yes |
|
||||
|
||||
#### EndpointProviderConfigResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| default | integer<br>string<br>number<br>boolean | | No |
|
||||
| help | [EndpointProviderConfigI18nResponse](#endpointproviderconfigi18nresponse) | | No |
|
||||
| label | [EndpointProviderConfigI18nResponse](#endpointproviderconfigi18nresponse) | | No |
|
||||
| multiple | boolean | | No |
|
||||
| name | string | | Yes |
|
||||
| options | [ [EndpointProviderConfigOptionResponse](#endpointproviderconfigoptionresponse) ] | | No |
|
||||
| placeholder | [EndpointProviderConfigI18nResponse](#endpointproviderconfigi18nresponse) | | No |
|
||||
| required | boolean | | No |
|
||||
| scope | [EndpointProviderConfigScope](#endpointproviderconfigscope) | | No |
|
||||
| type | [ProviderConfigType](#providerconfigtype) | | Yes |
|
||||
| url | string | | No |
|
||||
|
||||
#### EndpointProviderConfigScope
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| EndpointProviderConfigScope | string | | |
|
||||
|
||||
#### EndpointProviderDeclarationResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| endpoints | [ [EndpointDeclarationResponse](#endpointdeclarationresponse) ] | | No |
|
||||
| settings | [ [EndpointProviderConfigResponse](#endpointproviderconfigresponse) ] | | No |
|
||||
|
||||
#### EndpointSettingsPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| name | string | | Yes |
|
||||
| settings | object | | Yes |
|
||||
|
||||
#### EndpointUpdatePayload
|
||||
|
||||
@@ -17207,12 +17293,6 @@ Request payload for bulk downloading documents as a zip archive.
|
||||
| name | string | | Yes |
|
||||
| settings | object | | Yes |
|
||||
|
||||
#### EndpointUpdateResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| success | boolean | Operation success | Yes |
|
||||
|
||||
#### EnvSuggestion
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -18010,12 +18090,6 @@ Input field definition for snippet parameters.
|
||||
| required | boolean | | No |
|
||||
| type | string | | No |
|
||||
|
||||
#### InstallPermission
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| InstallPermission | string | | |
|
||||
|
||||
#### InstalledAppCreatePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -18321,7 +18395,7 @@ Enum class for large language model mode.
|
||||
| marketplace_plugin_unique_identifier | string | | Yes |
|
||||
| version | string | | No |
|
||||
|
||||
#### MemberActionTenantResponse
|
||||
#### MemberActionResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
@@ -19092,13 +19166,13 @@ Enum class for parameter type.
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| auto_upgrade | [PluginAutoUpgradeSettingsPayload](#pluginautoupgradesettingspayload) | | Yes |
|
||||
| category | [PluginCategory](#plugincategory) | | Yes |
|
||||
| category | [TenantPluginAutoUpgradeCategory](#tenantpluginautoupgradecategory) | | Yes |
|
||||
|
||||
#### ParserAutoUpgradeFetch
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| category | [PluginCategory](#plugincategory) | | Yes |
|
||||
| category | [TenantPluginAutoUpgradeCategory](#tenantpluginautoupgradecategory) | | Yes |
|
||||
|
||||
#### ParserCreateCredential
|
||||
|
||||
@@ -19196,7 +19270,7 @@ Enum class for parameter type.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| category | [PluginCategory](#plugincategory) | | Yes |
|
||||
| category | [TenantPluginAutoUpgradeCategory](#tenantpluginautoupgradecategory) | | Yes |
|
||||
| plugin_id | string | | Yes |
|
||||
|
||||
#### ParserGetCredentials
|
||||
@@ -19284,8 +19358,8 @@ Enum class for parameter type.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| debug_permission | [DebugPermission](#debugpermission) | | No |
|
||||
| install_permission | [InstallPermission](#installpermission) | | No |
|
||||
| debug_permission | [TenantPluginDebugPermission](#tenantplugindebugpermission) | | No |
|
||||
| install_permission | [TenantPluginInstallPermission](#tenantplugininstallpermission) | | No |
|
||||
|
||||
#### ParserPluginIdentifierQuery
|
||||
|
||||
@@ -19494,7 +19568,7 @@ Shared permission levels for resources (datasets, credentials, etc.)
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| auto_upgrade | [PluginAutoUpgradeSettingsResponseModel](#pluginautoupgradesettingsresponsemodel) | | Yes |
|
||||
| category | [PluginCategory](#plugincategory) | | Yes |
|
||||
| category | [TenantPluginAutoUpgradeCategory](#tenantpluginautoupgradecategory) | | Yes |
|
||||
|
||||
#### PluginAutoUpgradeSettingsPayload
|
||||
|
||||
@@ -19502,8 +19576,8 @@ Shared permission levels for resources (datasets, credentials, etc.)
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| exclude_plugins | [ string ] | | No |
|
||||
| include_plugins | [ string ] | | No |
|
||||
| strategy_setting | [StrategySetting](#strategysetting) | | No |
|
||||
| upgrade_mode | [UpgradeMode](#upgrademode) | | No |
|
||||
| strategy_setting | [TenantPluginAutoUpgradeStrategySetting](#tenantpluginautoupgradestrategysetting) | | No |
|
||||
| upgrade_mode | [TenantPluginAutoUpgradeMode](#tenantpluginautoupgrademode) | | No |
|
||||
| upgrade_time_of_day | integer | | No |
|
||||
|
||||
#### PluginAutoUpgradeSettingsResponseModel
|
||||
@@ -19512,8 +19586,8 @@ Shared permission levels for resources (datasets, credentials, etc.)
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| exclude_plugins | [ string ] | | Yes |
|
||||
| include_plugins | [ string ] | | Yes |
|
||||
| strategy_setting | [StrategySetting](#strategysetting) | | Yes |
|
||||
| upgrade_mode | [UpgradeMode](#upgrademode) | | Yes |
|
||||
| strategy_setting | [TenantPluginAutoUpgradeStrategySetting](#tenantpluginautoupgradestrategysetting) | | Yes |
|
||||
| upgrade_mode | [TenantPluginAutoUpgradeMode](#tenantpluginautoupgrademode) | | Yes |
|
||||
| upgrade_time_of_day | integer | | Yes |
|
||||
|
||||
#### PluginCategory
|
||||
@@ -19635,21 +19709,21 @@ Shared permission levels for resources (datasets, credentials, etc.)
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| current_identifier | string | | No |
|
||||
| type | [Type](#type) | | Yes |
|
||||
| type | [PluginDependencyType](#plugindependencytype) | | Yes |
|
||||
| value | [Github](#github)<br>[Marketplace](#marketplace)<br>[Package](#package) | | Yes |
|
||||
|
||||
#### PluginDependencyType
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| PluginDependencyType | string | | |
|
||||
|
||||
#### PluginDynamicOptionsResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| options | | | Yes |
|
||||
|
||||
#### PluginEndpointListResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| endpoints | [ object ] | Endpoint information | Yes |
|
||||
|
||||
#### PluginInstallationItemResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -19730,7 +19804,13 @@ Shared permission levels for resources (datasets, credentials, etc.)
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| type | [core__plugin__entities__parameters__PluginParameterAutoGenerate__Type](#core__plugin__entities__parameters__pluginparameterautogenerate__type) | | Yes |
|
||||
| type | [PluginParameterAutoGenerateType](#pluginparameterautogeneratetype) | | Yes |
|
||||
|
||||
#### PluginParameterAutoGenerateType
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| PluginParameterAutoGenerateType | string | | |
|
||||
|
||||
#### PluginParameterOption
|
||||
|
||||
@@ -19750,15 +19830,15 @@ Shared permission levels for resources (datasets, credentials, etc.)
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| debug_permission | [DebugPermission](#debugpermission) | | Yes |
|
||||
| install_permission | [InstallPermission](#installpermission) | | Yes |
|
||||
| debug_permission | [TenantPluginDebugPermission](#tenantplugindebugpermission) | | Yes |
|
||||
| install_permission | [TenantPluginInstallPermission](#tenantplugininstallpermission) | | Yes |
|
||||
|
||||
#### PluginPermissionSettingsPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| debug_permission | [DebugPermission](#debugpermission) | | No |
|
||||
| install_permission | [InstallPermission](#installpermission) | | No |
|
||||
| debug_permission | [TenantPluginDebugPermission](#tenantplugindebugpermission) | | No |
|
||||
| install_permission | [TenantPluginInstallPermission](#tenantplugininstallpermission) | | No |
|
||||
|
||||
#### PluginReadmeResponse
|
||||
|
||||
@@ -19840,9 +19920,15 @@ Model class for common provider settings like credentials
|
||||
| placeholder | [I18nObject](#i18nobject) | | No |
|
||||
| required | boolean | | No |
|
||||
| scope | [AppSelectorScope](#appselectorscope)<br>[ModelSelectorScope](#modelselectorscope)<br>[ToolSelectorScope](#toolselectorscope) | | No |
|
||||
| type | [core__entities__provider_entities__BasicProviderConfig__Type](#core__entities__provider_entities__basicproviderconfig__type) | The type of the credentials | Yes |
|
||||
| type | [ProviderConfigType](#providerconfigtype) | The type of the credentials | Yes |
|
||||
| url | string | | No |
|
||||
|
||||
#### ProviderConfigType
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| ProviderConfigType | string | | |
|
||||
|
||||
#### ProviderCredentialResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -20998,12 +21084,6 @@ Query parameters for listing snippet published workflows.
|
||||
| paused | integer | | Yes |
|
||||
| success | integer | | Yes |
|
||||
|
||||
#### StrategySetting
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| StrategySetting | string | | |
|
||||
|
||||
#### StringListSource
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -21258,6 +21338,36 @@ Tag type
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| workspaces | [ [TenantListItemResponse](#tenantlistitemresponse) ] | | Yes |
|
||||
|
||||
#### TenantPluginAutoUpgradeCategory
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| TenantPluginAutoUpgradeCategory | string | | |
|
||||
|
||||
#### TenantPluginAutoUpgradeMode
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| TenantPluginAutoUpgradeMode | string | | |
|
||||
|
||||
#### TenantPluginAutoUpgradeStrategySetting
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| TenantPluginAutoUpgradeStrategySetting | string | | |
|
||||
|
||||
#### TenantPluginDebugPermission
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| TenantPluginDebugPermission | string | | |
|
||||
|
||||
#### TenantPluginInstallPermission
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| TenantPluginInstallPermission | string | | |
|
||||
|
||||
#### TextContentResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -21764,12 +21874,6 @@ Enum class for tool provider
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| TriggerSubscriptionListResponse | array | | |
|
||||
|
||||
#### Type
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| Type | string | | |
|
||||
|
||||
#### UnaddedModelConfiguration
|
||||
|
||||
Model class for provider unadded model configuration.
|
||||
@@ -21810,12 +21914,6 @@ Payload for updating a snippet.
|
||||
| icon_info | [IconInfo](#iconinfo) | | No |
|
||||
| name | string | | No |
|
||||
|
||||
#### UpgradeMode
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| UpgradeMode | string | | |
|
||||
|
||||
#### UploadConfig
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -22946,7 +23044,13 @@ Workflow tool configuration
|
||||
| limit | integer, <br>**Default:** 20 | | No |
|
||||
| page | integer, <br>**Default:** 1 | | No |
|
||||
|
||||
#### WorkspaceListResponse
|
||||
#### WorkspaceLogoUploadResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| id | string | | Yes |
|
||||
|
||||
#### WorkspacePaginationResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
@@ -22956,19 +23060,6 @@ Workflow tool configuration
|
||||
| page | integer | | Yes |
|
||||
| total | integer | | Yes |
|
||||
|
||||
#### WorkspaceLogoUploadResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| id | string | | Yes |
|
||||
|
||||
#### WorkspaceMutationResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | string | | Yes |
|
||||
| tenant | [TenantInfoResponse](#tenantinforesponse) | | Yes |
|
||||
|
||||
#### WorkspacePermissionResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -22983,6 +23074,13 @@ Workflow tool configuration
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| permission_keys | [ string ] | | No |
|
||||
|
||||
#### WorkspaceTenantResultResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | string | | Yes |
|
||||
| tenant | [TenantInfoResponse](#tenantinforesponse) | | Yes |
|
||||
|
||||
#### _AccessControlLanguageQuery
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -23051,18 +23149,6 @@ Workflow tool configuration
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| scope | [RBACResourceWhitelistScope](#rbacresourcewhitelistscope) | | Yes |
|
||||
|
||||
#### core__entities__provider_entities__BasicProviderConfig__Type
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| core__entities__provider_entities__BasicProviderConfig__Type | string | | |
|
||||
|
||||
#### core__plugin__entities__parameters__PluginParameterAutoGenerate__Type
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| core__plugin__entities__parameters__PluginParameterAutoGenerate__Type | string | | |
|
||||
|
||||
#### core__tools__entities__common_entities__I18nObject
|
||||
|
||||
Model class for i18n object.
|
||||
|
||||
@@ -941,9 +941,15 @@ Strict (extra='forbid').
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| current_identifier | string | | No |
|
||||
| type | [Type](#type) | | Yes |
|
||||
| type | [PluginDependencyType](#plugindependencytype) | | Yes |
|
||||
| value | [Github](#github)<br>[Marketplace](#marketplace)<br>[Package](#package) | | Yes |
|
||||
|
||||
#### PluginDependencyType
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| PluginDependencyType | string | | |
|
||||
|
||||
#### RevokeResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -1024,12 +1030,6 @@ types it as a required `'success'` rather than an optional field.
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | string | | Yes |
|
||||
|
||||
#### Type
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| Type | string | | |
|
||||
|
||||
#### UsageInfo
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|
||||
@@ -1046,12 +1046,12 @@ Execute a single datasource node within the knowledge pipeline. Returns a stream
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Streaming response with node execution events. | **text/event-stream**: [GeneratedAppResponse](#generatedappresponse)<br> |
|
||||
| 401 | Unauthorized - invalid API token | |
|
||||
| 403 | Forbidden - dataset API access or workspace access denied | |
|
||||
| 404 | `not_found` : Dataset not found. | |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Streaming response with node execution events. |
|
||||
| 401 | Unauthorized - invalid API token |
|
||||
| 403 | Forbidden - dataset API access or workspace access denied |
|
||||
| 404 | `not_found` : Dataset not found. |
|
||||
|
||||
### [POST] /datasets/{dataset_id}/pipeline/run
|
||||
**Run Pipeline**
|
||||
@@ -2960,7 +2960,7 @@ Enum class for custom configuration status.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| credentials | [ [DatasourceCredentialInfoResponse](#datasourcecredentialinforesponse) ] | | Yes |
|
||||
| credentials | [ [DatasourceCredentialInfoResponse](#datasourcecredentialinforesponse) ] | | No |
|
||||
| datasource_type | string | | No |
|
||||
| node_id | string | | No |
|
||||
| plugin_id | string | | No |
|
||||
|
||||
@@ -600,7 +600,7 @@ Get authentication passport for web application access
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Passport retrieved successfully | **application/json**: [AccessTokenData](#accesstokendata)<br> |
|
||||
| 200 | Passport retrieved successfully | **application/json**: [PassportAccessTokenResponse](#passportaccesstokenresponse)<br> |
|
||||
| 401 | Unauthorized - missing app code or invalid authentication | |
|
||||
| 404 | Application or user not found | |
|
||||
|
||||
@@ -1430,6 +1430,12 @@ Form input definition.
|
||||
| text_to_speech | [JSONObject](#jsonobject) | | Yes |
|
||||
| user_input_form | [ [JSONObject](#jsonobject) ] | | Yes |
|
||||
|
||||
#### PassportAccessTokenResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| access_token | string | | Yes |
|
||||
|
||||
#### PassportQuery
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy import select
|
||||
import app
|
||||
from core.helper.marketplace import fetch_global_plugin_manifest
|
||||
from extensions.ext_database import db
|
||||
from models.account import TenantPluginAutoUpgradeStrategy
|
||||
from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginAutoUpgradeStrategySetting
|
||||
from tasks import process_tenant_plugin_autoupgrade_check_task as check_task
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -34,8 +34,7 @@ def check_upgradable_plugin_task():
|
||||
TenantPluginAutoUpgradeStrategy.upgrade_time_of_day >= now_seconds_of_day,
|
||||
TenantPluginAutoUpgradeStrategy.upgrade_time_of_day
|
||||
< now_seconds_of_day + AUTO_UPGRADE_MINIMAL_CHECKING_INTERVAL,
|
||||
TenantPluginAutoUpgradeStrategy.strategy_setting
|
||||
!= TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED,
|
||||
TenantPluginAutoUpgradeStrategy.strategy_setting != TenantPluginAutoUpgradeStrategySetting.DISABLED,
|
||||
)
|
||||
).all()
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ from models.account import (
|
||||
Tenant,
|
||||
TenantAccountJoin,
|
||||
TenantAccountRole,
|
||||
TenantPluginAutoUpgradeCategory,
|
||||
TenantPluginAutoUpgradeMode,
|
||||
TenantPluginAutoUpgradeStrategy,
|
||||
TenantStatus,
|
||||
)
|
||||
@@ -1257,13 +1259,13 @@ class TenantService:
|
||||
session.add(tenant)
|
||||
session.commit()
|
||||
|
||||
for category in TenantPluginAutoUpgradeStrategy.PluginCategory:
|
||||
for category in TenantPluginAutoUpgradeCategory:
|
||||
plugin_upgrade_strategy = TenantPluginAutoUpgradeStrategy(
|
||||
tenant_id=tenant.id,
|
||||
category=category,
|
||||
strategy_setting=PluginAutoUpgradeService.default_strategy_setting_for_category(category),
|
||||
upgrade_time_of_day=PluginAutoUpgradeService.default_upgrade_time_of_day(tenant.id),
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
upgrade_mode=TenantPluginAutoUpgradeMode.EXCLUDE,
|
||||
exclude_plugins=[],
|
||||
include_plugins=[],
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import re
|
||||
|
||||
from configs import dify_config
|
||||
from core.helper import marketplace
|
||||
from core.plugin.entities.plugin import PluginDependency, PluginInstallationSource
|
||||
from core.plugin.entities.plugin import PluginDependency, PluginDependencyType, PluginInstallationSource
|
||||
from core.plugin.impl.plugin import PluginInstaller
|
||||
from models.provider_ids import ModelProviderID, ToolProviderID
|
||||
|
||||
@@ -55,7 +55,7 @@ class DependenciesAnalysisService:
|
||||
unique_identifier = dependency.value.plugin_unique_identifier
|
||||
if unique_identifier in missing_plugin_unique_identifiers:
|
||||
# Extract version for Marketplace dependencies
|
||||
if dependency.type == PluginDependency.Type.Marketplace:
|
||||
if dependency.type == PluginDependencyType.Marketplace:
|
||||
version_match = _VERSION_REGEX.search(unique_identifier)
|
||||
if version_match:
|
||||
dependency.value.version = version_match.group("version")
|
||||
@@ -84,7 +84,7 @@ class DependenciesAnalysisService:
|
||||
if plugin.source == PluginInstallationSource.Github:
|
||||
result.append(
|
||||
PluginDependency(
|
||||
type=PluginDependency.Type.Github,
|
||||
type=PluginDependencyType.Github,
|
||||
value=PluginDependency.Github(
|
||||
repo=plugin.meta["repo"],
|
||||
version=plugin.meta["version"],
|
||||
@@ -96,7 +96,7 @@ class DependenciesAnalysisService:
|
||||
elif plugin.source == PluginInstallationSource.Marketplace:
|
||||
result.append(
|
||||
PluginDependency(
|
||||
type=PluginDependency.Type.Marketplace,
|
||||
type=PluginDependencyType.Marketplace,
|
||||
value=PluginDependency.Marketplace(
|
||||
marketplace_plugin_unique_identifier=plugin.plugin_unique_identifier
|
||||
),
|
||||
@@ -105,7 +105,7 @@ class DependenciesAnalysisService:
|
||||
elif plugin.source == PluginInstallationSource.Package:
|
||||
result.append(
|
||||
PluginDependency(
|
||||
type=PluginDependency.Type.Package,
|
||||
type=PluginDependencyType.Package,
|
||||
value=PluginDependency.Package(plugin_unique_identifier=plugin.plugin_unique_identifier),
|
||||
)
|
||||
)
|
||||
@@ -130,7 +130,7 @@ class DependenciesAnalysisService:
|
||||
deps = marketplace.batch_fetch_plugin_manifests(dependencies)
|
||||
return [
|
||||
PluginDependency(
|
||||
type=PluginDependency.Type.Marketplace,
|
||||
type=PluginDependencyType.Marketplace,
|
||||
value=PluginDependency.Marketplace(marketplace_plugin_unique_identifier=dep.latest_package_identifier),
|
||||
)
|
||||
for dep in deps
|
||||
|
||||
@@ -14,11 +14,16 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from core.db.session_factory import session_factory
|
||||
from core.plugin.impl.plugin import PluginInstaller
|
||||
from models.account import TenantPluginAutoUpgradeStrategy
|
||||
from models.account import (
|
||||
TenantPluginAutoUpgradeCategory,
|
||||
TenantPluginAutoUpgradeMode,
|
||||
TenantPluginAutoUpgradeStrategy,
|
||||
TenantPluginAutoUpgradeStrategySetting,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PluginCategory = TenantPluginAutoUpgradeStrategy.PluginCategory
|
||||
PluginCategory = TenantPluginAutoUpgradeCategory
|
||||
PLUGIN_CATEGORIES = tuple(PluginCategory)
|
||||
SECONDS_PER_DAY = 24 * 60 * 60
|
||||
AUTO_UPGRADE_CHECK_SLOT_SECONDS = 15 * 60
|
||||
@@ -35,10 +40,10 @@ class PluginAutoUpgradeService:
|
||||
@staticmethod
|
||||
def default_strategy_setting_for_category(
|
||||
category: PluginCategory,
|
||||
) -> TenantPluginAutoUpgradeStrategy.StrategySetting:
|
||||
) -> TenantPluginAutoUpgradeStrategySetting:
|
||||
if category == PluginCategory.MODEL:
|
||||
return TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
|
||||
return TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY
|
||||
return TenantPluginAutoUpgradeStrategySetting.LATEST
|
||||
return TenantPluginAutoUpgradeStrategySetting.FIX_ONLY
|
||||
|
||||
@staticmethod
|
||||
def default_upgrade_time_of_day(tenant_id: str) -> int:
|
||||
@@ -102,9 +107,9 @@ class PluginAutoUpgradeService:
|
||||
@staticmethod
|
||||
def _has_default_strategy(strategy: TenantPluginAutoUpgradeStrategy) -> bool:
|
||||
return (
|
||||
strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY
|
||||
strategy.strategy_setting == TenantPluginAutoUpgradeStrategySetting.FIX_ONLY
|
||||
and strategy.upgrade_time_of_day == 0
|
||||
and strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
|
||||
and strategy.upgrade_mode == TenantPluginAutoUpgradeMode.EXCLUDE
|
||||
and not strategy.exclude_plugins
|
||||
and not strategy.include_plugins
|
||||
)
|
||||
@@ -114,7 +119,7 @@ class PluginAutoUpgradeService:
|
||||
source_strategy: TenantPluginAutoUpgradeStrategy,
|
||||
category: PluginCategory,
|
||||
source_has_default_strategy: bool,
|
||||
) -> TenantPluginAutoUpgradeStrategy.StrategySetting:
|
||||
) -> TenantPluginAutoUpgradeStrategySetting:
|
||||
# Only pure legacy defaults adopt the new model=latest default. User-edited
|
||||
# strategies keep their original setting across all categories.
|
||||
if source_has_default_strategy:
|
||||
@@ -266,9 +271,9 @@ class PluginAutoUpgradeService:
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
category: PluginCategory,
|
||||
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting,
|
||||
strategy_setting: TenantPluginAutoUpgradeStrategySetting,
|
||||
upgrade_time_of_day: int,
|
||||
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
|
||||
upgrade_mode: TenantPluginAutoUpgradeMode,
|
||||
exclude_plugins: list[str],
|
||||
include_plugins: list[str],
|
||||
) -> None:
|
||||
@@ -294,9 +299,9 @@ class PluginAutoUpgradeService:
|
||||
@staticmethod
|
||||
def change_strategy(
|
||||
tenant_id: str,
|
||||
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting,
|
||||
strategy_setting: TenantPluginAutoUpgradeStrategySetting,
|
||||
upgrade_time_of_day: int,
|
||||
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
|
||||
upgrade_mode: TenantPluginAutoUpgradeMode,
|
||||
exclude_plugins: list[str],
|
||||
include_plugins: list[str],
|
||||
category: PluginCategory,
|
||||
@@ -329,28 +334,28 @@ class PluginAutoUpgradeService:
|
||||
session,
|
||||
tenant_id,
|
||||
category,
|
||||
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
|
||||
0,
|
||||
TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
TenantPluginAutoUpgradeMode.EXCLUDE,
|
||||
[plugin_id],
|
||||
[],
|
||||
)
|
||||
else:
|
||||
if exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE:
|
||||
if exist_strategy.upgrade_mode == TenantPluginAutoUpgradeMode.EXCLUDE:
|
||||
# In exclude mode, disabling one plugin means adding it to exclude_plugins.
|
||||
if plugin_id not in exist_strategy.exclude_plugins:
|
||||
new_exclude_plugins = exist_strategy.exclude_plugins.copy()
|
||||
new_exclude_plugins.append(plugin_id)
|
||||
exist_strategy.exclude_plugins = new_exclude_plugins
|
||||
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL:
|
||||
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeMode.PARTIAL:
|
||||
# In partial mode, disabling one plugin means removing it from include_plugins.
|
||||
if plugin_id in exist_strategy.include_plugins:
|
||||
new_include_plugins = exist_strategy.include_plugins.copy()
|
||||
new_include_plugins.remove(plugin_id)
|
||||
exist_strategy.include_plugins = new_include_plugins
|
||||
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
|
||||
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeMode.ALL:
|
||||
# In all mode, switch to exclude mode so only this plugin is skipped.
|
||||
exist_strategy.upgrade_mode = TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
|
||||
exist_strategy.upgrade_mode = TenantPluginAutoUpgradeMode.EXCLUDE
|
||||
exist_strategy.exclude_plugins = [plugin_id]
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import select
|
||||
|
||||
from core.db.session_factory import session_factory
|
||||
from models.account import TenantPluginPermission
|
||||
from models.account import TenantPluginDebugPermission, TenantPluginInstallPermission, TenantPluginPermission
|
||||
|
||||
|
||||
class PluginPermissionService:
|
||||
@@ -15,8 +15,8 @@ class PluginPermissionService:
|
||||
@staticmethod
|
||||
def change_permission(
|
||||
tenant_id: str,
|
||||
install_permission: TenantPluginPermission.InstallPermission,
|
||||
debug_permission: TenantPluginPermission.DebugPermission,
|
||||
install_permission: TenantPluginInstallPermission,
|
||||
debug_permission: TenantPluginDebugPermission,
|
||||
):
|
||||
with session_factory.create_session() as session, session.begin():
|
||||
permission = session.scalar(
|
||||
|
||||
@@ -73,7 +73,7 @@ class TagService:
|
||||
target must be bound to all requested tags.
|
||||
"""
|
||||
# Check if tag_ids is not empty to avoid WHERE false condition
|
||||
if not tag_ids or len(tag_ids) == 0:
|
||||
if not tag_ids:
|
||||
return []
|
||||
# Deduplicate repeated query params so match_all counts each requested tag once.
|
||||
requested_tag_ids = list(dict.fromkeys(tag_ids))
|
||||
@@ -88,7 +88,7 @@ class TagService:
|
||||
return []
|
||||
tag_ids = list(tags)
|
||||
# Check if tag_ids is not empty to avoid WHERE false condition
|
||||
if not tag_ids or len(tag_ids) == 0:
|
||||
if not tag_ids:
|
||||
return []
|
||||
if match_all:
|
||||
if len(tag_ids) != len(requested_tag_ids):
|
||||
|
||||
@@ -6,7 +6,7 @@ from httpx import get
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from core.entities.provider_entities import ProviderConfig
|
||||
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
|
||||
from core.tools.__base.tool_runtime import ToolRuntime
|
||||
from core.tools.custom_tool.provider import ApiToolProviderController
|
||||
from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity
|
||||
@@ -52,7 +52,7 @@ class ApiToolManageService:
|
||||
credentials_schema = [
|
||||
ProviderConfig(
|
||||
name="auth_type",
|
||||
type=ProviderConfig.Type.SELECT,
|
||||
type=ProviderConfigType.SELECT,
|
||||
required=True,
|
||||
default="none",
|
||||
options=[
|
||||
@@ -63,7 +63,7 @@ class ApiToolManageService:
|
||||
),
|
||||
ProviderConfig(
|
||||
name="api_key_header",
|
||||
type=ProviderConfig.Type.TEXT_INPUT,
|
||||
type=ProviderConfigType.TEXT_INPUT,
|
||||
required=False,
|
||||
placeholder=I18nObject(en_US="Enter api key header", zh_Hans="输入 api key header,如:X-API-KEY"),
|
||||
default="api_key",
|
||||
@@ -71,7 +71,7 @@ class ApiToolManageService:
|
||||
),
|
||||
ProviderConfig(
|
||||
name="api_key_value",
|
||||
type=ProviderConfig.Type.TEXT_INPUT,
|
||||
type=ProviderConfigType.TEXT_INPUT,
|
||||
required=False,
|
||||
placeholder=I18nObject(en_US="Enter api key", zh_Hans="输入 api key"),
|
||||
default="",
|
||||
|
||||
@@ -459,13 +459,11 @@ class MCPToolManageService:
|
||||
Returns:
|
||||
JSON string of encrypted data
|
||||
"""
|
||||
from core.entities.provider_entities import BasicProviderConfig
|
||||
from core.entities.provider_entities import BasicProviderConfig, ProviderConfigType
|
||||
from core.tools.utils.encryption import create_provider_encrypter
|
||||
|
||||
# Create config for secret fields
|
||||
config = [
|
||||
BasicProviderConfig(type=BasicProviderConfig.Type.SECRET_INPUT, name=field) for field in secret_fields
|
||||
]
|
||||
config = [BasicProviderConfig(type=ProviderConfigType.SECRET_INPUT, name=field) for field in secret_fields]
|
||||
|
||||
encrypter_instance, _ = create_provider_encrypter(
|
||||
tenant_id=tenant_id,
|
||||
|
||||
@@ -11,11 +11,15 @@ from core.plugin.entities.plugin import PluginInstallation, PluginInstallationSo
|
||||
from core.plugin.impl.plugin import PluginInstaller
|
||||
from core.plugin.plugin_service import PluginService
|
||||
from extensions.ext_redis import redis_client
|
||||
from models.account import TenantPluginAutoUpgradeStrategy
|
||||
from models.account import (
|
||||
TenantPluginAutoUpgradeCategory,
|
||||
TenantPluginAutoUpgradeMode,
|
||||
TenantPluginAutoUpgradeStrategySetting,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PluginCategory = TenantPluginAutoUpgradeStrategy.PluginCategory
|
||||
PluginCategory = TenantPluginAutoUpgradeCategory
|
||||
RETRY_TIMES_OF_ONE_PLUGIN_IN_ONE_TENANT = 3
|
||||
CACHE_REDIS_KEY_PREFIX = "plugin_autoupgrade_check_task:cached_plugin_snapshot:"
|
||||
CACHE_REDIS_TTL = 60 * 60 # 1 hour
|
||||
@@ -95,9 +99,9 @@ def _plugin_matches_category(plugin: PluginInstallation, category: str | None) -
|
||||
@shared_task(queue="plugin")
|
||||
def process_tenant_plugin_autoupgrade_check_task(
|
||||
tenant_id: str,
|
||||
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting,
|
||||
strategy_setting: TenantPluginAutoUpgradeStrategySetting,
|
||||
upgrade_time_of_day: int,
|
||||
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
|
||||
upgrade_mode: TenantPluginAutoUpgradeMode,
|
||||
exclude_plugins: list[str],
|
||||
include_plugins: list[str],
|
||||
category: PluginCategory | str | None = None,
|
||||
@@ -113,14 +117,14 @@ def process_tenant_plugin_autoupgrade_check_task(
|
||||
)
|
||||
)
|
||||
|
||||
if strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED:
|
||||
if strategy_setting == TenantPluginAutoUpgradeStrategySetting.DISABLED:
|
||||
return
|
||||
|
||||
# get plugin_ids to check
|
||||
plugin_ids: list[tuple[str, str, str]] = [] # plugin_id, version, unique_identifier
|
||||
click.echo(click.style(f"Upgrade mode: {upgrade_mode}", fg="green"))
|
||||
|
||||
if upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL and include_plugins:
|
||||
if upgrade_mode == TenantPluginAutoUpgradeMode.PARTIAL and include_plugins:
|
||||
all_plugins = manager.list_plugins(tenant_id)
|
||||
|
||||
for plugin in all_plugins:
|
||||
@@ -137,7 +141,7 @@ def process_tenant_plugin_autoupgrade_check_task(
|
||||
)
|
||||
)
|
||||
|
||||
elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE:
|
||||
elif upgrade_mode == TenantPluginAutoUpgradeMode.EXCLUDE:
|
||||
# get all plugins and remove excluded plugins
|
||||
all_plugins = manager.list_plugins(tenant_id)
|
||||
plugin_ids = [
|
||||
@@ -147,7 +151,7 @@ def process_tenant_plugin_autoupgrade_check_task(
|
||||
and plugin.plugin_id not in exclude_plugins
|
||||
and _plugin_matches_category(plugin, category_value)
|
||||
]
|
||||
elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
|
||||
elif upgrade_mode == TenantPluginAutoUpgradeMode.ALL:
|
||||
all_plugins = manager.list_plugins(tenant_id)
|
||||
plugin_ids = [
|
||||
(plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier)
|
||||
@@ -187,8 +191,8 @@ def process_tenant_plugin_autoupgrade_check_task(
|
||||
return False
|
||||
|
||||
version_checker = {
|
||||
TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST: operator.ne,
|
||||
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY: fix_only_checker,
|
||||
TenantPluginAutoUpgradeStrategySetting.LATEST: operator.ne,
|
||||
TenantPluginAutoUpgradeStrategySetting.FIX_ONLY: fix_only_checker,
|
||||
}
|
||||
|
||||
if version_checker[strategy_setting](latest_version, current_version):
|
||||
|
||||
@@ -3,11 +3,19 @@ from sqlalchemy import delete, func, select
|
||||
|
||||
from core.db.session_factory import session_factory
|
||||
from models import Tenant
|
||||
from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginPermission
|
||||
from models.account import (
|
||||
TenantPluginAutoUpgradeCategory,
|
||||
TenantPluginAutoUpgradeMode,
|
||||
TenantPluginAutoUpgradeStrategy,
|
||||
TenantPluginAutoUpgradeStrategySetting,
|
||||
TenantPluginDebugPermission,
|
||||
TenantPluginInstallPermission,
|
||||
TenantPluginPermission,
|
||||
)
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
from services.plugin.plugin_permission_service import PluginPermissionService
|
||||
|
||||
PLUGIN_CATEGORY = TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL
|
||||
PLUGIN_CATEGORY = TenantPluginAutoUpgradeCategory.TOOL
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -36,31 +44,31 @@ class TestPluginPermissionLifecycle:
|
||||
def test_change_creates_row(self, tenant):
|
||||
result = PluginPermissionService.change_permission(
|
||||
tenant,
|
||||
TenantPluginPermission.InstallPermission.ADMINS,
|
||||
TenantPluginPermission.DebugPermission.EVERYONE,
|
||||
TenantPluginInstallPermission.ADMINS,
|
||||
TenantPluginDebugPermission.EVERYONE,
|
||||
)
|
||||
assert result is True
|
||||
|
||||
perm = PluginPermissionService.get_permission(tenant)
|
||||
assert perm is not None
|
||||
assert perm.install_permission == TenantPluginPermission.InstallPermission.ADMINS
|
||||
assert perm.debug_permission == TenantPluginPermission.DebugPermission.EVERYONE
|
||||
assert perm.install_permission == TenantPluginInstallPermission.ADMINS
|
||||
assert perm.debug_permission == TenantPluginDebugPermission.EVERYONE
|
||||
|
||||
def test_change_updates_existing_row(self, tenant):
|
||||
PluginPermissionService.change_permission(
|
||||
tenant,
|
||||
TenantPluginPermission.InstallPermission.ADMINS,
|
||||
TenantPluginPermission.DebugPermission.NOBODY,
|
||||
TenantPluginInstallPermission.ADMINS,
|
||||
TenantPluginDebugPermission.NOBODY,
|
||||
)
|
||||
PluginPermissionService.change_permission(
|
||||
tenant,
|
||||
TenantPluginPermission.InstallPermission.EVERYONE,
|
||||
TenantPluginPermission.DebugPermission.ADMINS,
|
||||
TenantPluginInstallPermission.EVERYONE,
|
||||
TenantPluginDebugPermission.ADMINS,
|
||||
)
|
||||
perm = PluginPermissionService.get_permission(tenant)
|
||||
assert perm is not None
|
||||
assert perm.install_permission == TenantPluginPermission.InstallPermission.EVERYONE
|
||||
assert perm.debug_permission == TenantPluginPermission.DebugPermission.ADMINS
|
||||
assert perm.install_permission == TenantPluginInstallPermission.EVERYONE
|
||||
assert perm.debug_permission == TenantPluginDebugPermission.ADMINS
|
||||
|
||||
with session_factory.create_session() as session:
|
||||
count = session.scalar(
|
||||
@@ -78,9 +86,9 @@ class TestPluginAutoUpgradeLifecycle:
|
||||
def test_change_creates_row(self, tenant):
|
||||
result = PluginAutoUpgradeService.change_strategy(
|
||||
tenant,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategySetting.LATEST,
|
||||
upgrade_time_of_day=3,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
|
||||
upgrade_mode=TenantPluginAutoUpgradeMode.ALL,
|
||||
exclude_plugins=[],
|
||||
include_plugins=[],
|
||||
category=PLUGIN_CATEGORY,
|
||||
@@ -89,24 +97,24 @@ class TestPluginAutoUpgradeLifecycle:
|
||||
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
|
||||
assert strategy is not None
|
||||
assert strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
|
||||
assert strategy.strategy_setting == TenantPluginAutoUpgradeStrategySetting.LATEST
|
||||
assert strategy.upgrade_time_of_day == 3
|
||||
|
||||
def test_change_updates_existing_row(self, tenant):
|
||||
PluginAutoUpgradeService.change_strategy(
|
||||
tenant,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
|
||||
upgrade_time_of_day=0,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
|
||||
upgrade_mode=TenantPluginAutoUpgradeMode.ALL,
|
||||
exclude_plugins=[],
|
||||
include_plugins=[],
|
||||
category=PLUGIN_CATEGORY,
|
||||
)
|
||||
PluginAutoUpgradeService.change_strategy(
|
||||
tenant,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategySetting.LATEST,
|
||||
upgrade_time_of_day=12,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL,
|
||||
upgrade_mode=TenantPluginAutoUpgradeMode.PARTIAL,
|
||||
exclude_plugins=[],
|
||||
include_plugins=["plugin-a"],
|
||||
category=PLUGIN_CATEGORY,
|
||||
@@ -114,9 +122,9 @@ class TestPluginAutoUpgradeLifecycle:
|
||||
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
|
||||
assert strategy is not None
|
||||
assert strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
|
||||
assert strategy.strategy_setting == TenantPluginAutoUpgradeStrategySetting.LATEST
|
||||
assert strategy.upgrade_time_of_day == 12
|
||||
assert strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL
|
||||
assert strategy.upgrade_mode == TenantPluginAutoUpgradeMode.PARTIAL
|
||||
assert strategy.include_plugins == ["plugin-a"]
|
||||
|
||||
def test_exclude_plugin_creates_strategy_when_none_exists(self, tenant):
|
||||
@@ -124,15 +132,15 @@ class TestPluginAutoUpgradeLifecycle:
|
||||
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
|
||||
assert strategy is not None
|
||||
assert strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
|
||||
assert strategy.upgrade_mode == TenantPluginAutoUpgradeMode.EXCLUDE
|
||||
assert "my-plugin" in strategy.exclude_plugins
|
||||
|
||||
def test_exclude_plugin_appends_in_exclude_mode(self, tenant):
|
||||
PluginAutoUpgradeService.change_strategy(
|
||||
tenant,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
|
||||
upgrade_time_of_day=0,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
upgrade_mode=TenantPluginAutoUpgradeMode.EXCLUDE,
|
||||
exclude_plugins=["existing"],
|
||||
include_plugins=[],
|
||||
category=PLUGIN_CATEGORY,
|
||||
@@ -147,9 +155,9 @@ class TestPluginAutoUpgradeLifecycle:
|
||||
def test_exclude_plugin_dedup_in_exclude_mode(self, tenant):
|
||||
PluginAutoUpgradeService.change_strategy(
|
||||
tenant,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
|
||||
upgrade_time_of_day=0,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
upgrade_mode=TenantPluginAutoUpgradeMode.EXCLUDE,
|
||||
exclude_plugins=["same-plugin"],
|
||||
include_plugins=[],
|
||||
category=PLUGIN_CATEGORY,
|
||||
@@ -163,9 +171,9 @@ class TestPluginAutoUpgradeLifecycle:
|
||||
def test_exclude_from_partial_mode_removes_from_include(self, tenant):
|
||||
PluginAutoUpgradeService.change_strategy(
|
||||
tenant,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
|
||||
upgrade_time_of_day=0,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL,
|
||||
upgrade_mode=TenantPluginAutoUpgradeMode.PARTIAL,
|
||||
exclude_plugins=[],
|
||||
include_plugins=["p1", "p2"],
|
||||
category=PLUGIN_CATEGORY,
|
||||
@@ -180,9 +188,9 @@ class TestPluginAutoUpgradeLifecycle:
|
||||
def test_exclude_from_all_mode_switches_to_exclude(self, tenant):
|
||||
PluginAutoUpgradeService.change_strategy(
|
||||
tenant,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategySetting.LATEST,
|
||||
upgrade_time_of_day=0,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
|
||||
upgrade_mode=TenantPluginAutoUpgradeMode.ALL,
|
||||
exclude_plugins=[],
|
||||
include_plugins=[],
|
||||
category=PLUGIN_CATEGORY,
|
||||
@@ -191,5 +199,5 @@ class TestPluginAutoUpgradeLifecycle:
|
||||
|
||||
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
|
||||
assert strategy is not None
|
||||
assert strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
|
||||
assert strategy.upgrade_mode == TenantPluginAutoUpgradeMode.EXCLUDE
|
||||
assert "excluded-plugin" in strategy.exclude_plugins
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline_import import (
|
||||
RagPipelineImportCheckDependenciesApi,
|
||||
RagPipelineImportConfirmApi,
|
||||
)
|
||||
from core.plugin.entities.plugin import PluginDependency
|
||||
from core.plugin.entities.plugin import PluginDependency, PluginDependencyType
|
||||
from models.dataset import Pipeline
|
||||
from services.entities.dsl_entities import CheckDependenciesResult, ImportStatus
|
||||
from services.rag_pipeline.rag_pipeline_dsl_service import RagPipelineImportInfo
|
||||
@@ -237,7 +237,7 @@ class TestRagPipelineImportCheckDependenciesApi:
|
||||
|
||||
pipeline = MagicMock(spec=Pipeline)
|
||||
dependency = PluginDependency(
|
||||
type=PluginDependency.Type.Marketplace,
|
||||
type=PluginDependencyType.Marketplace,
|
||||
value=PluginDependency.Marketplace(
|
||||
marketplace_plugin_unique_identifier="langgenius/example:0.1.0",
|
||||
version="0.1.0",
|
||||
|
||||
+6
-2
@@ -607,7 +607,11 @@ class TestMiscApis:
|
||||
method = unwrap(api.get)
|
||||
|
||||
service = MagicMock()
|
||||
service.get_recommended_plugins.return_value = [{"id": "p1"}]
|
||||
recommended_plugins = {
|
||||
"installed_recommended_plugins": [{"id": "p1"}],
|
||||
"uninstalled_recommended_plugins": [{"id": "p2"}],
|
||||
}
|
||||
service.get_recommended_plugins.return_value = recommended_plugins
|
||||
user = make_account()
|
||||
tenant_id = "tenant-1"
|
||||
|
||||
@@ -619,7 +623,7 @@ class TestMiscApis:
|
||||
),
|
||||
):
|
||||
result = method(api, tenant_id, user)
|
||||
assert result == [{"id": "p1"}]
|
||||
assert result == recommended_plugins
|
||||
service.get_recommended_plugins.assert_called_once_with("all", user, tenant_id)
|
||||
|
||||
|
||||
|
||||
+21
-15
@@ -10,7 +10,13 @@ from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from controllers.console.workspace import plugin_permission_required
|
||||
from models.account import Tenant, TenantPluginPermission, TenantStatus
|
||||
from models.account import (
|
||||
Tenant,
|
||||
TenantPluginDebugPermission,
|
||||
TenantPluginInstallPermission,
|
||||
TenantPluginPermission,
|
||||
TenantStatus,
|
||||
)
|
||||
|
||||
|
||||
def _create_tenant(db_session: Session) -> Tenant:
|
||||
@@ -24,8 +30,8 @@ def _create_tenant(db_session: Session) -> Tenant:
|
||||
def _create_permission(
|
||||
db_session: Session,
|
||||
tenant_id: str,
|
||||
install: TenantPluginPermission.InstallPermission = TenantPluginPermission.InstallPermission.EVERYONE,
|
||||
debug: TenantPluginPermission.DebugPermission = TenantPluginPermission.DebugPermission.EVERYONE,
|
||||
install: TenantPluginInstallPermission = TenantPluginInstallPermission.EVERYONE,
|
||||
debug: TenantPluginDebugPermission = TenantPluginDebugPermission.EVERYONE,
|
||||
) -> TenantPluginPermission:
|
||||
perm = TenantPluginPermission(
|
||||
tenant_id=tenant_id,
|
||||
@@ -59,8 +65,8 @@ class TestPluginPermissionRequired:
|
||||
_create_permission(
|
||||
db_session_with_containers,
|
||||
tenant.id,
|
||||
install=TenantPluginPermission.InstallPermission.NOBODY,
|
||||
debug=TenantPluginPermission.DebugPermission.EVERYONE,
|
||||
install=TenantPluginInstallPermission.NOBODY,
|
||||
debug=TenantPluginDebugPermission.EVERYONE,
|
||||
)
|
||||
user = SimpleNamespace(is_admin_or_owner=True)
|
||||
|
||||
@@ -81,8 +87,8 @@ class TestPluginPermissionRequired:
|
||||
_create_permission(
|
||||
db_session_with_containers,
|
||||
tenant.id,
|
||||
install=TenantPluginPermission.InstallPermission.ADMINS,
|
||||
debug=TenantPluginPermission.DebugPermission.EVERYONE,
|
||||
install=TenantPluginInstallPermission.ADMINS,
|
||||
debug=TenantPluginDebugPermission.EVERYONE,
|
||||
)
|
||||
user = SimpleNamespace(is_admin_or_owner=False)
|
||||
|
||||
@@ -103,8 +109,8 @@ class TestPluginPermissionRequired:
|
||||
_create_permission(
|
||||
db_session_with_containers,
|
||||
tenant.id,
|
||||
install=TenantPluginPermission.InstallPermission.ADMINS,
|
||||
debug=TenantPluginPermission.DebugPermission.EVERYONE,
|
||||
install=TenantPluginInstallPermission.ADMINS,
|
||||
debug=TenantPluginDebugPermission.EVERYONE,
|
||||
)
|
||||
user = SimpleNamespace(is_admin_or_owner=True)
|
||||
|
||||
@@ -124,8 +130,8 @@ class TestPluginPermissionRequired:
|
||||
_create_permission(
|
||||
db_session_with_containers,
|
||||
tenant.id,
|
||||
install=TenantPluginPermission.InstallPermission.EVERYONE,
|
||||
debug=TenantPluginPermission.DebugPermission.NOBODY,
|
||||
install=TenantPluginInstallPermission.EVERYONE,
|
||||
debug=TenantPluginDebugPermission.NOBODY,
|
||||
)
|
||||
user = SimpleNamespace(is_admin_or_owner=True)
|
||||
|
||||
@@ -146,8 +152,8 @@ class TestPluginPermissionRequired:
|
||||
_create_permission(
|
||||
db_session_with_containers,
|
||||
tenant.id,
|
||||
install=TenantPluginPermission.InstallPermission.EVERYONE,
|
||||
debug=TenantPluginPermission.DebugPermission.ADMINS,
|
||||
install=TenantPluginInstallPermission.EVERYONE,
|
||||
debug=TenantPluginDebugPermission.ADMINS,
|
||||
)
|
||||
user = SimpleNamespace(is_admin_or_owner=False)
|
||||
|
||||
@@ -168,8 +174,8 @@ class TestPluginPermissionRequired:
|
||||
_create_permission(
|
||||
db_session_with_containers,
|
||||
tenant.id,
|
||||
install=TenantPluginPermission.InstallPermission.EVERYONE,
|
||||
debug=TenantPluginPermission.DebugPermission.ADMINS,
|
||||
install=TenantPluginInstallPermission.EVERYONE,
|
||||
debug=TenantPluginDebugPermission.ADMINS,
|
||||
)
|
||||
user = SimpleNamespace(is_admin_or_owner=True)
|
||||
|
||||
|
||||
+15
-15
@@ -6,7 +6,7 @@ import pytest
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.account import TenantPluginPermission
|
||||
from models.account import TenantPluginDebugPermission, TenantPluginInstallPermission, TenantPluginPermission
|
||||
from services.plugin.plugin_permission_service import PluginPermissionService
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ class TestGetPermission:
|
||||
tenant_id = _tenant_id()
|
||||
permission = TenantPluginPermission(
|
||||
tenant_id=tenant_id,
|
||||
install_permission=TenantPluginPermission.InstallPermission.ADMINS,
|
||||
debug_permission=TenantPluginPermission.DebugPermission.EVERYONE,
|
||||
install_permission=TenantPluginInstallPermission.ADMINS,
|
||||
debug_permission=TenantPluginDebugPermission.EVERYONE,
|
||||
)
|
||||
db_session_with_containers.add(permission)
|
||||
db_session_with_containers.commit()
|
||||
@@ -43,8 +43,8 @@ class TestGetPermission:
|
||||
assert result is not None
|
||||
assert result.id == permission.id
|
||||
assert result.tenant_id == tenant_id
|
||||
assert result.install_permission == TenantPluginPermission.InstallPermission.ADMINS
|
||||
assert result.debug_permission == TenantPluginPermission.DebugPermission.EVERYONE
|
||||
assert result.install_permission == TenantPluginInstallPermission.ADMINS
|
||||
assert result.debug_permission == TenantPluginDebugPermission.EVERYONE
|
||||
|
||||
@pytest.mark.usefixtures("flask_app_with_containers")
|
||||
def test_returns_none_when_not_found(self) -> None:
|
||||
@@ -61,36 +61,36 @@ class TestChangePermission:
|
||||
|
||||
result = PluginPermissionService.change_permission(
|
||||
tenant_id,
|
||||
TenantPluginPermission.InstallPermission.EVERYONE,
|
||||
TenantPluginPermission.DebugPermission.EVERYONE,
|
||||
TenantPluginInstallPermission.EVERYONE,
|
||||
TenantPluginDebugPermission.EVERYONE,
|
||||
)
|
||||
|
||||
permission = _get_permission(db_session_with_containers, tenant_id)
|
||||
assert result is True
|
||||
assert permission is not None
|
||||
assert permission.install_permission == TenantPluginPermission.InstallPermission.EVERYONE
|
||||
assert permission.debug_permission == TenantPluginPermission.DebugPermission.EVERYONE
|
||||
assert permission.install_permission == TenantPluginInstallPermission.EVERYONE
|
||||
assert permission.debug_permission == TenantPluginDebugPermission.EVERYONE
|
||||
|
||||
def test_updates_existing_permission(self, db_session_with_containers: Session) -> None:
|
||||
tenant_id = _tenant_id()
|
||||
existing = TenantPluginPermission(
|
||||
tenant_id=tenant_id,
|
||||
install_permission=TenantPluginPermission.InstallPermission.EVERYONE,
|
||||
debug_permission=TenantPluginPermission.DebugPermission.EVERYONE,
|
||||
install_permission=TenantPluginInstallPermission.EVERYONE,
|
||||
debug_permission=TenantPluginDebugPermission.EVERYONE,
|
||||
)
|
||||
db_session_with_containers.add(existing)
|
||||
db_session_with_containers.commit()
|
||||
|
||||
result = PluginPermissionService.change_permission(
|
||||
tenant_id,
|
||||
TenantPluginPermission.InstallPermission.ADMINS,
|
||||
TenantPluginPermission.DebugPermission.ADMINS,
|
||||
TenantPluginInstallPermission.ADMINS,
|
||||
TenantPluginDebugPermission.ADMINS,
|
||||
)
|
||||
|
||||
permission = _get_permission(db_session_with_containers, tenant_id)
|
||||
assert result is True
|
||||
assert permission is not None
|
||||
assert permission.id == existing.id
|
||||
assert permission.install_permission == TenantPluginPermission.InstallPermission.ADMINS
|
||||
assert permission.debug_permission == TenantPluginPermission.DebugPermission.ADMINS
|
||||
assert permission.install_permission == TenantPluginInstallPermission.ADMINS
|
||||
assert permission.debug_permission == TenantPluginDebugPermission.ADMINS
|
||||
assert _count_permissions(db_session_with_containers, tenant_id) == 1
|
||||
|
||||
@@ -77,6 +77,10 @@ def assert_has_actionable_violation(stderr: str, path: str) -> None:
|
||||
assert "no-new-getattr" in stderr
|
||||
|
||||
|
||||
def main_branch_rev(repo: Path) -> str:
|
||||
return git(repo, "rev-parse", "main")
|
||||
|
||||
|
||||
def test_resolve_ast_grep_command_prefers_ast_grep(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
module = load_guard_module()
|
||||
monkeypatch.setattr(
|
||||
@@ -130,8 +134,46 @@ def test_resolve_ast_grep_command_raises_without_explicit_binary(monkeypatch: py
|
||||
module.resolve_ast_grep_command()
|
||||
|
||||
|
||||
def test_cli_requires_explicit_diff_source(tmp_path: Path) -> None:
|
||||
result = run_script(tmp_path)
|
||||
|
||||
assert result.returncode == 2
|
||||
assert "one of the arguments --staged --base-rev is required" in result.stderr
|
||||
|
||||
|
||||
def test_cli_rejects_mixed_diff_sources(tmp_path: Path) -> None:
|
||||
result = run_script(tmp_path, "--staged", "--base-rev", "deadbeef")
|
||||
|
||||
assert result.returncode == 2
|
||||
assert "not allowed with argument" in result.stderr
|
||||
|
||||
|
||||
def test_cli_help_exposes_only_new_diff_source_flags(tmp_path: Path) -> None:
|
||||
help_result = run_script(tmp_path, "--help")
|
||||
|
||||
assert help_result.returncode == 0
|
||||
assert "--staged" in help_result.stdout
|
||||
assert "--base-rev" in help_result.stdout
|
||||
assert "--mode" not in help_result.stdout
|
||||
assert "--merge-target" not in help_result.stdout
|
||||
|
||||
result = run_script(tmp_path, "--staged", "--mode", "ci")
|
||||
|
||||
assert result.returncode == 2
|
||||
assert "unrecognized arguments: --mode ci" in result.stderr
|
||||
|
||||
result = run_script(tmp_path, "--base-rev", "deadbeef", "--merge-target", "main")
|
||||
|
||||
assert result.returncode == 2
|
||||
assert "unrecognized arguments: --merge-target main" in result.stderr
|
||||
|
||||
|
||||
def test_style_workflow_wires_no_new_getattr_guard() -> None:
|
||||
workflow = (REPO_ROOT / ".github" / "workflows" / "style.yml").read_text(encoding="utf-8")
|
||||
assert re.search(
|
||||
r"(?ms)^on:\n workflow_call:\n inputs:\n base-rev:\n required: true\n type: string\n",
|
||||
workflow,
|
||||
)
|
||||
python_style_job = re.search(
|
||||
r"(?ms)^ python-style:\n(?P<job>.*?)(?=^ [a-z0-9-]+:\n|\Z)",
|
||||
workflow,
|
||||
@@ -157,8 +199,9 @@ def test_style_workflow_wires_no_new_getattr_guard() -> None:
|
||||
assert "scripts/check_no_new_getattr.py\n" in files_block
|
||||
assert "scripts/ast_grep_rules/no_new_getattr.yml\n" in files_block
|
||||
assert ".github/workflows/style.yml\n" in files_block
|
||||
assert ".github/workflows/main-ci.yml\n" in files_block
|
||||
|
||||
guard_command = "scripts/check_no_new_getattr.py --mode ci --merge-target main"
|
||||
guard_command = 'scripts/check_no_new_getattr.py --base-rev "${{ inputs.base-rev }}"'
|
||||
assert guard_command in job_text
|
||||
|
||||
guard_step = re.search(
|
||||
@@ -168,55 +211,34 @@ def test_style_workflow_wires_no_new_getattr_guard() -> None:
|
||||
)
|
||||
assert guard_step is not None
|
||||
|
||||
pre_guard_text = job_text[: guard_step.start()]
|
||||
step_pattern = r"(?ms)^ - name: [^\n]*\n(?P<step>.*?)(?=^ - name: |\Z)"
|
||||
fetch_step_text = next(
|
||||
(
|
||||
match.group("step")
|
||||
for match in re.finditer(step_pattern, pre_guard_text)
|
||||
if any(
|
||||
re.search(pattern, line)
|
||||
for line in match.group("step").splitlines()
|
||||
for pattern in (
|
||||
r"git fetch .*refs/heads/main:refs/remotes/origin/main",
|
||||
r"git fetch .*main:refs/remotes/origin/main",
|
||||
r"git fetch .*refs/remotes/origin/main",
|
||||
)
|
||||
)
|
||||
),
|
||||
"",
|
||||
)
|
||||
assert fetch_step_text
|
||||
assert "git fetch" in fetch_step_text
|
||||
assert "origin" in fetch_step_text
|
||||
assert any(
|
||||
re.search(pattern, line)
|
||||
for line in fetch_step_text.splitlines()
|
||||
for pattern in (
|
||||
r"git fetch .*refs/heads/main:refs/remotes/origin/main",
|
||||
r"git fetch .*main:refs/remotes/origin/main",
|
||||
r"git fetch .*refs/remotes/origin/main",
|
||||
)
|
||||
)
|
||||
|
||||
bind_step = re.search(
|
||||
r"(?ms)^ - name: Bind merge target branch for getattr guard\n(?P<step>.*?)(?=^ - name: |\Z)",
|
||||
pre_guard_text,
|
||||
)
|
||||
assert bind_step is not None
|
||||
bind_step_text = bind_step.group("step")
|
||||
assert any(
|
||||
command in bind_step_text
|
||||
for command in (
|
||||
"git branch main origin/main",
|
||||
"git checkout -B main origin/main",
|
||||
"git switch -C main origin/main",
|
||||
"git update-ref refs/heads/main refs/remotes/origin/main",
|
||||
)
|
||||
)
|
||||
assert "GITHUB_BASE_SHA" not in guard_step.group("step")
|
||||
|
||||
|
||||
def test_ci_mode_passes_when_only_legacy_getattr_exists(tmp_path: Path) -> None:
|
||||
def test_main_ci_passes_style_base_rev_input() -> None:
|
||||
workflow = (REPO_ROOT / ".github" / "workflows" / "main-ci.yml").read_text(encoding="utf-8")
|
||||
style_job = re.search(
|
||||
r"(?ms)^ style-check:\n(?P<job>.*?)(?=^ [a-z0-9-]+:\n|\Z)",
|
||||
workflow,
|
||||
)
|
||||
assert style_job is not None
|
||||
assert "uses: ./.github/workflows/style.yml" in style_job.group("job")
|
||||
assert 'base-rev: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}' in style_job.group(
|
||||
"job"
|
||||
)
|
||||
|
||||
api_filter = re.search(
|
||||
r"(?ms)^ api:\n(?P<filter>(?:^ - '[^']+'\n)+)",
|
||||
workflow,
|
||||
)
|
||||
assert api_filter is not None
|
||||
filter_text = api_filter.group("filter")
|
||||
assert "scripts/check_no_new_getattr.py" in filter_text
|
||||
assert "scripts/ast_grep_rules/no_new_getattr.yml" in filter_text
|
||||
assert ".github/workflows/style.yml" in filter_text
|
||||
assert ".github/workflows/main-ci.yml" in filter_text
|
||||
|
||||
|
||||
def test_base_rev_mode_passes_when_only_legacy_getattr_exists(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
@@ -239,12 +261,12 @@ def test_ci_mode_passes_when_only_legacy_getattr_exists(tmp_path: Path) -> None:
|
||||
)
|
||||
commit_all(tmp_path, "unrelated change")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
result = run_script(tmp_path, "--base-rev", main_branch_rev(tmp_path))
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
def test_ci_mode_fails_for_new_file_with_getattr(tmp_path: Path) -> None:
|
||||
def test_base_rev_mode_fails_for_new_file_with_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
@@ -267,13 +289,13 @@ def test_ci_mode_fails_for_new_file_with_getattr(tmp_path: Path) -> None:
|
||||
)
|
||||
commit_all(tmp_path, "add new getattr usage")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
result = run_script(tmp_path, "--base-rev", main_branch_rev(tmp_path))
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")
|
||||
|
||||
|
||||
def test_ci_mode_fails_for_new_file_with_two_arg_getattr(tmp_path: Path) -> None:
|
||||
def test_base_rev_mode_fails_for_new_file_with_two_arg_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
@@ -296,13 +318,13 @@ def test_ci_mode_fails_for_new_file_with_two_arg_getattr(tmp_path: Path) -> None
|
||||
)
|
||||
commit_all(tmp_path, "add new two-arg getattr usage")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
result = run_script(tmp_path, "--base-rev", main_branch_rev(tmp_path))
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")
|
||||
|
||||
|
||||
def test_ci_mode_fails_for_new_file_with_builtins_getattr(tmp_path: Path) -> None:
|
||||
def test_base_rev_mode_fails_for_new_file_with_builtins_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
@@ -328,13 +350,13 @@ def test_ci_mode_fails_for_new_file_with_builtins_getattr(tmp_path: Path) -> Non
|
||||
)
|
||||
commit_all(tmp_path, "add new builtins getattr usage")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
result = run_script(tmp_path, "--base-rev", main_branch_rev(tmp_path))
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")
|
||||
|
||||
|
||||
def test_ci_mode_fails_for_new_file_with_two_arg_builtins_getattr(tmp_path: Path) -> None:
|
||||
def test_base_rev_mode_fails_for_new_file_with_two_arg_builtins_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
@@ -360,13 +382,13 @@ def test_ci_mode_fails_for_new_file_with_two_arg_builtins_getattr(tmp_path: Path
|
||||
)
|
||||
commit_all(tmp_path, "add new two-arg builtins getattr usage")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
result = run_script(tmp_path, "--base-rev", main_branch_rev(tmp_path))
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")
|
||||
|
||||
|
||||
def test_ci_mode_fails_for_new_file_with_dunder_builtins_getattr(tmp_path: Path) -> None:
|
||||
def test_base_rev_mode_fails_for_new_file_with_dunder_builtins_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
@@ -389,13 +411,13 @@ def test_ci_mode_fails_for_new_file_with_dunder_builtins_getattr(tmp_path: Path)
|
||||
)
|
||||
commit_all(tmp_path, "add new dunder builtins getattr usage")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
result = run_script(tmp_path, "--base-rev", main_branch_rev(tmp_path))
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")
|
||||
|
||||
|
||||
def test_ci_mode_fails_for_new_file_with_two_arg_dunder_builtins_getattr(tmp_path: Path) -> None:
|
||||
def test_base_rev_mode_fails_for_new_file_with_two_arg_dunder_builtins_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
@@ -418,13 +440,13 @@ def test_ci_mode_fails_for_new_file_with_two_arg_dunder_builtins_getattr(tmp_pat
|
||||
)
|
||||
commit_all(tmp_path, "add new two-arg dunder builtins getattr usage")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
result = run_script(tmp_path, "--base-rev", main_branch_rev(tmp_path))
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")
|
||||
|
||||
|
||||
def test_ci_mode_uses_merge_base_against_main_not_just_head_parent(tmp_path: Path) -> None:
|
||||
def test_base_rev_mode_uses_provided_base_revision_not_head_parent(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
@@ -435,6 +457,7 @@ def test_ci_mode_uses_merge_base_against_main_not_just_head_parent(tmp_path: Pat
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
base_rev = main_branch_rev(tmp_path)
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
@@ -457,13 +480,77 @@ def test_ci_mode_uses_merge_base_against_main_not_just_head_parent(tmp_path: Pat
|
||||
)
|
||||
commit_all(tmp_path, "later feature commit does not touch violating file")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
result = run_script(tmp_path, "--base-rev", base_rev)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/introduced_earlier.py")
|
||||
|
||||
|
||||
def test_pre_commit_mode_reads_staged_content_only(tmp_path: Path) -> None:
|
||||
def test_base_rev_mode_works_without_local_main_branch(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/existing.py",
|
||||
"""
|
||||
def stable() -> str:
|
||||
return "ok"
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
base_rev = main_branch_rev(tmp_path)
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/other.py",
|
||||
"""
|
||||
def meaning() -> int:
|
||||
return 42
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "feature change")
|
||||
|
||||
git(tmp_path, "checkout", "--detach", "HEAD")
|
||||
git(tmp_path, "branch", "-D", "main")
|
||||
|
||||
result = run_script(tmp_path, "--base-rev", base_rev)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
def test_base_rev_mode_ignores_github_base_sha_environment(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/existing.py",
|
||||
"""
|
||||
def stable() -> str:
|
||||
return "ok"
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
base_rev = main_branch_rev(tmp_path)
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/other.py",
|
||||
"""
|
||||
def meaning() -> int:
|
||||
return 42
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "feature change")
|
||||
monkeypatch.setenv("GITHUB_BASE_SHA", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
|
||||
|
||||
result = run_script(tmp_path, "--base-rev", base_rev)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
def test_staged_mode_reads_staged_content_only(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
@@ -494,12 +581,12 @@ def test_pre_commit_mode_reads_staged_content_only(tmp_path: Path) -> None:
|
||||
""",
|
||||
)
|
||||
|
||||
result = run_script(tmp_path, "--mode", "pre-commit")
|
||||
result = run_script(tmp_path, "--staged")
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
def test_pre_commit_mode_fails_for_staged_two_arg_getattr(tmp_path: Path) -> None:
|
||||
def test_staged_mode_fails_for_staged_two_arg_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
@@ -521,13 +608,13 @@ def test_pre_commit_mode_fails_for_staged_two_arg_getattr(tmp_path: Path) -> Non
|
||||
)
|
||||
git(tmp_path, "add", "pkg/module.py")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "pre-commit")
|
||||
result = run_script(tmp_path, "--staged")
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/module.py")
|
||||
|
||||
|
||||
def test_pre_commit_mode_fails_for_staged_builtins_getattr(tmp_path: Path) -> None:
|
||||
def test_staged_mode_fails_for_staged_builtins_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
@@ -552,13 +639,13 @@ def test_pre_commit_mode_fails_for_staged_builtins_getattr(tmp_path: Path) -> No
|
||||
)
|
||||
git(tmp_path, "add", "pkg/module.py")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "pre-commit")
|
||||
result = run_script(tmp_path, "--staged")
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/module.py")
|
||||
|
||||
|
||||
def test_pre_commit_mode_fails_for_staged_two_arg_builtins_getattr(tmp_path: Path) -> None:
|
||||
def test_staged_mode_fails_for_staged_two_arg_builtins_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
@@ -583,7 +670,7 @@ def test_pre_commit_mode_fails_for_staged_two_arg_builtins_getattr(tmp_path: Pat
|
||||
)
|
||||
git(tmp_path, "add", "pkg/module.py")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "pre-commit")
|
||||
result = run_script(tmp_path, "--staged")
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/module.py")
|
||||
@@ -603,6 +690,7 @@ def test_modified_hunk_with_same_getattr_count_is_allowed(tmp_path: Path) -> Non
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
base_rev = main_branch_rev(tmp_path)
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
@@ -618,7 +706,7 @@ def test_modified_hunk_with_same_getattr_count_is_allowed(tmp_path: Path) -> Non
|
||||
)
|
||||
commit_all(tmp_path, "touch legacy getattr hunk")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
result = run_script(tmp_path, "--base-rev", base_rev)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
@@ -635,6 +723,7 @@ def test_modified_hunk_with_decreased_getattr_count_is_allowed(tmp_path: Path) -
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
base_rev = main_branch_rev(tmp_path)
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
@@ -647,7 +736,7 @@ def test_modified_hunk_with_decreased_getattr_count_is_allowed(tmp_path: Path) -
|
||||
)
|
||||
commit_all(tmp_path, "remove one legacy getattr")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
result = run_script(tmp_path, "--base-rev", base_rev)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
@@ -663,6 +752,7 @@ def test_modified_hunk_with_increased_getattr_count_fails(tmp_path: Path) -> Non
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
base_rev = main_branch_rev(tmp_path)
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
@@ -676,7 +766,7 @@ def test_modified_hunk_with_increased_getattr_count_fails(tmp_path: Path) -> Non
|
||||
)
|
||||
commit_all(tmp_path, "add one more getattr")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
result = run_script(tmp_path, "--base-rev", base_rev)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/sample.py")
|
||||
@@ -694,6 +784,7 @@ def test_inline_noqa_suppression_with_explanatory_text_skips_added_getattr(tmp_p
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
base_rev = main_branch_rev(tmp_path)
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
@@ -706,7 +797,7 @@ def test_inline_noqa_suppression_with_explanatory_text_skips_added_getattr(tmp_p
|
||||
)
|
||||
commit_all(tmp_path, "add suppressed getattr")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
result = run_script(tmp_path, "--base-rev", base_rev)
|
||||
|
||||
assert "no-new-getattr needed for plugin-defined attributes" in (tmp_path / "pkg/existing.py").read_text(
|
||||
encoding="utf-8"
|
||||
@@ -725,6 +816,7 @@ def test_inline_noqa_without_explanatory_text_is_not_sufficient(tmp_path: Path)
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
base_rev = main_branch_rev(tmp_path)
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
@@ -737,7 +829,7 @@ def test_inline_noqa_without_explanatory_text_is_not_sufficient(tmp_path: Path)
|
||||
)
|
||||
commit_all(tmp_path, "add bare noqa getattr")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
result = run_script(tmp_path, "--base-rev", base_rev)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/existing.py")
|
||||
@@ -753,6 +845,7 @@ def test_non_python_file_with_getattr_text_does_not_fail_guard(tmp_path: Path) -
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
base_rev = main_branch_rev(tmp_path)
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
@@ -764,6 +857,6 @@ def test_non_python_file_with_getattr_text_does_not_fail_guard(tmp_path: Path) -
|
||||
)
|
||||
commit_all(tmp_path, "document getattr example")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
result = run_script(tmp_path, "--base-rev", base_rev)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
+224
-46
@@ -1,4 +1,5 @@
|
||||
import inspect
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -23,6 +24,76 @@ from graphon.model_runtime.errors.validate import CredentialsValidateFailedError
|
||||
from services.datasource_provider_service import DatasourceProviderService
|
||||
from services.plugin.oauth_service import OAuthProxyService
|
||||
|
||||
_PROVIDER_ID = "langgenius/notion_datasource/notion"
|
||||
|
||||
|
||||
def _i18n(text: str) -> dict[str, str]:
|
||||
return {"en_US": text, "zh_Hans": text, "pt_BR": text, "ja_JP": text}
|
||||
|
||||
|
||||
def _provider_config(name: str, type_: str, label: str, *, required: bool = True) -> dict:
|
||||
return {
|
||||
"type": type_,
|
||||
"name": name,
|
||||
"scope": None,
|
||||
"required": required,
|
||||
"default": None,
|
||||
"options": None,
|
||||
"multiple": False,
|
||||
"label": _i18n(label),
|
||||
"help": None,
|
||||
"url": None,
|
||||
"placeholder": None,
|
||||
}
|
||||
|
||||
|
||||
def _datasource_credential(credential_id: str = "cred-1", *, is_default: bool = True) -> dict:
|
||||
return {
|
||||
"credential": {
|
||||
"api_key": "******",
|
||||
"workspace": "engineering",
|
||||
"database_id": "db-123",
|
||||
},
|
||||
"type": "api-key",
|
||||
"name": "API Key",
|
||||
"avatar_url": "https://cdn.example.com/notion.png",
|
||||
"id": credential_id,
|
||||
"is_default": is_default,
|
||||
}
|
||||
|
||||
|
||||
def _datasource_auth() -> dict:
|
||||
return {
|
||||
"author": "Dify",
|
||||
"provider": "notion",
|
||||
"plugin_id": "langgenius/notion_datasource",
|
||||
"plugin_unique_identifier": "langgenius/notion_datasource:0.0.1",
|
||||
"icon": "icon.svg",
|
||||
"name": "notion",
|
||||
"label": _i18n("Notion"),
|
||||
"description": _i18n("Notion datasource"),
|
||||
"credential_schema": [
|
||||
_provider_config("api_key", "secret-input", "API key"),
|
||||
],
|
||||
"oauth_schema": {
|
||||
"client_schema": [
|
||||
_provider_config("client_id", "text-input", "Client ID"),
|
||||
],
|
||||
"credentials_schema": [
|
||||
_provider_config("access_token", "secret-input", "Access token"),
|
||||
],
|
||||
"oauth_custom_client_params": {"client_id": "masked-client", "client_secret": "********"},
|
||||
"is_oauth_custom_client_enabled": True,
|
||||
"is_system_oauth_params_exists": True,
|
||||
"redirect_uri": "https://api.example.com/oauth/callback",
|
||||
},
|
||||
"credentials_list": [_datasource_credential(), _datasource_credential("cred-2", is_default=False)],
|
||||
}
|
||||
|
||||
|
||||
def _success_response() -> dict[str, str]:
|
||||
return {"result": "success"}
|
||||
|
||||
|
||||
class TestDatasourcePluginOAuthAuthorizationUrl:
|
||||
def test_get_success(self, app: Flask):
|
||||
@@ -30,28 +101,50 @@ class TestDatasourcePluginOAuthAuthorizationUrl:
|
||||
method = inspect.unwrap(api.get)
|
||||
|
||||
user = MagicMock(id="user-1")
|
||||
oauth_client = {"client_id": "abc", "client_secret": "shh", "scopes": ["read", "write"]}
|
||||
auth_url_payload = {
|
||||
"authorization_url": "https://auth.example.com/oauth?client_id=abc&state=xyz",
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/?credential_id=cred-1"),
|
||||
patch.object(
|
||||
DatasourceProviderService,
|
||||
"get_oauth_client",
|
||||
return_value={"client_id": "abc"},
|
||||
),
|
||||
return_value=oauth_client,
|
||||
) as get_oauth_client,
|
||||
patch.object(
|
||||
OAuthProxyService,
|
||||
"create_proxy_context",
|
||||
return_value="ctx-1",
|
||||
),
|
||||
) as create_proxy_context,
|
||||
patch.object(
|
||||
OAuthHandler,
|
||||
"get_authorization_url",
|
||||
return_value={"url": "http://auth"},
|
||||
),
|
||||
return_value=auth_url_payload,
|
||||
) as get_authorization_url,
|
||||
):
|
||||
response = method(api, "tenant-1", user, "notion")
|
||||
response = method(api, "tenant-1", user, _PROVIDER_ID)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.get_json() == auth_url_payload
|
||||
assert "context_id=ctx-1" in response.headers.get("Set-Cookie")
|
||||
provider_id = get_oauth_client.call_args.kwargs["datasource_provider_id"]
|
||||
assert str(provider_id) == _PROVIDER_ID
|
||||
get_oauth_client.assert_called_once()
|
||||
create_proxy_context.assert_called_once_with(
|
||||
user_id="user-1",
|
||||
tenant_id="tenant-1",
|
||||
plugin_id="langgenius/notion_datasource",
|
||||
provider="notion",
|
||||
credential_id="cred-1",
|
||||
)
|
||||
get_authorization_url.assert_called_once()
|
||||
assert get_authorization_url.call_args.kwargs["tenant_id"] == "tenant-1"
|
||||
assert get_authorization_url.call_args.kwargs["user_id"] == "user-1"
|
||||
assert get_authorization_url.call_args.kwargs["plugin_id"] == "langgenius/notion_datasource"
|
||||
assert get_authorization_url.call_args.kwargs["provider"] == "notion"
|
||||
assert get_authorization_url.call_args.kwargs["system_credentials"] == oauth_client
|
||||
|
||||
def test_get_no_oauth_config(self, app: Flask):
|
||||
api = DatasourcePluginOAuthAuthorizationUrl()
|
||||
@@ -90,10 +183,10 @@ class TestDatasourcePluginOAuthAuthorizationUrl:
|
||||
patch.object(
|
||||
OAuthHandler,
|
||||
"get_authorization_url",
|
||||
return_value={"url": "http://auth"},
|
||||
return_value={"authorization_url": "http://auth"},
|
||||
),
|
||||
):
|
||||
response = method(api, "tenant-1", user, "notion")
|
||||
response = method(api, "tenant-1", user, _PROVIDER_ID)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "context_id" in response.headers.get("Set-Cookie")
|
||||
@@ -106,8 +199,9 @@ class TestDatasourceOAuthCallback:
|
||||
|
||||
oauth_response = MagicMock()
|
||||
oauth_response.credentials = {"token": "abc"}
|
||||
oauth_response.expires_at = None
|
||||
oauth_response.metadata = {"name": "test"}
|
||||
expires_at = datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC)
|
||||
oauth_response.expires_at = expires_at
|
||||
oauth_response.metadata = {"name": "Workspace Bot", "avatar_url": "https://avatar.example.com/bot.png"}
|
||||
|
||||
context = {
|
||||
"user_id": "user-1",
|
||||
@@ -125,7 +219,7 @@ class TestDatasourceOAuthCallback:
|
||||
patch.object(
|
||||
DatasourceProviderService,
|
||||
"get_oauth_client",
|
||||
return_value={"client_id": "abc"},
|
||||
return_value={"client_id": "abc", "client_secret": "secret"},
|
||||
),
|
||||
patch.object(
|
||||
OAuthHandler,
|
||||
@@ -136,11 +230,22 @@ class TestDatasourceOAuthCallback:
|
||||
DatasourceProviderService,
|
||||
"add_datasource_oauth_provider",
|
||||
return_value=None,
|
||||
),
|
||||
) as add_oauth_provider,
|
||||
):
|
||||
response = method(api, "notion")
|
||||
response = method(api, _PROVIDER_ID)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert "/oauth-callback" in response.location
|
||||
add_oauth_provider.assert_called_once()
|
||||
assert add_oauth_provider.call_args.kwargs == {
|
||||
"tenant_id": "tenant-1",
|
||||
"provider_id": add_oauth_provider.call_args.kwargs["provider_id"],
|
||||
"avatar_url": "https://avatar.example.com/bot.png",
|
||||
"name": "Workspace Bot",
|
||||
"expire_at": expires_at,
|
||||
"credentials": {"token": "abc"},
|
||||
}
|
||||
assert str(add_oauth_provider.call_args.kwargs["provider_id"]) == _PROVIDER_ID
|
||||
|
||||
def test_callback_missing_context(self, app: Flask):
|
||||
api = DatasourceOAuthCallback()
|
||||
@@ -223,12 +328,16 @@ class TestDatasourceOAuthCallback:
|
||||
DatasourceProviderService,
|
||||
"reauthorize_datasource_oauth_provider",
|
||||
return_value=None,
|
||||
),
|
||||
) as reauthorize_provider,
|
||||
):
|
||||
response = method(api, "notion")
|
||||
response = method(api, _PROVIDER_ID)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert "/oauth-callback" in response.location
|
||||
reauthorize_provider.assert_called_once()
|
||||
assert str(reauthorize_provider.call_args.kwargs["provider_id"]) == _PROVIDER_ID
|
||||
assert reauthorize_provider.call_args.kwargs["credential_id"] == "cred-1"
|
||||
assert reauthorize_provider.call_args.kwargs["credentials"] == {"token": "abc"}
|
||||
|
||||
def test_callback_context_id_from_cookie(self, app: Flask):
|
||||
api = DatasourceOAuthCallback()
|
||||
@@ -278,7 +387,14 @@ class TestDatasourceAuth:
|
||||
api = DatasourceAuth()
|
||||
method = inspect.unwrap(api.post)
|
||||
|
||||
payload = {"credentials": {"key": "val"}}
|
||||
payload = {
|
||||
"name": "Engineering Notion",
|
||||
"credentials": {
|
||||
"api_key": "secret-token",
|
||||
"workspace": "engineering",
|
||||
"database_id": "db-123",
|
||||
},
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
@@ -287,11 +403,17 @@ class TestDatasourceAuth:
|
||||
DatasourceProviderService,
|
||||
"add_datasource_api_key_provider",
|
||||
return_value=None,
|
||||
),
|
||||
) as add_api_key_provider,
|
||||
):
|
||||
response, status = method(api, "tenant-1", "notion")
|
||||
response, status = method(api, "tenant-1", _PROVIDER_ID)
|
||||
|
||||
assert response == _success_response()
|
||||
assert status == 200
|
||||
add_api_key_provider.assert_called_once()
|
||||
assert add_api_key_provider.call_args.kwargs["tenant_id"] == "tenant-1"
|
||||
assert str(add_api_key_provider.call_args.kwargs["provider_id"]) == _PROVIDER_ID
|
||||
assert add_api_key_provider.call_args.kwargs["credentials"] == payload["credentials"]
|
||||
assert add_api_key_provider.call_args.kwargs["name"] == "Engineering Notion"
|
||||
|
||||
def test_post_invalid_credentials(self, app: Flask):
|
||||
api = DatasourceAuth()
|
||||
@@ -321,19 +443,19 @@ class TestDatasourceAuth:
|
||||
patch.object(
|
||||
DatasourceProviderService,
|
||||
"list_datasource_credentials",
|
||||
return_value=[{"id": "1"}],
|
||||
return_value=[_datasource_credential()],
|
||||
),
|
||||
):
|
||||
response, status = method(api, "tenant-1", user, "notion")
|
||||
response, status = method(api, "tenant-1", user, _PROVIDER_ID)
|
||||
|
||||
assert status == 200
|
||||
assert response["result"]
|
||||
assert response == {"result": [_datasource_credential()]}
|
||||
|
||||
def test_post_missing_credentials(self, app: Flask):
|
||||
api = DatasourceAuth()
|
||||
method = inspect.unwrap(api.post)
|
||||
|
||||
payload = {}
|
||||
payload: dict[str, object] = {}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
@@ -375,17 +497,24 @@ class TestDatasourceAuthDeleteApi:
|
||||
DatasourceProviderService,
|
||||
"remove_datasource_credentials",
|
||||
return_value=None,
|
||||
),
|
||||
) as remove_datasource_credentials,
|
||||
):
|
||||
response, status = method(api, "tenant-1", "notion")
|
||||
response, status = method(api, "tenant-1", _PROVIDER_ID)
|
||||
|
||||
assert response == _success_response()
|
||||
assert status == 200
|
||||
remove_datasource_credentials.assert_called_once_with(
|
||||
tenant_id="tenant-1",
|
||||
auth_id="cred-1",
|
||||
provider="notion",
|
||||
plugin_id="langgenius/notion_datasource",
|
||||
)
|
||||
|
||||
def test_delete_missing_credential_id(self, app: Flask):
|
||||
api = DatasourceAuthDeleteApi()
|
||||
method = inspect.unwrap(api.post)
|
||||
|
||||
payload = {}
|
||||
payload: dict[str, object] = {}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
@@ -400,7 +529,11 @@ class TestDatasourceAuthUpdateApi:
|
||||
api = DatasourceAuthUpdateApi()
|
||||
method = inspect.unwrap(api.post)
|
||||
|
||||
payload = {"credential_id": "id", "credentials": {"k": "v"}}
|
||||
payload = {
|
||||
"credential_id": "cred-1",
|
||||
"name": "Updated Notion",
|
||||
"credentials": {"api_key": "new-secret", "database_id": "db-456"},
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
@@ -409,11 +542,20 @@ class TestDatasourceAuthUpdateApi:
|
||||
DatasourceProviderService,
|
||||
"update_datasource_credentials",
|
||||
return_value=None,
|
||||
),
|
||||
) as update_datasource_credentials,
|
||||
):
|
||||
response, status = method(api, "tenant-1", "notion")
|
||||
response, status = method(api, "tenant-1", _PROVIDER_ID)
|
||||
|
||||
assert response == _success_response()
|
||||
assert status == 201
|
||||
update_datasource_credentials.assert_called_once_with(
|
||||
tenant_id="tenant-1",
|
||||
auth_id="cred-1",
|
||||
provider="notion",
|
||||
plugin_id="langgenius/notion_datasource",
|
||||
credentials=payload["credentials"],
|
||||
name="Updated Notion",
|
||||
)
|
||||
|
||||
def test_update_with_credentials_none(self, app: Flask):
|
||||
api = DatasourceAuthUpdateApi()
|
||||
@@ -432,7 +574,9 @@ class TestDatasourceAuthUpdateApi:
|
||||
):
|
||||
response, status = method(api, "tenant-1", "notion")
|
||||
|
||||
assert response == _success_response()
|
||||
update_mock.assert_called_once()
|
||||
assert update_mock.call_args.kwargs["credentials"] == {}
|
||||
assert status == 201
|
||||
|
||||
def test_update_name_only(self, app: Flask):
|
||||
@@ -450,8 +594,9 @@ class TestDatasourceAuthUpdateApi:
|
||||
return_value=None,
|
||||
),
|
||||
):
|
||||
_, status = method(api, "tenant-1", "notion")
|
||||
response, status = method(api, "tenant-1", "notion")
|
||||
|
||||
assert response == _success_response()
|
||||
assert status == 201
|
||||
|
||||
def test_update_with_empty_credentials_dict(self, app: Flask):
|
||||
@@ -469,8 +614,9 @@ class TestDatasourceAuthUpdateApi:
|
||||
return_value=None,
|
||||
) as update_mock,
|
||||
):
|
||||
_, status = method(api, "tenant-1", "notion")
|
||||
response, status = method(api, "tenant-1", "notion")
|
||||
|
||||
assert response == _success_response()
|
||||
update_mock.assert_called_once()
|
||||
assert status == 201
|
||||
|
||||
@@ -485,12 +631,13 @@ class TestDatasourceAuthListApi:
|
||||
patch.object(
|
||||
DatasourceProviderService,
|
||||
"get_all_datasource_credentials",
|
||||
return_value=[{"id": "1"}],
|
||||
return_value=[_datasource_auth()],
|
||||
),
|
||||
):
|
||||
response, status = method(api, "tenant-1")
|
||||
|
||||
assert status == 200
|
||||
assert response == {"result": [_datasource_auth()]}
|
||||
|
||||
def test_auth_list_empty(self, app: Flask):
|
||||
api = DatasourceAuthListApi()
|
||||
@@ -537,7 +684,7 @@ class TestDatasourceHardCodeAuthListApi:
|
||||
patch.object(
|
||||
DatasourceProviderService,
|
||||
"get_hard_code_datasource_credentials",
|
||||
return_value=[{"id": "1"}],
|
||||
return_value=[_datasource_auth()],
|
||||
),
|
||||
):
|
||||
response, status = method(api, "tenant-1")
|
||||
@@ -550,7 +697,14 @@ class TestDatasourceAuthOauthCustomClient:
|
||||
api = DatasourceAuthOauthCustomClient()
|
||||
method = inspect.unwrap(api.post)
|
||||
|
||||
payload = {"client_params": {}, "enable_oauth_custom_client": True}
|
||||
payload = {
|
||||
"client_params": {
|
||||
"client_id": "custom-client",
|
||||
"client_secret": "custom-secret",
|
||||
"authorize_url": "https://auth.example.com/authorize",
|
||||
},
|
||||
"enable_oauth_custom_client": True,
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
@@ -559,11 +713,17 @@ class TestDatasourceAuthOauthCustomClient:
|
||||
DatasourceProviderService,
|
||||
"setup_oauth_custom_client_params",
|
||||
return_value=None,
|
||||
),
|
||||
) as setup_custom_client,
|
||||
):
|
||||
response, status = method(api, "tenant-1", "notion")
|
||||
response, status = method(api, "tenant-1", _PROVIDER_ID)
|
||||
|
||||
assert response == _success_response()
|
||||
assert status == 200
|
||||
setup_custom_client.assert_called_once()
|
||||
assert setup_custom_client.call_args.kwargs["tenant_id"] == "tenant-1"
|
||||
assert str(setup_custom_client.call_args.kwargs["datasource_provider_id"]) == _PROVIDER_ID
|
||||
assert setup_custom_client.call_args.kwargs["client_params"] == payload["client_params"]
|
||||
assert setup_custom_client.call_args.kwargs["enabled"] is True
|
||||
|
||||
def test_delete_success(self, app: Flask):
|
||||
api = DatasourceAuthOauthCustomClient()
|
||||
@@ -575,17 +735,20 @@ class TestDatasourceAuthOauthCustomClient:
|
||||
DatasourceProviderService,
|
||||
"remove_oauth_custom_client_params",
|
||||
return_value=None,
|
||||
),
|
||||
) as remove_custom_client,
|
||||
):
|
||||
response, status = method(api, "tenant-1", "notion")
|
||||
response, status = method(api, "tenant-1", _PROVIDER_ID)
|
||||
|
||||
assert response == _success_response()
|
||||
assert status == 200
|
||||
remove_custom_client.assert_called_once()
|
||||
assert str(remove_custom_client.call_args.kwargs["datasource_provider_id"]) == _PROVIDER_ID
|
||||
|
||||
def test_post_empty_payload(self, app: Flask):
|
||||
api = DatasourceAuthOauthCustomClient()
|
||||
method = inspect.unwrap(api.post)
|
||||
|
||||
payload = {}
|
||||
payload: dict[str, object] = {}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
@@ -596,8 +759,9 @@ class TestDatasourceAuthOauthCustomClient:
|
||||
return_value=None,
|
||||
),
|
||||
):
|
||||
_, status = method(api, "tenant-1", "notion")
|
||||
response, status = method(api, "tenant-1", "notion")
|
||||
|
||||
assert response == _success_response()
|
||||
assert status == 200
|
||||
|
||||
def test_post_disabled_flag(self, app: Flask):
|
||||
@@ -618,9 +782,12 @@ class TestDatasourceAuthOauthCustomClient:
|
||||
return_value=None,
|
||||
) as setup_mock,
|
||||
):
|
||||
_, status = method(api, "tenant-1", "notion")
|
||||
response, status = method(api, "tenant-1", "notion")
|
||||
|
||||
assert response == _success_response()
|
||||
setup_mock.assert_called_once()
|
||||
assert setup_mock.call_args.kwargs["client_params"] == {"a": 1}
|
||||
assert setup_mock.call_args.kwargs["enabled"] is False
|
||||
assert status == 200
|
||||
|
||||
|
||||
@@ -638,17 +805,22 @@ class TestDatasourceAuthDefaultApi:
|
||||
DatasourceProviderService,
|
||||
"set_default_datasource_provider",
|
||||
return_value=None,
|
||||
),
|
||||
) as set_default_datasource_provider,
|
||||
):
|
||||
response, status = method(api, "tenant-1", "notion")
|
||||
response, status = method(api, "tenant-1", _PROVIDER_ID)
|
||||
|
||||
assert response == _success_response()
|
||||
assert status == 200
|
||||
set_default_datasource_provider.assert_called_once()
|
||||
assert set_default_datasource_provider.call_args.kwargs["tenant_id"] == "tenant-1"
|
||||
assert str(set_default_datasource_provider.call_args.kwargs["datasource_provider_id"]) == _PROVIDER_ID
|
||||
assert set_default_datasource_provider.call_args.kwargs["credential_id"] == "cred-1"
|
||||
|
||||
def test_default_missing_id(self, app: Flask):
|
||||
api = DatasourceAuthDefaultApi()
|
||||
method = inspect.unwrap(api.post)
|
||||
|
||||
payload = {}
|
||||
payload: dict[str, object] = {}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
@@ -663,7 +835,7 @@ class TestDatasourceUpdateProviderNameApi:
|
||||
api = DatasourceUpdateProviderNameApi()
|
||||
method = inspect.unwrap(api.post)
|
||||
|
||||
payload = {"credential_id": "id", "name": "New Name"}
|
||||
payload = {"credential_id": "cred-1", "name": "New Name"}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
@@ -672,11 +844,17 @@ class TestDatasourceUpdateProviderNameApi:
|
||||
DatasourceProviderService,
|
||||
"update_datasource_provider_name",
|
||||
return_value=None,
|
||||
),
|
||||
) as update_datasource_provider_name,
|
||||
):
|
||||
response, status = method(api, "tenant-1", "notion")
|
||||
response, status = method(api, "tenant-1", _PROVIDER_ID)
|
||||
|
||||
assert response == _success_response()
|
||||
assert status == 200
|
||||
update_datasource_provider_name.assert_called_once()
|
||||
assert update_datasource_provider_name.call_args.kwargs["tenant_id"] == "tenant-1"
|
||||
assert str(update_datasource_provider_name.call_args.kwargs["datasource_provider_id"]) == _PROVIDER_ID
|
||||
assert update_datasource_provider_name.call_args.kwargs["name"] == "New Name"
|
||||
assert update_datasource_provider_name.call_args.kwargs["credential_id"] == "cred-1"
|
||||
|
||||
def test_update_name_too_long(self, app: Flask):
|
||||
api = DatasourceUpdateProviderNameApi()
|
||||
|
||||
+62
@@ -158,3 +158,65 @@ def test_rag_pipeline_workflow_patch_serializes_response_model(app: Flask, monke
|
||||
assert response["id"] == "workflow-1"
|
||||
assert response["marked_name"] == "Updated release"
|
||||
assert response["hash"] == "hash-1"
|
||||
|
||||
|
||||
def test_default_rag_pipeline_block_configs_serializes_root_response(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
block_configs = [{"type": "start", "config": {"title": "Start"}}]
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"RagPipelineService",
|
||||
lambda: SimpleNamespace(get_default_block_configs=lambda: block_configs),
|
||||
)
|
||||
|
||||
api = module.DefaultRagPipelineBlockConfigsApi()
|
||||
handler = unwrap_all(api.get)
|
||||
|
||||
response = handler(api, _pipeline())
|
||||
|
||||
assert response == block_configs
|
||||
|
||||
|
||||
def test_draft_rag_pipeline_second_step_parameters_serializes_variables(app, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
variables = [
|
||||
{
|
||||
"belong_to_node_id": "shared",
|
||||
"type": "number",
|
||||
"label": "Chunk size",
|
||||
"variable": "chunk_size",
|
||||
"default_value": 1024,
|
||||
"required": True,
|
||||
}
|
||||
]
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"RagPipelineService",
|
||||
lambda: SimpleNamespace(get_second_step_parameters=lambda **_kwargs: variables),
|
||||
)
|
||||
|
||||
api = module.DraftRagPipelineSecondStepApi()
|
||||
handler = unwrap_all(api.get)
|
||||
|
||||
with app.test_request_context("/?node_id=node-1"):
|
||||
response = handler(api, _pipeline())
|
||||
|
||||
assert response["variables"] == variables
|
||||
|
||||
|
||||
def test_rag_pipeline_recommended_plugins_serializes_known_envelope(app, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
recommended_plugins = {
|
||||
"installed_recommended_plugins": [{"name": "Dify Extractor", "meta": {"version": "1.0.0"}}],
|
||||
"uninstalled_recommended_plugins": [{"plugin_id": "langgenius/notion_datasource"}],
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"RagPipelineService",
|
||||
lambda: SimpleNamespace(get_recommended_plugins=lambda *_args: recommended_plugins),
|
||||
)
|
||||
|
||||
api = module.RagPipelineRecommendedPluginApi()
|
||||
handler = unwrap_all(api.get)
|
||||
|
||||
with app.test_request_context("/?type=tool"):
|
||||
response = handler(api, "tenant-1", _account())
|
||||
|
||||
assert response == recommended_plugins
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import inspect
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@@ -16,9 +17,39 @@ from controllers.console.workspace.endpoint import (
|
||||
EndpointListApi,
|
||||
EndpointListForSinglePluginApi,
|
||||
)
|
||||
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
|
||||
from core.plugin.entities.endpoint import EndpointEntityWithInstance, EndpointProviderDeclaration
|
||||
from core.plugin.impl.exc import PluginPermissionDeniedError
|
||||
|
||||
|
||||
def _endpoint_entity() -> EndpointEntityWithInstance:
|
||||
now = datetime(2026, 1, 1, tzinfo=UTC)
|
||||
return EndpointEntityWithInstance(
|
||||
id="e1",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
tenant_id="t1",
|
||||
plugin_id="p1",
|
||||
settings={
|
||||
"api_key": "pl********et",
|
||||
"enabled": True,
|
||||
"ids": ["a", "b"],
|
||||
"nested": {"limit": 3},
|
||||
},
|
||||
expired_at=now,
|
||||
declaration=EndpointProviderDeclaration(
|
||||
settings=[
|
||||
ProviderConfig(type=ProviderConfigType.SECRET_INPUT, name="api_key"),
|
||||
ProviderConfig(type=ProviderConfigType.BOOLEAN, name="enabled"),
|
||||
]
|
||||
),
|
||||
name="endpoint",
|
||||
enabled=True,
|
||||
url="https://example.test/hook-1",
|
||||
hook_id="hook-1",
|
||||
)
|
||||
|
||||
|
||||
class TestEndpointCollectionApi:
|
||||
def test_create_success(self, app: Flask):
|
||||
api = EndpointCollectionApi()
|
||||
@@ -99,15 +130,40 @@ class TestEndpointListApi:
|
||||
def test_list_success(self, app: Flask):
|
||||
api = EndpointListApi()
|
||||
method = inspect.unwrap(api.get)
|
||||
endpoint_entity = _endpoint_entity()
|
||||
|
||||
with (
|
||||
app.test_request_context("/?page=1&page_size=10"),
|
||||
patch("controllers.console.workspace.endpoint.EndpointService.list_endpoints", return_value=[{"id": "e1"}]),
|
||||
patch(
|
||||
"controllers.console.workspace.endpoint.EndpointService.list_endpoints",
|
||||
return_value=[endpoint_entity],
|
||||
),
|
||||
):
|
||||
result = method(api, "t1", "u1")
|
||||
|
||||
assert "endpoints" in result
|
||||
assert len(result["endpoints"]) == 1
|
||||
endpoint = result["endpoints"][0]
|
||||
assert endpoint["id"] == "e1"
|
||||
assert endpoint["created_at"] == "2026-01-01T00:00:00Z"
|
||||
assert endpoint["updated_at"] == "2026-01-01T00:00:00Z"
|
||||
assert endpoint["settings"] == {
|
||||
"api_key": "pl********et",
|
||||
"enabled": True,
|
||||
"ids": ["a", "b"],
|
||||
"nested": {"limit": 3},
|
||||
}
|
||||
assert endpoint["tenant_id"] == "t1"
|
||||
assert endpoint["plugin_id"] == "p1"
|
||||
assert endpoint["expired_at"] == "2026-01-01T00:00:00Z"
|
||||
assert endpoint["declaration"]["settings"][0]["type"] == "secret-input"
|
||||
assert endpoint["declaration"]["settings"][0]["name"] == "api_key"
|
||||
assert endpoint["declaration"]["settings"][1]["type"] == "boolean"
|
||||
assert endpoint["declaration"]["settings"][1]["name"] == "enabled"
|
||||
assert endpoint["declaration"]["endpoints"] == []
|
||||
assert endpoint["name"] == "endpoint"
|
||||
assert endpoint["enabled"] is True
|
||||
assert endpoint["url"] == "https://example.test/hook-1"
|
||||
assert endpoint["hook_id"] == "hook-1"
|
||||
assert endpoint_entity.settings["api_key"] == "pl********et"
|
||||
|
||||
def test_list_invalid_query(self, app: Flask):
|
||||
api = EndpointListApi()
|
||||
@@ -129,12 +185,14 @@ class TestEndpointListForSinglePluginApi:
|
||||
app.test_request_context("/?page=1&page_size=10&plugin_id=p1"),
|
||||
patch(
|
||||
"controllers.console.workspace.endpoint.EndpointService.list_endpoints_for_single_plugin",
|
||||
return_value=[{"id": "e1"}],
|
||||
return_value=[_endpoint_entity()],
|
||||
),
|
||||
):
|
||||
result = method(api, "t1", "u1")
|
||||
|
||||
assert "endpoints" in result
|
||||
assert result["endpoints"][0]["id"] == "e1"
|
||||
assert result["endpoints"][0]["settings"]["api_key"] == "pl********et"
|
||||
assert result["endpoints"][0]["settings"]["nested"] == {"limit": 3}
|
||||
|
||||
def test_list_for_plugin_missing_param(self, app: Flask):
|
||||
api = EndpointListForSinglePluginApi()
|
||||
|
||||
@@ -130,7 +130,7 @@ class TestMemberInviteEmailApi:
|
||||
features.workspace_members.is_available.return_value = True
|
||||
|
||||
payload = {
|
||||
"emails": ["a@test.com"],
|
||||
"emails": ["A@TEST.com", "a@test.com"],
|
||||
"role": "normal",
|
||||
"language": "en-US",
|
||||
}
|
||||
@@ -138,8 +138,10 @@ class TestMemberInviteEmailApi:
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
patch("controllers.console.workspace.members.FeatureService.get_features", return_value=features),
|
||||
patch("controllers.console.workspace.members._count_new_member_invites", return_value=1),
|
||||
patch("controllers.console.workspace.members.RegisterService.invite_new_member", return_value="token"),
|
||||
patch("controllers.console.workspace.members._count_new_member_invites", return_value=1) as mock_count,
|
||||
patch(
|
||||
"controllers.console.workspace.members.RegisterService.invite_new_member", return_value="token"
|
||||
) as mock_invite,
|
||||
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "http://x"),
|
||||
patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", False),
|
||||
patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
|
||||
@@ -148,6 +150,10 @@ class TestMemberInviteEmailApi:
|
||||
|
||||
assert status == 201
|
||||
assert result["result"] == "success"
|
||||
assert result["invitation_results"][0]["email"] == "a@test.com"
|
||||
mock_count.assert_not_called()
|
||||
mock_invite.assert_called_once()
|
||||
assert mock_invite.call_args.kwargs["email"] == "a@test.com"
|
||||
|
||||
def test_invite_limit_exceeded(self, app: Flask):
|
||||
api = MemberInviteEmailApi()
|
||||
|
||||
@@ -44,7 +44,15 @@ from controllers.console.workspace.plugin import (
|
||||
)
|
||||
from core.plugin.entities.plugin import PluginInstallation
|
||||
from core.plugin.impl.exc import PluginDaemonClientSideError
|
||||
from models.account import Account, TenantAccountRole, TenantPluginAutoUpgradeStrategy, TenantPluginPermission
|
||||
from models.account import (
|
||||
Account,
|
||||
TenantAccountRole,
|
||||
TenantPluginAutoUpgradeCategory,
|
||||
TenantPluginAutoUpgradeMode,
|
||||
TenantPluginAutoUpgradeStrategySetting,
|
||||
TenantPluginDebugPermission,
|
||||
TenantPluginInstallPermission,
|
||||
)
|
||||
|
||||
|
||||
def _plugin_category_list_item(category: str = "tool") -> dict[str, Any]:
|
||||
@@ -393,8 +401,8 @@ class TestPluginChangePermissionApi:
|
||||
user = _account(TenantAccountRole.NORMAL)
|
||||
|
||||
payload = {
|
||||
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
|
||||
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
|
||||
"install_permission": TenantPluginInstallPermission.EVERYONE,
|
||||
"debug_permission": TenantPluginDebugPermission.EVERYONE,
|
||||
}
|
||||
|
||||
with (
|
||||
@@ -410,8 +418,8 @@ class TestPluginChangePermissionApi:
|
||||
user = _account()
|
||||
|
||||
payload = {
|
||||
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
|
||||
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
|
||||
"install_permission": TenantPluginInstallPermission.EVERYONE,
|
||||
"debug_permission": TenantPluginDebugPermission.EVERYONE,
|
||||
}
|
||||
|
||||
with (
|
||||
@@ -1020,11 +1028,11 @@ class TestPluginChangeAutoUpgradeApi:
|
||||
user = _account()
|
||||
|
||||
payload = {
|
||||
"category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value,
|
||||
"category": TenantPluginAutoUpgradeCategory.TOOL.value,
|
||||
"auto_upgrade": {
|
||||
"strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
"strategy_setting": TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
|
||||
"upgrade_time_of_day": 0,
|
||||
"upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
"upgrade_mode": TenantPluginAutoUpgradeMode.EXCLUDE,
|
||||
"exclude_plugins": [],
|
||||
"include_plugins": [],
|
||||
},
|
||||
@@ -1048,11 +1056,11 @@ class TestPluginChangeAutoUpgradeApi:
|
||||
user = _account()
|
||||
|
||||
payload = {
|
||||
"category": TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL.value,
|
||||
"category": TenantPluginAutoUpgradeCategory.MODEL.value,
|
||||
"auto_upgrade": {
|
||||
"strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST,
|
||||
"strategy_setting": TenantPluginAutoUpgradeStrategySetting.LATEST,
|
||||
"upgrade_time_of_day": 3600,
|
||||
"upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
|
||||
"upgrade_mode": TenantPluginAutoUpgradeMode.ALL,
|
||||
"exclude_plugins": [],
|
||||
"include_plugins": [],
|
||||
},
|
||||
@@ -1068,7 +1076,7 @@ class TestPluginChangeAutoUpgradeApi:
|
||||
|
||||
assert result["success"] is True
|
||||
change.assert_called_once()
|
||||
assert change.call_args.kwargs["category"] == TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL
|
||||
assert change.call_args.kwargs["category"] == TenantPluginAutoUpgradeCategory.MODEL
|
||||
|
||||
def test_auto_upgrade_fail(self, app: Flask):
|
||||
api = PluginChangeAutoUpgradeApi()
|
||||
@@ -1077,11 +1085,11 @@ class TestPluginChangeAutoUpgradeApi:
|
||||
user = MagicMock(is_admin_or_owner=True)
|
||||
|
||||
payload = {
|
||||
"category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value,
|
||||
"category": TenantPluginAutoUpgradeCategory.TOOL.value,
|
||||
"auto_upgrade": {
|
||||
"strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
"strategy_setting": TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
|
||||
"upgrade_time_of_day": 0,
|
||||
"upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
"upgrade_mode": TenantPluginAutoUpgradeMode.EXCLUDE,
|
||||
"exclude_plugins": [],
|
||||
"include_plugins": [],
|
||||
},
|
||||
@@ -1102,16 +1110,16 @@ class TestPluginFetchAutoUpgradeApi:
|
||||
method = unwrap(api.get)
|
||||
|
||||
auto_upgrade = MagicMock(
|
||||
category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
category=TenantPluginAutoUpgradeCategory.TOOL,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
|
||||
upgrade_time_of_day=1,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
upgrade_mode=TenantPluginAutoUpgradeMode.EXCLUDE,
|
||||
exclude_plugins=[],
|
||||
include_plugins=[],
|
||||
)
|
||||
|
||||
with (
|
||||
app.test_request_context(f"/?category={TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value}"),
|
||||
app.test_request_context(f"/?category={TenantPluginAutoUpgradeCategory.TOOL.value}"),
|
||||
patch(
|
||||
"controllers.console.workspace.plugin.PluginAutoUpgradeService.get_strategy",
|
||||
return_value=auto_upgrade,
|
||||
@@ -1119,7 +1127,7 @@ class TestPluginFetchAutoUpgradeApi:
|
||||
):
|
||||
result = method(api, "t1")
|
||||
|
||||
assert result["category"] == TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL
|
||||
assert result["category"] == TenantPluginAutoUpgradeCategory.TOOL
|
||||
assert result["auto_upgrade"]["upgrade_time_of_day"] == 1
|
||||
|
||||
|
||||
@@ -1128,7 +1136,7 @@ class TestPluginAutoUpgradeExcludePluginApi:
|
||||
api = PluginAutoUpgradeExcludePluginApi()
|
||||
method = unwrap(api.post)
|
||||
|
||||
payload = {"plugin_id": "p", "category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value}
|
||||
payload = {"plugin_id": "p", "category": TenantPluginAutoUpgradeCategory.TOOL.value}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
@@ -1142,7 +1150,7 @@ class TestPluginAutoUpgradeExcludePluginApi:
|
||||
api = PluginAutoUpgradeExcludePluginApi()
|
||||
method = unwrap(api.post)
|
||||
|
||||
payload = {"plugin_id": "p", "category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value}
|
||||
payload = {"plugin_id": "p", "category": TenantPluginAutoUpgradeCategory.TOOL.value}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import inspect
|
||||
import logging
|
||||
from http import HTTPStatus
|
||||
from io import BytesIO
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -26,7 +27,9 @@ from controllers.console.workspace.workspace import (
|
||||
WebappLogoWorkspaceApi,
|
||||
WorkspaceInfoApi,
|
||||
WorkspaceListApi,
|
||||
WorkspaceLogoUploadResponse,
|
||||
WorkspacePermissionApi,
|
||||
WorkspacePermissionResponse,
|
||||
)
|
||||
from enums.cloud_plan import CloudPlan
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
@@ -99,7 +102,7 @@ class TestTenantListApi:
|
||||
):
|
||||
result, status = method(api, "t1", user)
|
||||
|
||||
assert status == 200
|
||||
assert status == HTTPStatus.OK
|
||||
assert len(result["workspaces"]) == 2
|
||||
assert result["workspaces"][0]["current"] is True
|
||||
assert result["workspaces"][0]["plan"] == CloudPlan.TEAM
|
||||
@@ -146,7 +149,7 @@ class TestTenantListApi:
|
||||
):
|
||||
result, status = method(api, "t1", user)
|
||||
|
||||
assert status == 200
|
||||
assert status == HTTPStatus.OK
|
||||
assert result["workspaces"][0]["plan"] == CloudPlan.TEAM
|
||||
assert result["workspaces"][1]["plan"] == CloudPlan.PROFESSIONAL
|
||||
get_plan_bulk_mock.assert_called_once_with(["t1", "t2"])
|
||||
@@ -192,7 +195,7 @@ class TestTenantListApi:
|
||||
):
|
||||
result, status = method(api, "t2", user)
|
||||
|
||||
assert status == 200
|
||||
assert status == HTTPStatus.OK
|
||||
assert result["workspaces"][0]["plan"] == CloudPlan.TEAM
|
||||
assert result["workspaces"][1]["plan"] == CloudPlan.TEAM
|
||||
get_plan_bulk_mock.assert_called_once_with(["t1", "t2"])
|
||||
@@ -226,7 +229,7 @@ class TestTenantListApi:
|
||||
):
|
||||
result, status = method(api, "t1", user)
|
||||
|
||||
assert status == 200
|
||||
assert status == HTTPStatus.OK
|
||||
assert result["workspaces"][0]["plan"] == CloudPlan.SANDBOX
|
||||
get_features_mock.assert_called_once_with("t1", exclude_vector_space=True)
|
||||
|
||||
@@ -251,7 +254,7 @@ class TestTenantListApi:
|
||||
):
|
||||
result, status = method(api, "t2", user)
|
||||
|
||||
assert status == 200
|
||||
assert status == HTTPStatus.OK
|
||||
assert result["workspaces"][0]["plan"] == CloudPlan.SANDBOX
|
||||
assert result["workspaces"][1]["plan"] == CloudPlan.SANDBOX
|
||||
assert result["workspaces"][0]["current"] is False
|
||||
@@ -276,7 +279,7 @@ class TestTenantListApi:
|
||||
):
|
||||
result, status = method(api, None, user)
|
||||
|
||||
assert status == 200
|
||||
assert status == HTTPStatus.OK
|
||||
assert result["workspaces"] == []
|
||||
get_features_mock.assert_not_called()
|
||||
|
||||
@@ -295,7 +298,7 @@ class TestWorkspaceListApi:
|
||||
):
|
||||
result, status = method(api)
|
||||
|
||||
assert status == 200
|
||||
assert status == HTTPStatus.OK
|
||||
assert result["total"] == 1
|
||||
assert result["has_more"] is False
|
||||
|
||||
@@ -312,7 +315,7 @@ class TestWorkspaceListApi:
|
||||
):
|
||||
result, status = method(api)
|
||||
|
||||
assert status == 200
|
||||
assert status == HTTPStatus.OK
|
||||
assert result["has_more"] is True
|
||||
|
||||
|
||||
@@ -332,7 +335,7 @@ class TestTenantApi:
|
||||
):
|
||||
result, status = method(api, user)
|
||||
|
||||
assert status == 200
|
||||
assert status == HTTPStatus.OK
|
||||
assert result["id"] == "t1"
|
||||
|
||||
def test_post_archived_with_switch(self, app: Flask):
|
||||
@@ -386,7 +389,7 @@ class TestTenantApi:
|
||||
result, status = method(api, user)
|
||||
|
||||
assert "Deprecated URL /info was used." in caplog.messages
|
||||
assert status == 200
|
||||
assert status == HTTPStatus.OK
|
||||
|
||||
|
||||
class TestTenantInfoResponse:
|
||||
@@ -586,8 +589,9 @@ class TestWebappLogoWorkspaceApi:
|
||||
|
||||
result, status = method(api, user)
|
||||
|
||||
assert status == 201
|
||||
assert result["id"] == "file1"
|
||||
assert status == HTTPStatus.CREATED
|
||||
assert result == {"id": "file1"}
|
||||
assert WorkspaceLogoUploadResponse.model_validate(result).model_dump(mode="json") == {"id": "file1"}
|
||||
|
||||
def test_filename_missing(self, app: Flask):
|
||||
api = WebappLogoWorkspaceApi()
|
||||
@@ -676,7 +680,7 @@ class TestWorkspaceInfoApi:
|
||||
patch("controllers.console.workspace.workspace.db.session.commit"),
|
||||
patch(
|
||||
"controllers.console.workspace.workspace.WorkspaceService.get_tenant_info",
|
||||
return_value={"name": "New Name"},
|
||||
return_value={"id": "t1", "name": "New Name"},
|
||||
),
|
||||
):
|
||||
result = method(api, "t1")
|
||||
@@ -716,8 +720,14 @@ class TestWorkspacePermissionApi:
|
||||
):
|
||||
result, status = method(api, "t1")
|
||||
|
||||
assert status == 200
|
||||
assert result["workspace_id"] == "t1"
|
||||
assert status == HTTPStatus.OK
|
||||
expected = {
|
||||
"workspace_id": "t1",
|
||||
"allow_member_invite": True,
|
||||
"allow_owner_transfer": False,
|
||||
}
|
||||
assert result == expected
|
||||
assert WorkspacePermissionResponse.model_validate(result).model_dump(mode="json") == expected
|
||||
|
||||
def test_no_current_tenant(self, app: Flask):
|
||||
api = WorkspacePermissionApi()
|
||||
|
||||
+41
-6
@@ -325,10 +325,12 @@ class TestPipelineRunApiEntity:
|
||||
def test_entity_missing_required_field(self):
|
||||
"""Test entity raises on missing required field."""
|
||||
with pytest.raises(ValueError):
|
||||
PipelineRunApiEntity(
|
||||
inputs={},
|
||||
datasource_type="online_document",
|
||||
# missing datasource_info_list, start_node_id, etc.
|
||||
PipelineRunApiEntity.model_validate(
|
||||
{
|
||||
"inputs": {},
|
||||
"datasource_type": "online_document",
|
||||
# missing datasource_info_list, start_node_id, etc.
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -382,8 +384,19 @@ class TestDatasourcePluginsApiGet:
|
||||
mock_dataset = Mock()
|
||||
mock_db.session.scalar.return_value = mock_dataset
|
||||
|
||||
datasource_plugins = [
|
||||
{
|
||||
"node_id": "node-datasource-1",
|
||||
"plugin_id": "plugin-a",
|
||||
"provider_name": "provider-a",
|
||||
"datasource_type": "online_document",
|
||||
"title": "Online Docs",
|
||||
"user_input_variables": [{"variable": "url", "label": "URL", "type": "text-input", "required": True}],
|
||||
"credentials": [{"id": "cred-1", "name": "Default credential", "type": "oauth2", "is_default": True}],
|
||||
}
|
||||
]
|
||||
mock_svc_instance = Mock()
|
||||
mock_svc_instance.get_datasource_plugins.return_value = [{"name": "plugin_a"}]
|
||||
mock_svc_instance.get_datasource_plugins.return_value = datasource_plugins
|
||||
mock_svc_cls.return_value = mock_svc_instance
|
||||
|
||||
with app.test_request_context("/datasets/test/pipeline/datasource-plugins?is_published=true"):
|
||||
@@ -391,11 +404,33 @@ class TestDatasourcePluginsApiGet:
|
||||
response, status = api.get(tenant_id=tenant_id, dataset_id=dataset_id)
|
||||
|
||||
assert status == 200
|
||||
assert response == [{"name": "plugin_a"}]
|
||||
assert response == datasource_plugins
|
||||
mock_svc_instance.get_datasource_plugins.assert_called_once_with(
|
||||
tenant_id=tenant_id, dataset_id=dataset_id, is_published=True
|
||||
)
|
||||
|
||||
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.db")
|
||||
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.RagPipelineService")
|
||||
def test_get_plugins_parses_false_is_published_query(self, mock_svc_cls, mock_db, app: Flask):
|
||||
"""Test false query string is parsed as boolean False."""
|
||||
tenant_id = str(uuid.uuid4())
|
||||
dataset_id = str(uuid.uuid4())
|
||||
|
||||
mock_db.session.scalar.return_value = Mock()
|
||||
mock_svc_instance = Mock()
|
||||
mock_svc_instance.get_datasource_plugins.return_value = []
|
||||
mock_svc_cls.return_value = mock_svc_instance
|
||||
|
||||
with app.test_request_context("/datasets/test/pipeline/datasource-plugins?is_published=false"):
|
||||
api = DatasourcePluginsApi()
|
||||
response, status = api.get(tenant_id=tenant_id, dataset_id=dataset_id)
|
||||
|
||||
assert status == 200
|
||||
assert response == []
|
||||
mock_svc_instance.get_datasource_plugins.assert_called_once_with(
|
||||
tenant_id=tenant_id, dataset_id=dataset_id, is_published=False
|
||||
)
|
||||
|
||||
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.db")
|
||||
def test_get_plugins_not_found(self, mock_db, app: Flask):
|
||||
"""Test NotFound when dataset check fails."""
|
||||
|
||||
+5
-20
@@ -2,9 +2,10 @@
|
||||
Unit tests for Service API knowledge pipeline file-upload serialization.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow import PipelineUploadFileResponse
|
||||
from libs.helper import dump_response
|
||||
|
||||
|
||||
class FakeUploadFile:
|
||||
@@ -17,21 +18,7 @@ class FakeUploadFile:
|
||||
created_at: datetime | None
|
||||
|
||||
|
||||
def _load_serialize_upload_file():
|
||||
api_dir = Path(__file__).resolve().parents[5]
|
||||
serializers_path = api_dir / "controllers" / "service_api" / "dataset" / "rag_pipeline" / "serializers.py"
|
||||
|
||||
spec = importlib.util.spec_from_file_location("rag_pipeline_serializers", serializers_path)
|
||||
assert spec
|
||||
assert spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module) # type: ignore[attr-defined]
|
||||
return module.serialize_upload_file
|
||||
|
||||
|
||||
def test_file_upload_created_at_is_isoformat_string():
|
||||
serialize_upload_file = _load_serialize_upload_file()
|
||||
|
||||
created_at = datetime(2026, 2, 8, 12, 0, 0, tzinfo=UTC)
|
||||
upload_file = FakeUploadFile()
|
||||
upload_file.id = "file-1"
|
||||
@@ -42,13 +29,11 @@ def test_file_upload_created_at_is_isoformat_string():
|
||||
upload_file.created_by = "account-1"
|
||||
upload_file.created_at = created_at
|
||||
|
||||
result = serialize_upload_file(upload_file)
|
||||
result = dump_response(PipelineUploadFileResponse, upload_file)
|
||||
assert result["created_at"] == created_at.isoformat()
|
||||
|
||||
|
||||
def test_file_upload_created_at_none_serializes_to_null():
|
||||
serialize_upload_file = _load_serialize_upload_file()
|
||||
|
||||
upload_file = FakeUploadFile()
|
||||
upload_file.id = "file-1"
|
||||
upload_file.name = "test.pdf"
|
||||
@@ -58,5 +43,5 @@ def test_file_upload_created_at_none_serializes_to_null():
|
||||
upload_file.created_by = "account-1"
|
||||
upload_file.created_at = None
|
||||
|
||||
result = serialize_upload_file(upload_file)
|
||||
result = dump_response(PipelineUploadFileResponse, upload_file)
|
||||
assert result["created_at"] is None
|
||||
|
||||
@@ -34,12 +34,11 @@ def test_decode_enterprise_webapp_user_id_valid(monkeypatch: pytest.MonkeyPatch)
|
||||
def test_exchange_token_public_flow(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
site = SimpleNamespace(id="s1", app_id="a1", code="code", status="normal")
|
||||
app_model = SimpleNamespace(id="a1", status="normal", enable_site=True)
|
||||
call_state = {"calls": 0}
|
||||
|
||||
def _scalar_side_effect(*_args, **_kwargs):
|
||||
if not hasattr(_scalar_side_effect, "calls"):
|
||||
_scalar_side_effect.calls = 0
|
||||
_scalar_side_effect.calls += 1
|
||||
return site if _scalar_side_effect.calls == 1 else app_model
|
||||
call_state["calls"] += 1
|
||||
return site if call_state["calls"] == 1 else app_model
|
||||
|
||||
db_session = SimpleNamespace(scalar=_scalar_side_effect)
|
||||
monkeypatch.setattr("controllers.web.passport.db", SimpleNamespace(session=db_session))
|
||||
@@ -53,12 +52,11 @@ def test_exchange_token_public_flow(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_exchange_token_requires_external(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
site = SimpleNamespace(id="s1", app_id="a1", code="code", status="normal")
|
||||
app_model = SimpleNamespace(id="a1", status="normal", enable_site=True)
|
||||
call_state = {"calls": 0}
|
||||
|
||||
def _scalar_side_effect(*_args, **_kwargs):
|
||||
if not hasattr(_scalar_side_effect, "calls"):
|
||||
_scalar_side_effect.calls = 0
|
||||
_scalar_side_effect.calls += 1
|
||||
return site if _scalar_side_effect.calls == 1 else app_model
|
||||
call_state["calls"] += 1
|
||||
return site if call_state["calls"] == 1 else app_model
|
||||
|
||||
db_session = SimpleNamespace(scalar=_scalar_side_effect)
|
||||
monkeypatch.setattr("controllers.web.passport.db", SimpleNamespace(session=db_session))
|
||||
@@ -71,14 +69,13 @@ def test_exchange_token_requires_external(monkeypatch: pytest.MonkeyPatch) -> No
|
||||
def test_exchange_token_missing_session_id(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
site = SimpleNamespace(id="s1", app_id="a1", code="code", status="normal")
|
||||
app_model = SimpleNamespace(id="a1", status="normal", enable_site=True, tenant_id="t1")
|
||||
call_state = {"calls": 0}
|
||||
|
||||
def _scalar_side_effect(*_args, **_kwargs):
|
||||
if not hasattr(_scalar_side_effect, "calls"):
|
||||
_scalar_side_effect.calls = 0
|
||||
_scalar_side_effect.calls += 1
|
||||
if _scalar_side_effect.calls == 1:
|
||||
call_state["calls"] += 1
|
||||
if call_state["calls"] == 1:
|
||||
return site
|
||||
if _scalar_side_effect.calls == 2:
|
||||
if call_state["calls"] == 2:
|
||||
return app_model
|
||||
return None
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ class TestEmailCodeLoginApi:
|
||||
):
|
||||
response = EmailCodeLoginApi().post()
|
||||
|
||||
assert response.get_json() == {"result": "success", "data": {"access_token": "new-access-token"}}
|
||||
assert response == {"result": "success", "data": {"access_token": "new-access-token"}}
|
||||
mock_get_user.assert_called_once_with("User@Example.com")
|
||||
mock_revoke_token.assert_called_once_with("token-123")
|
||||
mock_login.assert_called_once()
|
||||
@@ -115,7 +115,7 @@ class TestLoginApi:
|
||||
):
|
||||
response = LoginApi().post()
|
||||
|
||||
assert response.get_json()["data"]["access_token"] == "access-tok"
|
||||
assert response["data"]["access_token"] == "access-tok"
|
||||
mock_auth.assert_called_once()
|
||||
|
||||
@patch(
|
||||
|
||||
@@ -33,6 +33,7 @@ class TestDecodeEnterpriseWebappUserId:
|
||||
"user_id": "u1",
|
||||
}
|
||||
result = decode_enterprise_webapp_user_id("valid-jwt")
|
||||
assert result is not None
|
||||
assert result["user_id"] == "u1"
|
||||
|
||||
@patch("controllers.web.passport.PassportService")
|
||||
@@ -143,7 +144,7 @@ class TestPassportResource:
|
||||
with app.test_request_context("/passport", headers={"X-App-Code": "code1"}):
|
||||
response = PassportResource().get()
|
||||
|
||||
assert response.get_json()["access_token"] == "issued-token"
|
||||
assert response["access_token"] == "issued-token"
|
||||
mock_db.session.add.assert_called_once()
|
||||
mock_db.session.commit.assert_called_once()
|
||||
|
||||
@@ -167,7 +168,7 @@ class TestPassportResource:
|
||||
with app.test_request_context("/passport?user_id=sess-existing", headers={"X-App-Code": "code1"}):
|
||||
response = PassportResource().get()
|
||||
|
||||
assert response.get_json()["access_token"] == "reused-token"
|
||||
assert response["access_token"] == "reused-token"
|
||||
# Should not create a new end user
|
||||
mock_db.session.add.assert_not_called()
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from core.app.apps.base_app_queue_manager import PublishFrom
|
||||
from core.app.apps.base_app_runner import AppRunner
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.app.entities.queue_entities import QueueMessageFileEvent
|
||||
@@ -81,59 +80,55 @@ class TestBaseAppRunnerMultimodal:
|
||||
# Setup mock message file
|
||||
mock_msg_file_class.return_value = mock_message_file
|
||||
|
||||
with patch("core.app.apps.base_app_runner.db.session", autospec=True) as mock_session:
|
||||
mock_session.add = MagicMock()
|
||||
mock_session.commit = MagicMock()
|
||||
mock_session.refresh = MagicMock()
|
||||
file_session = MagicMock()
|
||||
mock_session_factory = MagicMock()
|
||||
mock_session_factory.begin.return_value.__enter__ = MagicMock(return_value=file_session)
|
||||
mock_session_factory.begin.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
# Act
|
||||
# Create a mock runner with the method bound
|
||||
runner = MagicMock()
|
||||
with patch("core.app.apps.base_app_runner.sessionmaker", return_value=mock_session_factory) as mock_sm:
|
||||
with patch("core.app.apps.base_app_runner.db") as mock_db:
|
||||
# Act
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(
|
||||
runner, *args, **kwargs
|
||||
)
|
||||
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs)
|
||||
runner._handle_multimodal_image_content(
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
runner._handle_multimodal_image_content(
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
# Assert
|
||||
mock_mgr.create_file_by_url.assert_called_once_with(
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
file_url=image_url,
|
||||
conversation_id=None,
|
||||
)
|
||||
|
||||
# Assert
|
||||
# Verify tool file was created from URL
|
||||
mock_mgr.create_file_by_url.assert_called_once_with(
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
file_url=image_url,
|
||||
conversation_id=None,
|
||||
)
|
||||
mock_msg_file_class.assert_called_once()
|
||||
call_kwargs = mock_msg_file_class.call_args[1]
|
||||
assert call_kwargs["message_id"] == mock_message_id
|
||||
assert call_kwargs["type"] == FileType.IMAGE
|
||||
assert call_kwargs["transfer_method"] == FileTransferMethod.TOOL_FILE
|
||||
assert call_kwargs["belongs_to"] == "assistant"
|
||||
assert call_kwargs["created_by"] == mock_user_id
|
||||
|
||||
# Verify message file was created with correct parameters
|
||||
mock_msg_file_class.assert_called_once()
|
||||
call_kwargs = mock_msg_file_class.call_args[1]
|
||||
assert call_kwargs["message_id"] == mock_message_id
|
||||
assert call_kwargs["type"] == FileType.IMAGE
|
||||
assert call_kwargs["transfer_method"] == FileTransferMethod.TOOL_FILE
|
||||
assert call_kwargs["belongs_to"] == "assistant"
|
||||
assert call_kwargs["created_by"] == mock_user_id
|
||||
# Verify independent session was used (not db.session)
|
||||
mock_sm.assert_called_once_with(bind=mock_db.engine, expire_on_commit=False)
|
||||
file_session.add.assert_called_once_with(mock_message_file)
|
||||
mock_db.session.commit.assert_not_called()
|
||||
mock_db.session.close.assert_not_called()
|
||||
|
||||
# Verify database operations
|
||||
mock_session.add.assert_called_once_with(mock_message_file)
|
||||
mock_session.commit.assert_called_once()
|
||||
mock_session.refresh.assert_called_once_with(mock_message_file)
|
||||
|
||||
# Verify event was published
|
||||
mock_queue_manager.publish.assert_called_once()
|
||||
publish_call = mock_queue_manager.publish.call_args
|
||||
assert isinstance(publish_call[0][0], QueueMessageFileEvent)
|
||||
assert publish_call[0][0].message_file_id == mock_message_file.id
|
||||
# publish_from might be passed as positional or keyword argument
|
||||
assert (
|
||||
publish_call[0][1] == PublishFrom.APPLICATION_MANAGER
|
||||
or publish_call.kwargs.get("publish_from") == PublishFrom.APPLICATION_MANAGER
|
||||
)
|
||||
# Verify event was published
|
||||
mock_queue_manager.publish.assert_called_once()
|
||||
publish_call = mock_queue_manager.publish.call_args
|
||||
assert isinstance(publish_call[0][0], QueueMessageFileEvent)
|
||||
assert publish_call[0][0].message_file_id == mock_message_file.id
|
||||
|
||||
def test_handle_multimodal_image_content_with_base64(
|
||||
self,
|
||||
@@ -165,50 +160,44 @@ class TestBaseAppRunnerMultimodal:
|
||||
mock_mgr_class.return_value = mock_mgr
|
||||
|
||||
with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class:
|
||||
# Setup mock message file
|
||||
mock_msg_file_class.return_value = mock_message_file
|
||||
|
||||
with patch("core.app.apps.base_app_runner.db.session", autospec=True) as mock_session:
|
||||
mock_session.add = MagicMock()
|
||||
mock_session.commit = MagicMock()
|
||||
mock_session.refresh = MagicMock()
|
||||
file_session = MagicMock()
|
||||
mock_session_factory = MagicMock()
|
||||
mock_session_factory.begin.return_value.__enter__ = MagicMock(return_value=file_session)
|
||||
mock_session_factory.begin.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
# Act
|
||||
# Create a mock runner with the method bound
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs)
|
||||
with patch("core.app.apps.base_app_runner.sessionmaker", return_value=mock_session_factory):
|
||||
with patch("core.app.apps.base_app_runner.db") as mock_db:
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(
|
||||
runner, *args, **kwargs
|
||||
)
|
||||
|
||||
runner._handle_multimodal_image_content(
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
runner._handle_multimodal_image_content(
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
# Assert
|
||||
# Verify tool file was created from base64
|
||||
mock_mgr.create_file_by_raw.assert_called_once()
|
||||
call_kwargs = mock_mgr.create_file_by_raw.call_args[1]
|
||||
assert call_kwargs["user_id"] == mock_user_id
|
||||
assert call_kwargs["tenant_id"] == mock_tenant_id
|
||||
assert call_kwargs["conversation_id"] is None
|
||||
assert "file_binary" in call_kwargs
|
||||
assert call_kwargs["mimetype"] == "image/png"
|
||||
assert call_kwargs["filename"].startswith("generated_image")
|
||||
assert call_kwargs["filename"].endswith(".png")
|
||||
mock_mgr.create_file_by_raw.assert_called_once()
|
||||
call_kwargs = mock_mgr.create_file_by_raw.call_args[1]
|
||||
assert call_kwargs["user_id"] == mock_user_id
|
||||
assert call_kwargs["tenant_id"] == mock_tenant_id
|
||||
assert call_kwargs["conversation_id"] is None
|
||||
assert "file_binary" in call_kwargs
|
||||
assert call_kwargs["mimetype"] == "image/png"
|
||||
assert call_kwargs["filename"].startswith("generated_image")
|
||||
assert call_kwargs["filename"].endswith(".png")
|
||||
|
||||
# Verify message file was created
|
||||
mock_msg_file_class.assert_called_once()
|
||||
mock_msg_file_class.assert_called_once()
|
||||
file_session.add.assert_called_once()
|
||||
mock_db.session.commit.assert_not_called()
|
||||
|
||||
# Verify database operations
|
||||
mock_session.add.assert_called_once()
|
||||
mock_session.commit.assert_called_once()
|
||||
mock_session.refresh.assert_called_once()
|
||||
|
||||
# Verify event was published
|
||||
mock_queue_manager.publish.assert_called_once()
|
||||
mock_queue_manager.publish.assert_called_once()
|
||||
|
||||
def test_handle_multimodal_image_content_with_base64_data_uri(
|
||||
self,
|
||||
@@ -238,33 +227,32 @@ class TestBaseAppRunnerMultimodal:
|
||||
mock_mgr_class.return_value = mock_mgr
|
||||
|
||||
with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class:
|
||||
# Setup mock message file
|
||||
mock_msg_file_class.return_value = mock_message_file
|
||||
|
||||
with patch("core.app.apps.base_app_runner.db.session", autospec=True) as mock_session:
|
||||
mock_session.add = MagicMock()
|
||||
mock_session.commit = MagicMock()
|
||||
mock_session.refresh = MagicMock()
|
||||
file_session = MagicMock()
|
||||
mock_session_factory = MagicMock()
|
||||
mock_session_factory.begin.return_value.__enter__ = MagicMock(return_value=file_session)
|
||||
mock_session_factory.begin.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
# Act
|
||||
# Create a mock runner with the method bound
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs)
|
||||
with patch("core.app.apps.base_app_runner.sessionmaker", return_value=mock_session_factory):
|
||||
with patch("core.app.apps.base_app_runner.db"):
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(
|
||||
runner, *args, **kwargs
|
||||
)
|
||||
|
||||
runner._handle_multimodal_image_content(
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
runner._handle_multimodal_image_content(
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
# Assert - verify that base64 data was extracted correctly (without prefix)
|
||||
mock_mgr.create_file_by_raw.assert_called_once()
|
||||
call_kwargs = mock_mgr.create_file_by_raw.call_args[1]
|
||||
# The base64 data should be decoded, so we check the binary was passed
|
||||
assert "file_binary" in call_kwargs
|
||||
mock_mgr.create_file_by_raw.assert_called_once()
|
||||
call_kwargs = mock_mgr.create_file_by_raw.call_args[1]
|
||||
assert "file_binary" in call_kwargs
|
||||
|
||||
def test_handle_multimodal_image_content_without_url_or_base64(
|
||||
self,
|
||||
@@ -284,9 +272,7 @@ class TestBaseAppRunnerMultimodal:
|
||||
|
||||
with patch("core.app.apps.base_app_runner.ToolFileManager", autospec=True) as mock_mgr_class:
|
||||
with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class:
|
||||
with patch("core.app.apps.base_app_runner.db.session", autospec=True) as mock_session:
|
||||
# Act
|
||||
# Create a mock runner with the method bound
|
||||
with patch("core.app.apps.base_app_runner.db"):
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs)
|
||||
@@ -299,10 +285,8 @@ class TestBaseAppRunnerMultimodal:
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
# Assert - should not create any files or publish events
|
||||
mock_mgr_class.assert_not_called()
|
||||
mock_msg_file_class.assert_not_called()
|
||||
mock_session.add.assert_not_called()
|
||||
mock_queue_manager.publish.assert_not_called()
|
||||
|
||||
def test_handle_multimodal_image_content_with_error(
|
||||
@@ -322,20 +306,16 @@ class TestBaseAppRunnerMultimodal:
|
||||
)
|
||||
|
||||
with patch("core.app.apps.base_app_runner.ToolFileManager", autospec=True) as mock_mgr_class:
|
||||
# Setup mock to raise exception
|
||||
mock_mgr = MagicMock()
|
||||
mock_mgr.create_file_by_url.side_effect = Exception("Network error")
|
||||
mock_mgr_class.return_value = mock_mgr
|
||||
|
||||
with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class:
|
||||
with patch("core.app.apps.base_app_runner.db.session", autospec=True) as mock_session:
|
||||
# Act
|
||||
# Create a mock runner with the method bound
|
||||
with patch("core.app.apps.base_app_runner.db"):
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs)
|
||||
|
||||
# Should not raise exception, just log it
|
||||
runner._handle_multimodal_image_content(
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
@@ -344,9 +324,7 @@ class TestBaseAppRunnerMultimodal:
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
# Assert - should not create message file or publish event on error
|
||||
mock_msg_file_class.assert_not_called()
|
||||
mock_session.add.assert_not_called()
|
||||
mock_queue_manager.publish.assert_not_called()
|
||||
|
||||
def test_handle_multimodal_image_content_debugger_mode(
|
||||
@@ -369,37 +347,36 @@ class TestBaseAppRunnerMultimodal:
|
||||
mock_queue_manager.invoke_from = InvokeFrom.DEBUGGER
|
||||
|
||||
with patch("core.app.apps.base_app_runner.ToolFileManager", autospec=True) as mock_mgr_class:
|
||||
# Setup mock tool file manager
|
||||
mock_mgr = MagicMock()
|
||||
mock_mgr.create_file_by_url.return_value = mock_tool_file
|
||||
mock_mgr_class.return_value = mock_mgr
|
||||
|
||||
with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class:
|
||||
# Setup mock message file
|
||||
mock_msg_file_class.return_value = mock_message_file
|
||||
|
||||
with patch("core.app.apps.base_app_runner.db.session", autospec=True) as mock_session:
|
||||
mock_session.add = MagicMock()
|
||||
mock_session.commit = MagicMock()
|
||||
mock_session.refresh = MagicMock()
|
||||
file_session = MagicMock()
|
||||
mock_session_factory = MagicMock()
|
||||
mock_session_factory.begin.return_value.__enter__ = MagicMock(return_value=file_session)
|
||||
mock_session_factory.begin.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
# Act
|
||||
# Create a mock runner with the method bound
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs)
|
||||
with patch("core.app.apps.base_app_runner.sessionmaker", return_value=mock_session_factory):
|
||||
with patch("core.app.apps.base_app_runner.db"):
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(
|
||||
runner, *args, **kwargs
|
||||
)
|
||||
|
||||
runner._handle_multimodal_image_content(
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
runner._handle_multimodal_image_content(
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
# Assert - verify created_by_role is ACCOUNT for debugger mode
|
||||
call_kwargs = mock_msg_file_class.call_args[1]
|
||||
assert call_kwargs["created_by_role"] == CreatorUserRole.ACCOUNT
|
||||
call_kwargs = mock_msg_file_class.call_args[1]
|
||||
assert call_kwargs["created_by_role"] == CreatorUserRole.ACCOUNT
|
||||
|
||||
def test_handle_multimodal_image_content_service_api_mode(
|
||||
self,
|
||||
@@ -421,34 +398,33 @@ class TestBaseAppRunnerMultimodal:
|
||||
mock_queue_manager.invoke_from = InvokeFrom.SERVICE_API
|
||||
|
||||
with patch("core.app.apps.base_app_runner.ToolFileManager", autospec=True) as mock_mgr_class:
|
||||
# Setup mock tool file manager
|
||||
mock_mgr = MagicMock()
|
||||
mock_mgr.create_file_by_url.return_value = mock_tool_file
|
||||
mock_mgr_class.return_value = mock_mgr
|
||||
|
||||
with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class:
|
||||
# Setup mock message file
|
||||
mock_msg_file_class.return_value = mock_message_file
|
||||
|
||||
with patch("core.app.apps.base_app_runner.db.session", autospec=True) as mock_session:
|
||||
mock_session.add = MagicMock()
|
||||
mock_session.commit = MagicMock()
|
||||
mock_session.refresh = MagicMock()
|
||||
file_session = MagicMock()
|
||||
mock_session_factory = MagicMock()
|
||||
mock_session_factory.begin.return_value.__enter__ = MagicMock(return_value=file_session)
|
||||
mock_session_factory.begin.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
# Act
|
||||
# Create a mock runner with the method bound
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs)
|
||||
with patch("core.app.apps.base_app_runner.sessionmaker", return_value=mock_session_factory):
|
||||
with patch("core.app.apps.base_app_runner.db"):
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(
|
||||
runner, *args, **kwargs
|
||||
)
|
||||
|
||||
runner._handle_multimodal_image_content(
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
runner._handle_multimodal_image_content(
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
# Assert - verify created_by_role is END_USER for service API
|
||||
call_kwargs = mock_msg_file_class.call_args[1]
|
||||
assert call_kwargs["created_by_role"] == CreatorUserRole.END_USER
|
||||
call_kwargs = mock_msg_file_class.call_args[1]
|
||||
assert call_kwargs["created_by_role"] == CreatorUserRole.END_USER
|
||||
|
||||
@@ -14,6 +14,9 @@ def mock_queue_manager(mocker: MockerFixture):
|
||||
|
||||
@pytest.fixture
|
||||
def handler(mock_queue_manager, mocker: MockerFixture):
|
||||
mocker.patch(
|
||||
"core.callback_handler.index_tool_callback_handler.db",
|
||||
)
|
||||
return DatasetIndexToolCallbackHandler(
|
||||
queue_manager=mock_queue_manager,
|
||||
app_id="app-1",
|
||||
@@ -33,8 +36,18 @@ class TestOnQuery:
|
||||
],
|
||||
)
|
||||
def test_on_query_success_roles(self, mocker: MockerFixture, mock_queue_manager, invoke_from, expected_role):
|
||||
# Arrange
|
||||
mock_session = mocker.Mock()
|
||||
# Arrange — the caller passes a session, but our fix uses an independent one
|
||||
caller_session = mocker.Mock()
|
||||
|
||||
independent_session = mocker.MagicMock()
|
||||
mock_session_factory = mocker.MagicMock()
|
||||
mock_session_factory.begin.return_value.__enter__ = mocker.MagicMock(return_value=independent_session)
|
||||
mock_session_factory.begin.return_value.__exit__ = mocker.MagicMock(return_value=False)
|
||||
mocker.patch(
|
||||
"core.callback_handler.index_tool_callback_handler.sessionmaker",
|
||||
return_value=mock_session_factory,
|
||||
)
|
||||
mocker.patch("core.callback_handler.index_tool_callback_handler.db")
|
||||
|
||||
handler = DatasetIndexToolCallbackHandler(
|
||||
queue_manager=mock_queue_manager,
|
||||
@@ -46,17 +59,28 @@ class TestOnQuery:
|
||||
|
||||
handler._invoke_from = invoke_from
|
||||
|
||||
# Act
|
||||
handler.on_query("test query", "dataset-1", mock_session)
|
||||
# Act — pass caller_session as required by signature
|
||||
handler.on_query("test query", "dataset-1", caller_session)
|
||||
|
||||
# Assert
|
||||
mock_session.add.assert_called_once()
|
||||
dataset_query = mock_session.add.call_args.args[0]
|
||||
# Assert — independent session used, not the caller's session
|
||||
independent_session.add.assert_called_once()
|
||||
dataset_query = independent_session.add.call_args.args[0]
|
||||
assert dataset_query.created_by_role == expected_role
|
||||
mock_session.commit.assert_called_once()
|
||||
caller_session.add.assert_not_called()
|
||||
caller_session.commit.assert_not_called()
|
||||
|
||||
def test_on_query_none_values(self, mocker: MockerFixture, mock_queue_manager):
|
||||
mock_session = mocker.Mock()
|
||||
caller_session = mocker.Mock()
|
||||
|
||||
independent_session = mocker.MagicMock()
|
||||
mock_session_factory = mocker.MagicMock()
|
||||
mock_session_factory.begin.return_value.__enter__ = mocker.MagicMock(return_value=independent_session)
|
||||
mock_session_factory.begin.return_value.__exit__ = mocker.MagicMock(return_value=False)
|
||||
mocker.patch(
|
||||
"core.callback_handler.index_tool_callback_handler.sessionmaker",
|
||||
return_value=mock_session_factory,
|
||||
)
|
||||
mocker.patch("core.callback_handler.index_tool_callback_handler.db")
|
||||
|
||||
handler = DatasetIndexToolCallbackHandler(
|
||||
queue_manager=mock_queue_manager,
|
||||
@@ -66,40 +90,67 @@ class TestOnQuery:
|
||||
invoke_from=None,
|
||||
)
|
||||
|
||||
handler.on_query(None, None, mock_session)
|
||||
handler.on_query(None, None, caller_session)
|
||||
|
||||
mock_session.add.assert_called_once()
|
||||
mock_session.commit.assert_called_once()
|
||||
independent_session.add.assert_called_once()
|
||||
caller_session.add.assert_not_called()
|
||||
|
||||
|
||||
class TestOnToolEnd:
|
||||
def test_on_tool_end_no_metadata(self, handler: DatasetIndexToolCallbackHandler, mocker: MockerFixture):
|
||||
mock_session = mocker.Mock()
|
||||
caller_session = mocker.Mock()
|
||||
|
||||
independent_session = mocker.MagicMock()
|
||||
mocker.patch(
|
||||
"core.callback_handler.index_tool_callback_handler.Session",
|
||||
return_value=independent_session,
|
||||
)
|
||||
independent_session.__enter__ = mocker.MagicMock(return_value=independent_session)
|
||||
independent_session.__exit__ = mocker.MagicMock(return_value=False)
|
||||
|
||||
document = mocker.Mock()
|
||||
document.metadata = None
|
||||
|
||||
handler.on_tool_end([document], mock_session)
|
||||
handler.on_tool_end([document], caller_session)
|
||||
|
||||
mock_session.commit.assert_not_called()
|
||||
independent_session.commit.assert_called_once()
|
||||
independent_session.execute.assert_not_called()
|
||||
caller_session.commit.assert_not_called()
|
||||
|
||||
def test_on_tool_end_dataset_document_not_found(
|
||||
self, handler: DatasetIndexToolCallbackHandler, mocker: MockerFixture
|
||||
):
|
||||
mock_session = mocker.Mock()
|
||||
mock_session.scalar.return_value = None
|
||||
caller_session = mocker.Mock()
|
||||
|
||||
independent_session = mocker.MagicMock()
|
||||
mocker.patch(
|
||||
"core.callback_handler.index_tool_callback_handler.Session",
|
||||
return_value=independent_session,
|
||||
)
|
||||
independent_session.__enter__ = mocker.MagicMock(return_value=independent_session)
|
||||
independent_session.__exit__ = mocker.MagicMock(return_value=False)
|
||||
independent_session.scalar.return_value = None
|
||||
|
||||
document = mocker.Mock()
|
||||
document.metadata = {"document_id": "doc-1", "doc_id": "node-1"}
|
||||
|
||||
handler.on_tool_end([document], mock_session)
|
||||
handler.on_tool_end([document], caller_session)
|
||||
|
||||
mock_session.scalar.assert_called_once()
|
||||
independent_session.scalar.assert_called_once()
|
||||
caller_session.scalar.assert_not_called()
|
||||
|
||||
def test_on_tool_end_parent_child_index_with_child(
|
||||
self, handler: DatasetIndexToolCallbackHandler, mocker: MockerFixture
|
||||
):
|
||||
mock_session = mocker.Mock()
|
||||
caller_session = mocker.Mock()
|
||||
|
||||
independent_session = mocker.MagicMock()
|
||||
mocker.patch(
|
||||
"core.callback_handler.index_tool_callback_handler.Session",
|
||||
return_value=independent_session,
|
||||
)
|
||||
independent_session.__enter__ = mocker.MagicMock(return_value=independent_session)
|
||||
independent_session.__exit__ = mocker.MagicMock(return_value=False)
|
||||
|
||||
mock_dataset_doc = mocker.Mock()
|
||||
from core.callback_handler.index_tool_callback_handler import IndexStructureType
|
||||
@@ -111,23 +162,32 @@ class TestOnToolEnd:
|
||||
mock_child_chunk = mocker.Mock()
|
||||
mock_child_chunk.segment_id = "segment-1"
|
||||
|
||||
mock_session.scalar.side_effect = [mock_dataset_doc, mock_child_chunk]
|
||||
independent_session.scalar.side_effect = [mock_dataset_doc, mock_child_chunk]
|
||||
|
||||
document = mocker.Mock()
|
||||
document.metadata = {"document_id": "doc-1", "doc_id": "node-1"}
|
||||
|
||||
handler.on_tool_end([document], mock_session)
|
||||
handler.on_tool_end([document], caller_session)
|
||||
|
||||
mock_session.execute.assert_called_once()
|
||||
mock_session.commit.assert_called_once()
|
||||
independent_session.execute.assert_called_once()
|
||||
independent_session.commit.assert_called_once()
|
||||
caller_session.execute.assert_not_called()
|
||||
|
||||
def test_on_tool_end_non_parent_child_index(self, handler: DatasetIndexToolCallbackHandler, mocker: MockerFixture):
|
||||
mock_session = mocker.Mock()
|
||||
caller_session = mocker.Mock()
|
||||
|
||||
independent_session = mocker.MagicMock()
|
||||
mocker.patch(
|
||||
"core.callback_handler.index_tool_callback_handler.Session",
|
||||
return_value=independent_session,
|
||||
)
|
||||
independent_session.__enter__ = mocker.MagicMock(return_value=independent_session)
|
||||
independent_session.__exit__ = mocker.MagicMock(return_value=False)
|
||||
|
||||
mock_dataset_doc = mocker.Mock()
|
||||
mock_dataset_doc.doc_form = "OTHER"
|
||||
|
||||
mock_session.scalar.return_value = mock_dataset_doc
|
||||
independent_session.scalar.return_value = mock_dataset_doc
|
||||
|
||||
document = mocker.Mock()
|
||||
document.metadata = {
|
||||
@@ -136,14 +196,24 @@ class TestOnToolEnd:
|
||||
"dataset_id": "dataset-1",
|
||||
}
|
||||
|
||||
handler.on_tool_end([document], mock_session)
|
||||
handler.on_tool_end([document], caller_session)
|
||||
|
||||
mock_session.execute.assert_called_once()
|
||||
mock_session.commit.assert_called_once()
|
||||
independent_session.execute.assert_called_once()
|
||||
independent_session.commit.assert_called_once()
|
||||
caller_session.execute.assert_not_called()
|
||||
|
||||
def test_on_tool_end_empty_documents(self, handler: DatasetIndexToolCallbackHandler, mocker: MockerFixture):
|
||||
mock_session = mocker.Mock()
|
||||
handler.on_tool_end([], mock_session)
|
||||
caller_session = mocker.Mock()
|
||||
|
||||
independent_session = mocker.MagicMock()
|
||||
mocker.patch(
|
||||
"core.callback_handler.index_tool_callback_handler.Session",
|
||||
return_value=independent_session,
|
||||
)
|
||||
independent_session.__enter__ = mocker.MagicMock(return_value=independent_session)
|
||||
independent_session.__exit__ = mocker.MagicMock(return_value=False)
|
||||
|
||||
handler.on_tool_end([], caller_session)
|
||||
|
||||
|
||||
class TestReturnRetrieverResourceInfo:
|
||||
|
||||
@@ -8,7 +8,7 @@ from core.datasource.entities.datasource_entities import (
|
||||
DatasourceProviderEntityWithPlugin,
|
||||
DatasourceProviderType,
|
||||
)
|
||||
from core.entities.provider_entities import ProviderConfig
|
||||
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
|
||||
from core.tools.errors import ToolProviderCredentialValidationError
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ class TestDatasourcePluginProviderController:
|
||||
mock_config = MagicMock(spec=ProviderConfig)
|
||||
mock_config.name = "text_field"
|
||||
mock_config.required = True
|
||||
mock_config.type = ProviderConfig.Type.TEXT_INPUT
|
||||
mock_config.type = ProviderConfigType.TEXT_INPUT
|
||||
|
||||
mock_entity = MagicMock(spec=DatasourceProviderEntityWithPlugin)
|
||||
mock_entity.credentials_schema = [mock_config]
|
||||
@@ -167,7 +167,7 @@ class TestDatasourcePluginProviderController:
|
||||
mock_config = MagicMock(spec=ProviderConfig)
|
||||
mock_config.name = "select_field"
|
||||
mock_config.required = True
|
||||
mock_config.type = ProviderConfig.Type.SELECT
|
||||
mock_config.type = ProviderConfigType.SELECT
|
||||
mock_config.options = [mock_option]
|
||||
|
||||
mock_entity = MagicMock(spec=DatasourceProviderEntityWithPlugin)
|
||||
@@ -206,7 +206,7 @@ class TestDatasourcePluginProviderController:
|
||||
mock_config = MagicMock(spec=ProviderConfig)
|
||||
mock_config.name = "valid_field"
|
||||
mock_config.required = True
|
||||
mock_config.type = ProviderConfig.Type.TEXT_INPUT
|
||||
mock_config.type = ProviderConfigType.TEXT_INPUT
|
||||
|
||||
mock_entity = MagicMock(spec=DatasourceProviderEntityWithPlugin)
|
||||
mock_entity.credentials_schema = [mock_config]
|
||||
@@ -243,7 +243,7 @@ class TestDatasourcePluginProviderController:
|
||||
mock_config_text = MagicMock(spec=ProviderConfig)
|
||||
mock_config_text.name = "text_def"
|
||||
mock_config_text.required = False
|
||||
mock_config_text.type = ProviderConfig.Type.TEXT_INPUT
|
||||
mock_config_text.type = ProviderConfigType.TEXT_INPUT
|
||||
mock_config_text.default = 123 # Int default, should be converted to str
|
||||
|
||||
mock_config_other = MagicMock(spec=ProviderConfig)
|
||||
|
||||
@@ -5,6 +5,7 @@ from core.entities.provider_entities import (
|
||||
BasicProviderConfig,
|
||||
ModelSettings,
|
||||
ProviderConfig,
|
||||
ProviderConfigType,
|
||||
ProviderQuotaType,
|
||||
)
|
||||
from core.tools.entities.common_entities import I18nObject
|
||||
@@ -27,22 +28,22 @@ def test_provider_quota_type_value_of_rejects_unknown_values() -> None:
|
||||
|
||||
def test_basic_provider_config_type_value_of_handles_known_values() -> None:
|
||||
# Arrange / Act
|
||||
parameter_type = BasicProviderConfig.Type.value_of("text-input")
|
||||
parameter_type = ProviderConfigType.value_of("text-input")
|
||||
|
||||
# Assert
|
||||
assert parameter_type == BasicProviderConfig.Type.TEXT_INPUT
|
||||
assert parameter_type == ProviderConfigType.TEXT_INPUT
|
||||
|
||||
|
||||
def test_basic_provider_config_type_value_of_rejects_invalid_values() -> None:
|
||||
# Arrange / Act / Assert
|
||||
with pytest.raises(ValueError, match="invalid mode value"):
|
||||
BasicProviderConfig.Type.value_of("unknown")
|
||||
ProviderConfigType.value_of("unknown")
|
||||
|
||||
|
||||
def test_provider_config_to_basic_provider_config_keeps_type_and_name() -> None:
|
||||
# Arrange
|
||||
provider_config = ProviderConfig(
|
||||
type=BasicProviderConfig.Type.SELECT,
|
||||
type=ProviderConfigType.SELECT,
|
||||
name="workspace",
|
||||
scope=AppSelectorScope.ALL,
|
||||
options=[ProviderConfig.Option(value="all", label=I18nObject(en_US="All"))],
|
||||
@@ -53,7 +54,7 @@ def test_provider_config_to_basic_provider_config_keeps_type_and_name() -> None:
|
||||
|
||||
# Assert
|
||||
assert isinstance(basic_config, BasicProviderConfig)
|
||||
assert basic_config.type == BasicProviderConfig.Type.SELECT
|
||||
assert basic_config.type == ProviderConfigType.SELECT
|
||||
assert basic_config.name == "workspace"
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import pytest
|
||||
from packaging.version import Version
|
||||
from requests import HTTPError
|
||||
|
||||
from core.plugin.entities.bundle import PluginBundleDependency
|
||||
from core.plugin.entities.bundle import PluginBundleDependency, PluginBundleDependencyType
|
||||
from core.plugin.entities.plugin import (
|
||||
MissingPluginDependency,
|
||||
PluginCategory,
|
||||
@@ -581,11 +581,11 @@ class TestDependencyResolution:
|
||||
bundle_data = b"mock-bundle-data"
|
||||
mock_dependencies = [
|
||||
PluginBundleDependency(
|
||||
type=PluginBundleDependency.Type.Marketplace,
|
||||
type=PluginBundleDependencyType.Marketplace,
|
||||
value=PluginBundleDependency.Marketplace(organization="org1", plugin="plugin1", version="1.0.0"),
|
||||
),
|
||||
PluginBundleDependency(
|
||||
type=PluginBundleDependency.Type.Github,
|
||||
type=PluginBundleDependencyType.Github,
|
||||
value=PluginBundleDependency.Github(
|
||||
repo_address="https://github.com/org/repo",
|
||||
repo="org/repo",
|
||||
@@ -603,8 +603,8 @@ class TestDependencyResolution:
|
||||
|
||||
# Assert: Verify dependencies were extracted
|
||||
assert len(result) == 2
|
||||
assert result[0].type == PluginBundleDependency.Type.Marketplace
|
||||
assert result[1].type == PluginBundleDependency.Type.Github
|
||||
assert result[0].type == PluginBundleDependencyType.Marketplace
|
||||
assert result[1].type == PluginBundleDependencyType.Github
|
||||
mock_request.assert_called_once()
|
||||
|
||||
def test_fetch_missing_dependencies(self, plugin_installer):
|
||||
@@ -1129,7 +1129,7 @@ class TestPluginBundleOperations:
|
||||
bundle_data = b"mock-marketplace-bundle"
|
||||
mock_dependencies = [
|
||||
PluginBundleDependency(
|
||||
type=PluginBundleDependency.Type.Marketplace,
|
||||
type=PluginBundleDependencyType.Marketplace,
|
||||
value=PluginBundleDependency.Marketplace(
|
||||
organization="langgenius", plugin="search-tool", version="1.2.0"
|
||||
),
|
||||
@@ -1142,7 +1142,7 @@ class TestPluginBundleOperations:
|
||||
|
||||
# Assert: Verify marketplace dependency was extracted
|
||||
assert len(result) == 1
|
||||
assert result[0].type == PluginBundleDependency.Type.Marketplace
|
||||
assert result[0].type == PluginBundleDependencyType.Marketplace
|
||||
assert isinstance(result[0].value, PluginBundleDependency.Marketplace)
|
||||
assert result[0].value.organization == "langgenius"
|
||||
assert result[0].value.plugin == "search-tool"
|
||||
@@ -1158,7 +1158,7 @@ class TestPluginBundleOperations:
|
||||
bundle_data = b"mock-github-bundle"
|
||||
mock_dependencies = [
|
||||
PluginBundleDependency(
|
||||
type=PluginBundleDependency.Type.Github,
|
||||
type=PluginBundleDependencyType.Github,
|
||||
value=PluginBundleDependency.Github(
|
||||
repo_address="https://github.com/example/plugin",
|
||||
repo="example/plugin",
|
||||
@@ -1174,7 +1174,7 @@ class TestPluginBundleOperations:
|
||||
|
||||
# Assert: Verify GitHub dependency was extracted
|
||||
assert len(result) == 1
|
||||
assert result[0].type == PluginBundleDependency.Type.Github
|
||||
assert result[0].type == PluginBundleDependencyType.Github
|
||||
assert isinstance(result[0].value, PluginBundleDependency.Github)
|
||||
assert result[0].value.repo == "example/plugin"
|
||||
assert result[0].value.release == "v2.0.0"
|
||||
@@ -1204,7 +1204,7 @@ class TestPluginBundleOperations:
|
||||
|
||||
mock_dependencies = [
|
||||
PluginBundleDependency(
|
||||
type=PluginBundleDependency.Type.Package,
|
||||
type=PluginBundleDependencyType.Package,
|
||||
value=PluginBundleDependency.Package(
|
||||
unique_identifier="org/bundled-plugin/1.5.0", manifest=mock_manifest
|
||||
),
|
||||
@@ -1217,7 +1217,7 @@ class TestPluginBundleOperations:
|
||||
|
||||
# Assert: Verify package dependency was extracted with manifest
|
||||
assert len(result) == 1
|
||||
assert result[0].type == PluginBundleDependency.Type.Package
|
||||
assert result[0].type == PluginBundleDependencyType.Package
|
||||
assert isinstance(result[0].value, PluginBundleDependency.Package)
|
||||
assert result[0].value.unique_identifier == "org/bundled-plugin/1.5.0"
|
||||
assert result[0].value.manifest.name == "bundled-plugin"
|
||||
@@ -1233,11 +1233,11 @@ class TestPluginBundleOperations:
|
||||
bundle_data = b"mock-mixed-bundle"
|
||||
mock_dependencies = [
|
||||
PluginBundleDependency(
|
||||
type=PluginBundleDependency.Type.Marketplace,
|
||||
type=PluginBundleDependencyType.Marketplace,
|
||||
value=PluginBundleDependency.Marketplace(organization="org1", plugin="plugin1", version="1.0.0"),
|
||||
),
|
||||
PluginBundleDependency(
|
||||
type=PluginBundleDependency.Type.Github,
|
||||
type=PluginBundleDependencyType.Github,
|
||||
value=PluginBundleDependency.Github(
|
||||
repo_address="https://github.com/org2/plugin2",
|
||||
repo="org2/plugin2",
|
||||
@@ -1253,8 +1253,8 @@ class TestPluginBundleOperations:
|
||||
|
||||
# Assert: Verify all dependency types were extracted
|
||||
assert len(result) == 2
|
||||
assert result[0].type == PluginBundleDependency.Type.Marketplace
|
||||
assert result[1].type == PluginBundleDependency.Type.Github
|
||||
assert result[0].type == PluginBundleDependencyType.Marketplace
|
||||
assert result[1].type == PluginBundleDependencyType.Github
|
||||
|
||||
|
||||
class TestPluginTaskStatusTransitions:
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any, override
|
||||
|
||||
import pytest
|
||||
|
||||
from core.entities.provider_entities import ProviderConfig
|
||||
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
|
||||
from core.tools.__base.tool import Tool
|
||||
from core.tools.__base.tool_provider import ToolProviderController
|
||||
from core.tools.__base.tool_runtime import ToolRuntime
|
||||
@@ -66,7 +66,7 @@ def _provider_identity() -> ToolProviderIdentity:
|
||||
def test_tool_provider_controller_get_credentials_schema_returns_deep_copy():
|
||||
entity = ToolProviderEntity(
|
||||
identity=_provider_identity(),
|
||||
credentials_schema=[ProviderConfig(type=ProviderConfig.Type.TEXT_INPUT, name="api_key", required=False)],
|
||||
credentials_schema=[ProviderConfig(type=ProviderConfigType.TEXT_INPUT, name="api_key", required=False)],
|
||||
)
|
||||
controller = _DummyController(entity=entity)
|
||||
|
||||
@@ -88,10 +88,10 @@ def test_validate_credentials_format_covers_required_default_and_type_rules():
|
||||
entity = ToolProviderEntity(
|
||||
identity=_provider_identity(),
|
||||
credentials_schema=[
|
||||
ProviderConfig(type=ProviderConfig.Type.TEXT_INPUT, name="required_text", required=True),
|
||||
ProviderConfig(type=ProviderConfig.Type.SECRET_INPUT, name="secret", required=False),
|
||||
ProviderConfig(type=ProviderConfig.Type.SELECT, name="choice", required=False, options=select_options),
|
||||
ProviderConfig(type=ProviderConfig.Type.TEXT_INPUT, name="with_default", required=False, default="x"),
|
||||
ProviderConfig(type=ProviderConfigType.TEXT_INPUT, name="required_text", required=True),
|
||||
ProviderConfig(type=ProviderConfigType.SECRET_INPUT, name="secret", required=False),
|
||||
ProviderConfig(type=ProviderConfigType.SELECT, name="choice", required=False, options=select_options),
|
||||
ProviderConfig(type=ProviderConfigType.TEXT_INPUT, name="with_default", required=False, default="x"),
|
||||
],
|
||||
)
|
||||
controller = _DummyController(entity=entity)
|
||||
|
||||
@@ -5,7 +5,7 @@ from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from core.entities.provider_entities import BasicProviderConfig
|
||||
from core.entities.provider_entities import BasicProviderConfig, ProviderConfigType
|
||||
from core.helper.provider_encryption import ProviderConfigEncrypter
|
||||
from core.tools.utils.encryption import create_tool_provider_encrypter
|
||||
|
||||
@@ -31,7 +31,7 @@ def secret_field() -> BasicProviderConfig:
|
||||
"""A SECRET_INPUT field named 'password'."""
|
||||
return BasicProviderConfig(
|
||||
name="password",
|
||||
type=BasicProviderConfig.Type.SECRET_INPUT,
|
||||
type=ProviderConfigType.SECRET_INPUT,
|
||||
)
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ def normal_field() -> BasicProviderConfig:
|
||||
"""A TEXT_INPUT field named 'username'."""
|
||||
return BasicProviderConfig(
|
||||
name="username",
|
||||
type=BasicProviderConfig.Type.TEXT_INPUT,
|
||||
type=ProviderConfigType.TEXT_INPUT,
|
||||
)
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ def test_decrypt_swallow_exception_and_keep_original(encrypter_obj):
|
||||
|
||||
|
||||
def test_create_tool_provider_encrypter_builds_cache_and_encrypter():
|
||||
basic_config = BasicProviderConfig(name="key", type=BasicProviderConfig.Type.TEXT_INPUT)
|
||||
basic_config = BasicProviderConfig(name="key", type=ProviderConfigType.TEXT_INPUT)
|
||||
credential_schema_item = SimpleNamespace(to_basic_provider_config=lambda: basic_config)
|
||||
controller = SimpleNamespace(
|
||||
provider_type=SimpleNamespace(value="builtin"),
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
|
||||
from flask_restx import marshal
|
||||
|
||||
from fields.snippet_fields import snippet_list_fields
|
||||
from fields.snippet_fields import SnippetListItemResponse
|
||||
from libs.helper import dump_response
|
||||
|
||||
|
||||
def test_snippet_list_fields_include_author_name() -> None:
|
||||
@@ -24,6 +23,6 @@ def test_snippet_list_fields_include_author_name() -> None:
|
||||
updated_at=datetime.fromtimestamp(1704067201, tz=UTC),
|
||||
)
|
||||
|
||||
result = marshal(snippet, snippet_list_fields)
|
||||
result = dump_response(SnippetListItemResponse, snippet)
|
||||
|
||||
assert result["author_name"] == "Alice"
|
||||
|
||||
@@ -11,7 +11,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from core.plugin.entities.plugin import PluginDependency, PluginInstallationSource
|
||||
from core.plugin.entities.plugin import PluginDependency, PluginDependencyType, PluginInstallationSource
|
||||
from services.plugin.dependencies_analysis import DependenciesAnalysisService
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class TestAnalyzeModelProviderDependency:
|
||||
|
||||
|
||||
class TestGetLeakedDependencies:
|
||||
def _make_dependency(self, identifier: str, dep_type=PluginDependency.Type.Marketplace):
|
||||
def _make_dependency(self, identifier: str, dep_type=PluginDependencyType.Marketplace):
|
||||
return PluginDependency(
|
||||
type=dep_type,
|
||||
value=PluginDependency.Marketplace(marketplace_plugin_unique_identifier=identifier),
|
||||
@@ -110,7 +110,7 @@ class TestGenerateDependencies:
|
||||
result = DependenciesAnalysisService.generate_dependencies("t1", ["p1"])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].type == PluginDependency.Type.Github
|
||||
assert result[0].type == PluginDependencyType.Github
|
||||
assert result[0].value.repo == "org/repo"
|
||||
|
||||
@patch("services.plugin.dependencies_analysis.PluginInstaller")
|
||||
@@ -120,7 +120,7 @@ class TestGenerateDependencies:
|
||||
|
||||
result = DependenciesAnalysisService.generate_dependencies("t1", ["p1"])
|
||||
|
||||
assert result[0].type == PluginDependency.Type.Marketplace
|
||||
assert result[0].type == PluginDependencyType.Marketplace
|
||||
|
||||
@patch("services.plugin.dependencies_analysis.PluginInstaller")
|
||||
def test_package_source(self, mock_installer_cls):
|
||||
@@ -129,7 +129,7 @@ class TestGenerateDependencies:
|
||||
|
||||
result = DependenciesAnalysisService.generate_dependencies("t1", ["p1"])
|
||||
|
||||
assert result[0].type == PluginDependency.Type.Package
|
||||
assert result[0].type == PluginDependencyType.Package
|
||||
|
||||
@patch("services.plugin.dependencies_analysis.PluginInstaller")
|
||||
def test_remote_source_raises(self, mock_installer_cls):
|
||||
@@ -169,4 +169,4 @@ class TestGenerateLatestDependencies:
|
||||
result = DependenciesAnalysisService.generate_latest_dependencies(["p1"])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].type == PluginDependency.Type.Marketplace
|
||||
assert result[0].type == PluginDependencyType.Marketplace
|
||||
|
||||
@@ -4,10 +4,14 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from models.account import TenantPluginAutoUpgradeStrategy
|
||||
from models.account import (
|
||||
TenantPluginAutoUpgradeCategory,
|
||||
TenantPluginAutoUpgradeMode,
|
||||
TenantPluginAutoUpgradeStrategySetting,
|
||||
)
|
||||
|
||||
MODULE = "services.plugin.plugin_auto_upgrade_service"
|
||||
PLUGIN_CATEGORY = TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL
|
||||
PLUGIN_CATEGORY = TenantPluginAutoUpgradeCategory.TOOL
|
||||
|
||||
|
||||
def _patched_session():
|
||||
@@ -57,9 +61,9 @@ class TestChangeStrategy:
|
||||
|
||||
result = PluginAutoUpgradeService.change_strategy(
|
||||
"t1",
|
||||
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
|
||||
3,
|
||||
TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
|
||||
TenantPluginAutoUpgradeMode.ALL,
|
||||
[],
|
||||
[],
|
||||
category=PLUGIN_CATEGORY,
|
||||
@@ -78,18 +82,18 @@ class TestChangeStrategy:
|
||||
|
||||
result = PluginAutoUpgradeService.change_strategy(
|
||||
"t1",
|
||||
TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST,
|
||||
TenantPluginAutoUpgradeStrategySetting.LATEST,
|
||||
5,
|
||||
TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL,
|
||||
TenantPluginAutoUpgradeMode.PARTIAL,
|
||||
["p1"],
|
||||
["p2"],
|
||||
category=PLUGIN_CATEGORY,
|
||||
)
|
||||
|
||||
assert result is True
|
||||
assert existing.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
|
||||
assert existing.strategy_setting == TenantPluginAutoUpgradeStrategySetting.LATEST
|
||||
assert existing.upgrade_time_of_day == 5
|
||||
assert existing.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL
|
||||
assert existing.upgrade_mode == TenantPluginAutoUpgradeMode.PARTIAL
|
||||
assert existing.exclude_plugins == ["p1"]
|
||||
assert existing.include_plugins == ["p2"]
|
||||
|
||||
@@ -102,10 +106,8 @@ class TestExcludePlugin:
|
||||
with (
|
||||
p1,
|
||||
patch(f"{MODULE}.select"),
|
||||
patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls,
|
||||
patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy"),
|
||||
):
|
||||
strat_cls.StrategySetting.FIX_ONLY = "fix_only"
|
||||
strat_cls.UpgradeMode.EXCLUDE = "exclude"
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
|
||||
result = PluginAutoUpgradeService.exclude_plugin(
|
||||
@@ -120,14 +122,11 @@ class TestExcludePlugin:
|
||||
def test_appends_to_exclude_list_in_exclude_mode(self):
|
||||
p1, session = _patched_session()
|
||||
existing = MagicMock()
|
||||
existing.upgrade_mode = "exclude"
|
||||
existing.upgrade_mode = TenantPluginAutoUpgradeMode.EXCLUDE
|
||||
existing.exclude_plugins = ["p-existing"]
|
||||
session.scalar.return_value = existing
|
||||
|
||||
with p1, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls:
|
||||
strat_cls.UpgradeMode.EXCLUDE = "exclude"
|
||||
strat_cls.UpgradeMode.PARTIAL = "partial"
|
||||
strat_cls.UpgradeMode.ALL = "all"
|
||||
with p1, patch(f"{MODULE}.select"):
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
|
||||
result = PluginAutoUpgradeService.exclude_plugin("t1", "p-new", PLUGIN_CATEGORY)
|
||||
@@ -138,14 +137,11 @@ class TestExcludePlugin:
|
||||
def test_removes_from_include_list_in_partial_mode(self):
|
||||
p1, session = _patched_session()
|
||||
existing = MagicMock()
|
||||
existing.upgrade_mode = "partial"
|
||||
existing.upgrade_mode = TenantPluginAutoUpgradeMode.PARTIAL
|
||||
existing.include_plugins = ["p1", "p2"]
|
||||
session.scalar.return_value = existing
|
||||
|
||||
with p1, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls:
|
||||
strat_cls.UpgradeMode.EXCLUDE = "exclude"
|
||||
strat_cls.UpgradeMode.PARTIAL = "partial"
|
||||
strat_cls.UpgradeMode.ALL = "all"
|
||||
with p1, patch(f"{MODULE}.select"):
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
|
||||
result = PluginAutoUpgradeService.exclude_plugin("t1", "p1", PLUGIN_CATEGORY)
|
||||
@@ -156,32 +152,26 @@ class TestExcludePlugin:
|
||||
def test_switches_to_exclude_mode_from_all(self):
|
||||
p1, session = _patched_session()
|
||||
existing = MagicMock()
|
||||
existing.upgrade_mode = "all"
|
||||
existing.upgrade_mode = TenantPluginAutoUpgradeMode.ALL
|
||||
session.scalar.return_value = existing
|
||||
|
||||
with p1, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls:
|
||||
strat_cls.UpgradeMode.EXCLUDE = "exclude"
|
||||
strat_cls.UpgradeMode.PARTIAL = "partial"
|
||||
strat_cls.UpgradeMode.ALL = "all"
|
||||
with p1, patch(f"{MODULE}.select"):
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
|
||||
result = PluginAutoUpgradeService.exclude_plugin("t1", "p1", PLUGIN_CATEGORY)
|
||||
|
||||
assert result is True
|
||||
assert existing.upgrade_mode == "exclude"
|
||||
assert existing.upgrade_mode == TenantPluginAutoUpgradeMode.EXCLUDE
|
||||
assert existing.exclude_plugins == ["p1"]
|
||||
|
||||
def test_no_duplicate_in_exclude_list(self):
|
||||
p1, session = _patched_session()
|
||||
existing = MagicMock()
|
||||
existing.upgrade_mode = "exclude"
|
||||
existing.upgrade_mode = TenantPluginAutoUpgradeMode.EXCLUDE
|
||||
existing.exclude_plugins = ["p1"]
|
||||
session.scalar.return_value = existing
|
||||
|
||||
with p1, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls:
|
||||
strat_cls.UpgradeMode.EXCLUDE = "exclude"
|
||||
strat_cls.UpgradeMode.PARTIAL = "partial"
|
||||
strat_cls.UpgradeMode.ALL = "all"
|
||||
with p1, patch(f"{MODULE}.select"):
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
|
||||
PluginAutoUpgradeService.exclude_plugin("t1", "p1", PLUGIN_CATEGORY)
|
||||
@@ -193,10 +183,10 @@ class TestBackfillStrategyCategories:
|
||||
def test_creates_default_missing_categories_without_fetching_daemon(self):
|
||||
p1, session = _patched_session()
|
||||
tool_strategy = SimpleNamespace(
|
||||
category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
category=TenantPluginAutoUpgradeCategory.TOOL,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
|
||||
upgrade_time_of_day=0,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
upgrade_mode=TenantPluginAutoUpgradeMode.EXCLUDE,
|
||||
exclude_plugins=[],
|
||||
include_plugins=[],
|
||||
)
|
||||
@@ -209,17 +199,15 @@ class TestBackfillStrategyCategories:
|
||||
result = PluginAutoUpgradeService.backfill_strategy_categories("t1")
|
||||
expected_time = PluginAutoUpgradeService.default_upgrade_time_of_day("t1")
|
||||
|
||||
assert result.created_count == len(TenantPluginAutoUpgradeStrategy.PluginCategory) - 1
|
||||
assert result.created_count == len(TenantPluginAutoUpgradeCategory) - 1
|
||||
assert result.normalized is False
|
||||
installer.list_plugins.assert_not_called()
|
||||
assert tool_strategy.upgrade_time_of_day == expected_time
|
||||
created_strategies = [call.args[0] for call in session.add.call_args_list]
|
||||
model_strategy = next(
|
||||
strategy
|
||||
for strategy in created_strategies
|
||||
if strategy.category == TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL
|
||||
strategy for strategy in created_strategies if strategy.category == TenantPluginAutoUpgradeCategory.MODEL
|
||||
)
|
||||
assert model_strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
|
||||
assert model_strategy.strategy_setting == TenantPluginAutoUpgradeStrategySetting.LATEST
|
||||
assert model_strategy.upgrade_time_of_day == expected_time
|
||||
|
||||
def test_default_upgrade_time_is_aligned_to_fifteen_minutes(self):
|
||||
@@ -233,18 +221,18 @@ class TestBackfillStrategyCategories:
|
||||
def test_creates_missing_categories_and_splits_known_plugins(self, caplog: pytest.LogCaptureFixture):
|
||||
p1, session = _patched_session()
|
||||
tool_strategy = SimpleNamespace(
|
||||
category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
category=TenantPluginAutoUpgradeCategory.TOOL,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
|
||||
upgrade_time_of_day=0,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
upgrade_mode=TenantPluginAutoUpgradeMode.EXCLUDE,
|
||||
exclude_plugins=["tool-plugin", "model-plugin", "unknown-plugin"],
|
||||
include_plugins=["model-plugin", "tool-plugin"],
|
||||
)
|
||||
model_strategy = SimpleNamespace(
|
||||
category=TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
category=TenantPluginAutoUpgradeCategory.MODEL,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
|
||||
upgrade_time_of_day=0,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
upgrade_mode=TenantPluginAutoUpgradeMode.EXCLUDE,
|
||||
exclude_plugins=["tool-plugin", "model-plugin", "unknown-plugin"],
|
||||
include_plugins=["model-plugin", "tool-plugin"],
|
||||
)
|
||||
@@ -253,11 +241,11 @@ class TestBackfillStrategyCategories:
|
||||
installed_plugins = [
|
||||
SimpleNamespace(
|
||||
plugin_id="tool-plugin",
|
||||
declaration=SimpleNamespace(category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL),
|
||||
declaration=SimpleNamespace(category=TenantPluginAutoUpgradeCategory.TOOL),
|
||||
),
|
||||
SimpleNamespace(
|
||||
plugin_id="model-plugin",
|
||||
declaration=SimpleNamespace(category=TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL),
|
||||
declaration=SimpleNamespace(category=TenantPluginAutoUpgradeCategory.MODEL),
|
||||
),
|
||||
]
|
||||
installer = MagicMock()
|
||||
@@ -272,9 +260,9 @@ class TestBackfillStrategyCategories:
|
||||
|
||||
result = PluginAutoUpgradeService.backfill_strategy_categories("t1")
|
||||
|
||||
assert result.created_count == len(TenantPluginAutoUpgradeStrategy.PluginCategory) - 2
|
||||
assert result.created_count == len(TenantPluginAutoUpgradeCategory) - 2
|
||||
assert result.normalized is True
|
||||
assert session.add.call_count == len(TenantPluginAutoUpgradeStrategy.PluginCategory) - 2
|
||||
assert session.add.call_count == len(TenantPluginAutoUpgradeCategory) - 2
|
||||
assert tool_strategy.exclude_plugins == ["tool-plugin"]
|
||||
assert tool_strategy.include_plugins == ["tool-plugin"]
|
||||
assert model_strategy.exclude_plugins == ["model-plugin"]
|
||||
|
||||
@@ -87,11 +87,11 @@ def test_check_dependencies_returns_empty_when_no_redis_data(mocker: MockerFixtu
|
||||
|
||||
|
||||
def test_check_dependencies_returns_leaked_deps_from_redis(mocker: MockerFixture) -> None:
|
||||
from core.plugin.entities.plugin import PluginDependency
|
||||
from core.plugin.entities.plugin import PluginDependency, PluginDependencyType
|
||||
from services.rag_pipeline.rag_pipeline_dsl_service import CheckDependenciesPendingData
|
||||
|
||||
dep = PluginDependency(
|
||||
type=PluginDependency.Type.Marketplace,
|
||||
type=PluginDependencyType.Marketplace,
|
||||
value=PluginDependency.Marketplace(marketplace_plugin_unique_identifier="test/plugin:0.1.0"),
|
||||
)
|
||||
pending_data = CheckDependenciesPendingData(
|
||||
@@ -1371,7 +1371,7 @@ def test_confirm_import_fails_when_no_knowledge_index_node(mocker: MockerFixture
|
||||
|
||||
|
||||
def test_create_or_update_pipeline_saves_dependencies_to_redis(mocker: MockerFixture) -> None:
|
||||
from core.plugin.entities.plugin import PluginDependency
|
||||
from core.plugin.entities.plugin import PluginDependency, PluginDependencyType
|
||||
|
||||
session = cast(MagicMock, Mock())
|
||||
service = RagPipelineDslService(session=cast(Session, session))
|
||||
@@ -1386,7 +1386,7 @@ def test_create_or_update_pipeline_saves_dependencies_to_redis(mocker: MockerFix
|
||||
session.scalar.return_value = None
|
||||
setex = mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.redis_client.setex")
|
||||
dependency = PluginDependency(
|
||||
type=PluginDependency.Type.Marketplace,
|
||||
type=PluginDependencyType.Marketplace,
|
||||
value=PluginDependency.Marketplace(marketplace_plugin_unique_identifier="langgenius/example:0.1.0"),
|
||||
)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user